Compare commits
49 Commits
refactor/i
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
502da61b8b | ||
|
|
f2777a9a75 | ||
|
|
4bd3e89f89 | ||
|
|
09e9601940 | ||
|
|
fd4a5c626f | ||
|
|
67cfef82af | ||
|
|
aa18249791 | ||
|
|
9d8fdb4d04 | ||
|
|
fbca9b06c3 | ||
|
|
bb0bf35164 | ||
|
|
5a35c098ec | ||
|
|
ab7c29d228 | ||
|
|
d9c75ed79e | ||
|
|
6fa722df7b | ||
|
|
18864bfecf | ||
|
|
95754cb2b4 | ||
|
|
ed2a461dc5 | ||
|
|
8d5a456dbd | ||
|
|
2528bbb92f | ||
|
|
259cd48dbb | ||
|
|
b43531f200 | ||
|
|
26da3e18a9 | ||
|
|
bb4b640e58 | ||
|
|
1cc845e17d | ||
|
|
60bfb6fe2c | ||
|
|
144d14ab5b | ||
|
|
8f1ca6e282 | ||
|
|
a93758c6b7 | ||
|
|
1829c088cc | ||
|
|
ee1425d0dd | ||
|
|
24ae090916 | ||
|
|
a3aa9b68fc | ||
|
|
50f475334e | ||
|
|
7b18526f24 | ||
|
|
23afc201a1 | ||
|
|
b1982d74a6 | ||
|
|
e93a37c711 | ||
|
|
8d7509cdea | ||
|
|
f5d2e4f11f | ||
|
|
4caf0053cd | ||
|
|
93ce86f32d | ||
|
|
507fe69efe | ||
|
|
23e3739718 | ||
|
|
6daa043a1b | ||
|
|
9dcbc4a126 | ||
|
|
a215860782 | ||
|
|
59b5a50a97 | ||
|
|
d1c9c3583f | ||
|
|
2462492c86 |
@@ -12,8 +12,8 @@ SESSION_SECRET='add some secret here'
|
|||||||
|
|
||||||
# Hoppscotch App Domain Config
|
# Hoppscotch App Domain Config
|
||||||
REDIRECT_URL="http://localhost:3000"
|
REDIRECT_URL="http://localhost:3000"
|
||||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||||
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||||
|
|
||||||
# Google Auth Config
|
# Google Auth Config
|
||||||
GOOGLE_CLIENT_ID="************************************************"
|
GOOGLE_CLIENT_ID="************************************************"
|
||||||
@@ -59,3 +59,6 @@ VITE_BACKEND_API_URL=http://localhost:3170/v1
|
|||||||
# Terms Of Service And Privacy Policy Links (Optional)
|
# Terms Of Service And Privacy Policy Links (Optional)
|
||||||
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
|
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
|
||||||
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
|
VITE_APP_PRIVACY_POLICY_LINK=https://docs.hoppscotch.io/support/privacy
|
||||||
|
|
||||||
|
# Set to `true` for subpath based access
|
||||||
|
ENABLE_SUBPATH_BASED_ACCESS=false
|
||||||
|
|||||||
14
.vscode/extensions.json
vendored
14
.vscode/extensions.json
vendored
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"antfu.iconify",
|
|
||||||
"vue.volar",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"editorconfig.editorconfig",
|
|
||||||
"csstools.postcss",
|
|
||||||
"folke.vscode-monorepo-workspace"
|
|
||||||
],
|
|
||||||
"unwantedRecommendations": [
|
|
||||||
"octref.vetur"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
19
aio-multiport-setup.Caddyfile
Normal file
19
aio-multiport-setup.Caddyfile
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
:3000 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/selfhost-web
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
:3100 {
|
||||||
|
try_files {path} /
|
||||||
|
root * /site/sh-admin-multiport-setup
|
||||||
|
file_server
|
||||||
|
}
|
||||||
|
|
||||||
|
:3170 {
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
:80 {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
37
aio-subpath-access.Caddyfile
Normal file
37
aio-subpath-access.Caddyfile
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
:3000 {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
|
:3100 {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
|
||||||
|
:3170 {
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
:80 {
|
||||||
|
# Serve the `selfhost-web` SPA by default
|
||||||
|
root * /site/selfhost-web
|
||||||
|
file_server
|
||||||
|
|
||||||
|
handle_path /admin* {
|
||||||
|
root * /site/sh-admin-subpath-access
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Ensures any non-existent file in the server is routed to the SPA
|
||||||
|
try_files {path} /
|
||||||
|
}
|
||||||
|
|
||||||
|
# Handle requests under `/backend*` path
|
||||||
|
handle_path /backend* {
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
}
|
||||||
|
|
||||||
|
# Catch-all route for unknown paths, serves `selfhost-web` SPA
|
||||||
|
handle {
|
||||||
|
root * /site/selfhost-web
|
||||||
|
file_server
|
||||||
|
try_files {path} /
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
:3000 {
|
|
||||||
try_files {path} /
|
|
||||||
root * /site/selfhost-web
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
|
|
||||||
:3100 {
|
|
||||||
try_files {path} /
|
|
||||||
root * /site/sh-admin
|
|
||||||
file_server
|
|
||||||
}
|
|
||||||
@@ -49,7 +49,8 @@ execSync(`npx import-meta-env -x build.env -e build.env -p "/site/**/*"`)
|
|||||||
|
|
||||||
fs.rmSync("build.env")
|
fs.rmSync("build.env")
|
||||||
|
|
||||||
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
const caddyFileName = process.env.ENABLE_SUBPATH_BASED_ACCESS === 'true' ? 'aio-subpath-access.Caddyfile' : 'aio-multiport-setup.Caddyfile'
|
||||||
|
const caddyProcess = runChildProcessWithPrefix("caddy", ["run", "--config", `/etc/caddy/${caddyFileName}`, "--adapter", "caddyfile"], "App/Admin Dashboard Caddy")
|
||||||
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
const backendProcess = runChildProcessWithPrefix("pnpm", ["run", "start:prod"], "Backend Server")
|
||||||
|
|
||||||
caddyProcess.on("exit", (code) => {
|
caddyProcess.on("exit", (code) => {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
# Edit the below line to match your PostgresDB URL if you have an outside DB (make sure to update the .env file as well)
|
||||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||||
- PORT=3170
|
- PORT=8080
|
||||||
volumes:
|
volumes:
|
||||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||||
@@ -26,6 +26,7 @@ services:
|
|||||||
hoppscotch-db:
|
hoppscotch-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
|
- "3180:80"
|
||||||
- "3170:3170"
|
- "3170:3170"
|
||||||
|
|
||||||
# The main hoppscotch app. This will be hosted at port 3000
|
# The main hoppscotch app. This will be hosted at port 3000
|
||||||
@@ -42,7 +43,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-backend
|
- hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3000:8080"
|
- "3080:80"
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
# The Self Host dashboard for managing the app. This will be hosted at port 3100
|
# The Self Host dashboard for managing the app. This will be hosted at port 3100
|
||||||
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
# NOTE: To do TLS or play around with how the app is hosted, you can look into the Caddyfile for
|
||||||
@@ -58,7 +60,8 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- hoppscotch-backend
|
- hoppscotch-backend
|
||||||
ports:
|
ports:
|
||||||
- "3100:8080"
|
- "3280:80"
|
||||||
|
- "3100:3100"
|
||||||
|
|
||||||
# The service that spins up all 3 services at once in one container
|
# The service that spins up all 3 services at once in one container
|
||||||
hoppscotch-aio:
|
hoppscotch-aio:
|
||||||
@@ -76,6 +79,7 @@ services:
|
|||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "3100:3100"
|
- "3100:3100"
|
||||||
- "3170:3170"
|
- "3170:3170"
|
||||||
|
- "3080:80"
|
||||||
|
|
||||||
# The preset DB service, you can delete/comment the below lines if
|
# The preset DB service, you can delete/comment the below lines if
|
||||||
# you are using an external postgres instance
|
# you are using an external postgres instance
|
||||||
|
|||||||
3
packages/hoppscotch-backend/backend.Caddyfile
Normal file
3
packages/hoppscotch-backend/backend.Caddyfile
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
:80 :3170 {
|
||||||
|
reverse_proxy localhost:8080
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "hoppscotch-backend",
|
"name": "hoppscotch-backend",
|
||||||
"version": "2023.8.3-1",
|
"version": "2023.8.4-1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[id]` on the table `Shortcode` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Shortcode" ADD COLUMN "embedProperties" JSONB,
|
||||||
|
ADD COLUMN "updatedOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Shortcode_id_key" ON "Shortcode"("id");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Shortcode" ADD CONSTRAINT "Shortcode_creatorUid_fkey" FOREIGN KEY ("creatorUid") REFERENCES "User"("uid") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;
|
||||||
@@ -43,6 +43,7 @@ model TeamInvitation {
|
|||||||
model TeamCollection {
|
model TeamCollection {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
parentID String?
|
parentID String?
|
||||||
|
data Json?
|
||||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||||
requests TeamRequest[]
|
requests TeamRequest[]
|
||||||
@@ -68,10 +69,13 @@ model TeamRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Shortcode {
|
model Shortcode {
|
||||||
id String @id
|
id String @id @unique
|
||||||
request Json
|
request Json
|
||||||
creatorUid String?
|
embedProperties Json?
|
||||||
createdOn DateTime @default(now())
|
creatorUid String?
|
||||||
|
User User? @relation(fields: [creatorUid], references: [uid])
|
||||||
|
createdOn DateTime @default(now())
|
||||||
|
updatedOn DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
||||||
}
|
}
|
||||||
@@ -102,6 +106,7 @@ model User {
|
|||||||
currentGQLSession Json?
|
currentGQLSession Json?
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
invitedUsers InvitedUsers[]
|
invitedUsers InvitedUsers[]
|
||||||
|
shortcodes Shortcode[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Account {
|
model Account {
|
||||||
@@ -192,6 +197,7 @@ model UserCollection {
|
|||||||
userUid String
|
userUid String
|
||||||
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
|
data Json?
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
type ReqType
|
type ReqType
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
|
|||||||
66
packages/hoppscotch-backend/prod_run.mjs
Normal file
66
packages/hoppscotch-backend/prod_run.mjs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/local/bin/node
|
||||||
|
// @ts-check
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import process from 'process';
|
||||||
|
|
||||||
|
function runChildProcessWithPrefix(command, args, prefix) {
|
||||||
|
const childProcess = spawn(command, args);
|
||||||
|
|
||||||
|
childProcess.stdout.on('data', (data) => {
|
||||||
|
const output = data.toString().trim().split('\n');
|
||||||
|
output.forEach((line) => {
|
||||||
|
console.log(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.stderr.on('data', (data) => {
|
||||||
|
const error = data.toString().trim().split('\n');
|
||||||
|
error.forEach((line) => {
|
||||||
|
console.error(`${prefix} | ${line}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('close', (code) => {
|
||||||
|
console.log(`${prefix} Child process exited with code ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on('error', (stuff) => {
|
||||||
|
console.error('error');
|
||||||
|
console.error(stuff);
|
||||||
|
});
|
||||||
|
|
||||||
|
return childProcess;
|
||||||
|
}
|
||||||
|
|
||||||
|
const caddyProcess = runChildProcessWithPrefix(
|
||||||
|
'caddy',
|
||||||
|
['run', '--config', '/etc/caddy/backend.Caddyfile', '--adapter', 'caddyfile'],
|
||||||
|
'App/Admin Dashboard Caddy',
|
||||||
|
);
|
||||||
|
const backendProcess = runChildProcessWithPrefix(
|
||||||
|
'pnpm',
|
||||||
|
['run', 'start:prod'],
|
||||||
|
'Backend Server',
|
||||||
|
);
|
||||||
|
|
||||||
|
caddyProcess.on('exit', (code) => {
|
||||||
|
console.log(`Exiting process because Caddy Server exited with code ${code}`);
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
backendProcess.on('exit', (code) => {
|
||||||
|
console.log(
|
||||||
|
`Exiting process because Backend Server exited with code ${code}`,
|
||||||
|
);
|
||||||
|
process.exit(code);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('SIGINT received, exiting...');
|
||||||
|
|
||||||
|
caddyProcess.kill('SIGINT');
|
||||||
|
backendProcess.kill('SIGINT');
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ObjectType } from '@nestjs/graphql';
|
import { ObjectType, OmitType } from '@nestjs/graphql';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Admin {}
|
export class Admin extends OmitType(User, [
|
||||||
|
'isAdmin',
|
||||||
|
'currentRESTSession',
|
||||||
|
'currentGQLSession',
|
||||||
|
]) {}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { TeamInvitationModule } from '../team-invitation/team-invitation.module'
|
|||||||
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
import { TeamEnvironmentsModule } from '../team-environments/team-environments.module';
|
||||||
import { TeamCollectionModule } from '../team-collection/team-collection.module';
|
import { TeamCollectionModule } from '../team-collection/team-collection.module';
|
||||||
import { TeamRequestModule } from '../team-request/team-request.module';
|
import { TeamRequestModule } from '../team-request/team-request.module';
|
||||||
|
import { InfraResolver } from './infra.resolver';
|
||||||
|
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -22,8 +24,9 @@ import { TeamRequestModule } from '../team-request/team-request.module';
|
|||||||
TeamEnvironmentsModule,
|
TeamEnvironmentsModule,
|
||||||
TeamCollectionModule,
|
TeamCollectionModule,
|
||||||
TeamRequestModule,
|
TeamRequestModule,
|
||||||
|
ShortcodeModule,
|
||||||
],
|
],
|
||||||
providers: [AdminResolver, AdminService],
|
providers: [InfraResolver, AdminResolver, AdminService],
|
||||||
exports: [AdminService],
|
exports: [AdminService],
|
||||||
})
|
})
|
||||||
export class AdminModule {}
|
export class AdminModule {}
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ import { InvitedUser } from './invited-user.model';
|
|||||||
import { GqlUser } from '../decorators/gql-user.decorator';
|
import { GqlUser } from '../decorators/gql-user.decorator';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { Team, TeamMember } from '../team/team.model';
|
import { Team, TeamMember } from '../team/team.model';
|
||||||
import { User } from '../user/user.model';
|
|
||||||
import { TeamInvitation } from '../team-invitation/team-invitation.model';
|
|
||||||
import { PaginationArgs } from '../types/input-types.args';
|
|
||||||
import {
|
import {
|
||||||
AddUserToTeamArgs,
|
AddUserToTeamArgs,
|
||||||
ChangeUserRoleInTeamArgs,
|
ChangeUserRoleInTeamArgs,
|
||||||
} from './input-types.args';
|
} from './input-types.args';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
|
import { PaginationArgs } from 'src/types/input-types.args';
|
||||||
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Admin)
|
@Resolver(() => Admin)
|
||||||
@@ -51,6 +51,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [User], {
|
@ResolveField(() => [User], {
|
||||||
description: 'Returns a list of all admin users in infra',
|
description: 'Returns a list of all admin users in infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async admins() {
|
async admins() {
|
||||||
@@ -59,6 +60,7 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
@ResolveField(() => User, {
|
@ResolveField(() => User, {
|
||||||
description: 'Returns a user info by UID',
|
description: 'Returns a user info by UID',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async userInfo(
|
async userInfo(
|
||||||
@@ -76,6 +78,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [User], {
|
@ResolveField(() => [User], {
|
||||||
description: 'Returns a list of all the users in infra',
|
description: 'Returns a list of all the users in infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
async allUsers(
|
async allUsers(
|
||||||
@@ -88,6 +91,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [InvitedUser], {
|
@ResolveField(() => [InvitedUser], {
|
||||||
description: 'Returns a list of all the invited users',
|
description: 'Returns a list of all the invited users',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||||
const users = await this.adminService.fetchInvitedUsers();
|
const users = await this.adminService.fetchInvitedUsers();
|
||||||
@@ -96,6 +100,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [Team], {
|
@ResolveField(() => [Team], {
|
||||||
description: 'Returns a list of all the teams in the infra',
|
description: 'Returns a list of all the teams in the infra',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async allTeams(
|
async allTeams(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -106,6 +111,7 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
@ResolveField(() => Team, {
|
@ResolveField(() => Team, {
|
||||||
description: 'Returns a team info by ID when requested by Admin',
|
description: 'Returns a team info by ID when requested by Admin',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async teamInfo(
|
async teamInfo(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -123,6 +129,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return count of all the members in a team',
|
description: 'Return count of all the members in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async membersCountInTeam(
|
async membersCountInTeam(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -140,6 +147,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return count of all the stored collections in a team',
|
description: 'Return count of all the stored collections in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async collectionCountInTeam(
|
async collectionCountInTeam(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -155,6 +163,7 @@ export class AdminResolver {
|
|||||||
}
|
}
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return count of all the stored requests in a team',
|
description: 'Return count of all the stored requests in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async requestCountInTeam(
|
async requestCountInTeam(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -171,6 +180,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return count of all the stored environments in a team',
|
description: 'Return count of all the stored environments in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async environmentCountInTeam(
|
async environmentCountInTeam(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -187,6 +197,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => [TeamInvitation], {
|
@ResolveField(() => [TeamInvitation], {
|
||||||
description: 'Return all the pending invitations in a team',
|
description: 'Return all the pending invitations in a team',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async pendingInvitationCountInTeam(
|
async pendingInvitationCountInTeam(
|
||||||
@Parent() admin: Admin,
|
@Parent() admin: Admin,
|
||||||
@@ -205,6 +216,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return total number of Users in organization',
|
description: 'Return total number of Users in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async usersCount() {
|
async usersCount() {
|
||||||
return this.adminService.getUsersCount();
|
return this.adminService.getUsersCount();
|
||||||
@@ -212,6 +224,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return total number of Teams in organization',
|
description: 'Return total number of Teams in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async teamsCount() {
|
async teamsCount() {
|
||||||
return this.adminService.getTeamsCount();
|
return this.adminService.getTeamsCount();
|
||||||
@@ -219,6 +232,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return total number of Team Collections in organization',
|
description: 'Return total number of Team Collections in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async teamCollectionsCount() {
|
async teamCollectionsCount() {
|
||||||
return this.adminService.getTeamCollectionsCount();
|
return this.adminService.getTeamCollectionsCount();
|
||||||
@@ -226,6 +240,7 @@ export class AdminResolver {
|
|||||||
|
|
||||||
@ResolveField(() => Number, {
|
@ResolveField(() => Number, {
|
||||||
description: 'Return total number of Team Requests in organization',
|
description: 'Return total number of Team Requests in organization',
|
||||||
|
deprecationReason: 'Use `infra` query instead',
|
||||||
})
|
})
|
||||||
async teamRequestsCount() {
|
async teamRequestsCount() {
|
||||||
return this.adminService.getTeamRequestsCount();
|
return this.adminService.getTeamRequestsCount();
|
||||||
@@ -428,6 +443,23 @@ export class AdminResolver {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Boolean, {
|
||||||
|
description: 'Revoke Shortcode by ID',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async revokeShortcodeByAdmin(
|
||||||
|
@Args({
|
||||||
|
name: 'code',
|
||||||
|
description: 'The shortcode to delete',
|
||||||
|
type: () => ID,
|
||||||
|
})
|
||||||
|
code: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const res = await this.adminService.deleteShortcode(code);
|
||||||
|
if (E.isLeft(res)) throwErr(res.left);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/* Subscriptions */
|
/* Subscriptions */
|
||||||
|
|
||||||
@Subscription(() => InvitedUser, {
|
@Subscription(() => InvitedUser, {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
INVALID_EMAIL,
|
INVALID_EMAIL,
|
||||||
USER_ALREADY_INVITED,
|
USER_ALREADY_INVITED,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -25,6 +26,7 @@ const mockTeamRequestService = mockDeep<TeamRequestService>();
|
|||||||
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
||||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||||
const mockMailerService = mockDeep<MailerService>();
|
const mockMailerService = mockDeep<MailerService>();
|
||||||
|
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||||
|
|
||||||
const adminService = new AdminService(
|
const adminService = new AdminService(
|
||||||
mockUserService,
|
mockUserService,
|
||||||
@@ -36,6 +38,7 @@ const adminService = new AdminService(
|
|||||||
mockPubSub as any,
|
mockPubSub as any,
|
||||||
mockPrisma as any,
|
mockPrisma as any,
|
||||||
mockMailerService,
|
mockMailerService,
|
||||||
|
mockShortcodeService,
|
||||||
);
|
);
|
||||||
|
|
||||||
const invitedUsers: InvitedUsers[] = [
|
const invitedUsers: InvitedUsers[] = [
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { TeamRequestService } from '../team-request/team-request.service';
|
|||||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||||
import { TeamMemberRole } from '../team/team.model';
|
import { TeamMemberRole } from '../team/team.model';
|
||||||
|
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AdminService {
|
export class AdminService {
|
||||||
@@ -37,6 +38,7 @@ export class AdminService {
|
|||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly mailerService: MailerService,
|
private readonly mailerService: MailerService,
|
||||||
|
private readonly shortcodeService: ShortcodeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -432,4 +434,35 @@ export class AdminService {
|
|||||||
|
|
||||||
return E.right(teamInvite.right);
|
return E.right(teamInvite.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all created ShortCodes
|
||||||
|
*
|
||||||
|
* @param args Pagination arguments
|
||||||
|
* @param userEmail User email
|
||||||
|
* @returns ShortcodeWithUserEmail
|
||||||
|
*/
|
||||||
|
async fetchAllShortcodes(
|
||||||
|
cursorID: string,
|
||||||
|
take: number,
|
||||||
|
userEmail: string = null,
|
||||||
|
) {
|
||||||
|
return this.shortcodeService.fetchAllShortcodes(
|
||||||
|
{ cursor: cursorID, take },
|
||||||
|
userEmail,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Shortcode
|
||||||
|
*
|
||||||
|
* @param shortcodeID ID of Shortcode being deleted
|
||||||
|
* @returns Boolean on successful deletion
|
||||||
|
*/
|
||||||
|
async deleteShortcode(shortcodeID: string) {
|
||||||
|
const result = await this.shortcodeService.deleteShortcode(shortcodeID);
|
||||||
|
|
||||||
|
if (E.isLeft(result)) return E.left(result.left);
|
||||||
|
return E.right(result.right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
packages/hoppscotch-backend/src/admin/infra.model.ts
Normal file
10
packages/hoppscotch-backend/src/admin/infra.model.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { Admin } from './admin.model';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class Infra {
|
||||||
|
@Field(() => Admin, {
|
||||||
|
description: 'Admin who executed the action',
|
||||||
|
})
|
||||||
|
executedBy: Admin;
|
||||||
|
}
|
||||||
225
packages/hoppscotch-backend/src/admin/infra.resolver.ts
Normal file
225
packages/hoppscotch-backend/src/admin/infra.resolver.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { UseGuards } from '@nestjs/common';
|
||||||
|
import { Args, ID, Query, ResolveField, Resolver } from '@nestjs/graphql';
|
||||||
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
|
import { Infra } from './infra.model';
|
||||||
|
import { AdminService } from './admin.service';
|
||||||
|
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||||
|
import { GqlAdminGuard } from './guards/gql-admin.guard';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { throwErr } from 'src/utils';
|
||||||
|
import * as E from 'fp-ts/Either';
|
||||||
|
import { Admin } from './admin.model';
|
||||||
|
import { PaginationArgs } from 'src/types/input-types.args';
|
||||||
|
import { InvitedUser } from './invited-user.model';
|
||||||
|
import { Team } from 'src/team/team.model';
|
||||||
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
|
import { GqlAdmin } from './decorators/gql-admin.decorator';
|
||||||
|
import { ShortcodeWithUserEmail } from 'src/shortcode/shortcode.model';
|
||||||
|
|
||||||
|
@UseGuards(GqlThrottlerGuard)
|
||||||
|
@Resolver(() => Infra)
|
||||||
|
export class InfraResolver {
|
||||||
|
constructor(private adminService: AdminService) {}
|
||||||
|
|
||||||
|
@Query(() => Infra, {
|
||||||
|
description: 'Fetch details of the Infrastructure',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
infra(@GqlAdmin() admin: Admin) {
|
||||||
|
const infra: Infra = { executedBy: admin };
|
||||||
|
return infra;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [User], {
|
||||||
|
description: 'Returns a list of all admin users in infra',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async admins() {
|
||||||
|
const admins = await this.adminService.fetchAdmins();
|
||||||
|
return admins;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => User, {
|
||||||
|
description: 'Returns a user info by UID',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async userInfo(
|
||||||
|
@Args({
|
||||||
|
name: 'userUid',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'The user UID',
|
||||||
|
})
|
||||||
|
userUid: string,
|
||||||
|
): Promise<AuthUser> {
|
||||||
|
const user = await this.adminService.fetchUserInfo(userUid);
|
||||||
|
if (E.isLeft(user)) throwErr(user.left);
|
||||||
|
return user.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [User], {
|
||||||
|
description: 'Returns a list of all the users in infra',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||||
|
async allUsers(@Args() args: PaginationArgs): Promise<AuthUser[]> {
|
||||||
|
const users = await this.adminService.fetchUsers(args.cursor, args.take);
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [InvitedUser], {
|
||||||
|
description: 'Returns a list of all the invited users',
|
||||||
|
})
|
||||||
|
async invitedUsers(): Promise<InvitedUser[]> {
|
||||||
|
const users = await this.adminService.fetchInvitedUsers();
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [Team], {
|
||||||
|
description: 'Returns a list of all the teams in the infra',
|
||||||
|
})
|
||||||
|
async allTeams(@Args() args: PaginationArgs): Promise<Team[]> {
|
||||||
|
const teams = await this.adminService.fetchAllTeams(args.cursor, args.take);
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Team, {
|
||||||
|
description: 'Returns a team info by ID when requested by Admin',
|
||||||
|
})
|
||||||
|
async teamInfo(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which info to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<Team> {
|
||||||
|
const team = await this.adminService.getTeamInfo(teamID);
|
||||||
|
if (E.isLeft(team)) throwErr(team.left);
|
||||||
|
return team.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the members in a team',
|
||||||
|
})
|
||||||
|
async membersCountInTeam(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
nullable: false,
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamMembersCount = await this.adminService.membersCountInTeam(teamID);
|
||||||
|
return teamMembersCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored collections in a team',
|
||||||
|
})
|
||||||
|
async collectionCountInTeam(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamCollCount = await this.adminService.collectionCountInTeam(teamID);
|
||||||
|
return teamCollCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored requests in a team',
|
||||||
|
})
|
||||||
|
async requestCountInTeam(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const teamReqCount = await this.adminService.requestCountInTeam(teamID);
|
||||||
|
return teamReqCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return count of all the stored environments in a team',
|
||||||
|
})
|
||||||
|
async environmentCountInTeam(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
): Promise<number> {
|
||||||
|
const envsCount = await this.adminService.environmentCountInTeam(teamID);
|
||||||
|
return envsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [TeamInvitation], {
|
||||||
|
description: 'Return all the pending invitations in a team',
|
||||||
|
})
|
||||||
|
async pendingInvitationCountInTeam(
|
||||||
|
@Args({
|
||||||
|
name: 'teamID',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'Team ID for which team members to fetch',
|
||||||
|
})
|
||||||
|
teamID: string,
|
||||||
|
) {
|
||||||
|
const invitations = await this.adminService.pendingInvitationCountInTeam(
|
||||||
|
teamID,
|
||||||
|
);
|
||||||
|
return invitations;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Users in organization',
|
||||||
|
})
|
||||||
|
async usersCount() {
|
||||||
|
return this.adminService.getUsersCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Teams in organization',
|
||||||
|
})
|
||||||
|
async teamsCount() {
|
||||||
|
return this.adminService.getTeamsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Team Collections in organization',
|
||||||
|
})
|
||||||
|
async teamCollectionsCount() {
|
||||||
|
return this.adminService.getTeamCollectionsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => Number, {
|
||||||
|
description: 'Return total number of Team Requests in organization',
|
||||||
|
})
|
||||||
|
async teamRequestsCount() {
|
||||||
|
return this.adminService.getTeamRequestsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [ShortcodeWithUserEmail], {
|
||||||
|
description: 'Returns a list of all the shortcodes in the infra',
|
||||||
|
})
|
||||||
|
async allShortcodes(
|
||||||
|
@Args() args: PaginationArgs,
|
||||||
|
@Args({
|
||||||
|
name: 'userEmail',
|
||||||
|
nullable: true,
|
||||||
|
description: 'Users email to filter shortcodes by',
|
||||||
|
})
|
||||||
|
userEmail: string,
|
||||||
|
) {
|
||||||
|
return await this.adminService.fetchAllShortcodes(
|
||||||
|
args.cursor,
|
||||||
|
args.take,
|
||||||
|
userEmail,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -254,6 +254,13 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
|
|||||||
*/
|
*/
|
||||||
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Team Collection data is not valid
|
||||||
|
* (TeamCollectionService)
|
||||||
|
*/
|
||||||
|
export const TEAM_COLL_DATA_INVALID =
|
||||||
|
'team_coll/team_coll_data_invalid' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tried to perform an action on a request that doesn't accept their member role level
|
* Tried to perform an action on a request that doesn't accept their member role level
|
||||||
* (GqlRequestTeamMemberGuard)
|
* (GqlRequestTeamMemberGuard)
|
||||||
@@ -318,18 +325,6 @@ export const TEAM_INVITATION_NOT_FOUND =
|
|||||||
*/
|
*/
|
||||||
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
|
export const SHORTCODE_NOT_FOUND = 'shortcode/not_found' as const;
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalid ShortCode format
|
|
||||||
* (ShortcodeService)
|
|
||||||
*/
|
|
||||||
export const SHORTCODE_INVALID_JSON = 'shortcode/invalid_json' as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ShortCode already exists in DB
|
|
||||||
* (ShortcodeService)
|
|
||||||
*/
|
|
||||||
export const SHORTCODE_ALREADY_EXISTS = 'shortcode/already_exists' as const;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invalid or non-existent TEAM ENVIRONMENT ID
|
* Invalid or non-existent TEAM ENVIRONMENT ID
|
||||||
* (TeamEnvironmentsService)
|
* (TeamEnvironmentsService)
|
||||||
@@ -597,6 +592,13 @@ export const USER_COLL_REORDERING_FAILED =
|
|||||||
export const USER_COLL_SAME_NEXT_COLL =
|
export const USER_COLL_SAME_NEXT_COLL =
|
||||||
'user_coll/user_collection_and_next_user_collection_are_same' as const;
|
'user_coll/user_collection_and_next_user_collection_are_same' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The User Collection data is not valid
|
||||||
|
* (UserCollectionService)
|
||||||
|
*/
|
||||||
|
export const USER_COLL_DATA_INVALID =
|
||||||
|
'user_coll/user_coll_data_invalid' as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The User Collection does not belong to the logged-in user
|
* The User Collection does not belong to the logged-in user
|
||||||
* (UserCollectionService)
|
* (UserCollectionService)
|
||||||
@@ -621,3 +623,24 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
|
|||||||
*/
|
*/
|
||||||
export const MAILER_FROM_ADDRESS_UNDEFINED =
|
export const MAILER_FROM_ADDRESS_UNDEFINED =
|
||||||
'mailer/from_address_undefined' as const;
|
'mailer/from_address_undefined' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedRequest invalid request JSON format
|
||||||
|
* (ShortcodeService)
|
||||||
|
*/
|
||||||
|
export const SHORTCODE_INVALID_REQUEST_JSON =
|
||||||
|
'shortcode/request_invalid_format' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedRequest invalid properties JSON format
|
||||||
|
* (ShortcodeService)
|
||||||
|
*/
|
||||||
|
export const SHORTCODE_INVALID_PROPERTIES_JSON =
|
||||||
|
'shortcode/properties_invalid_format' as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SharedRequest invalid properties not found
|
||||||
|
* (ShortcodeService)
|
||||||
|
*/
|
||||||
|
export const SHORTCODE_PROPERTIES_NOT_FOUND =
|
||||||
|
'shortcode/properties_not_found' as const;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { UserRequestUserCollectionResolver } from './user-request/resolvers/user
|
|||||||
import { UserEnvsUserResolver } from './user-environment/user.resolver';
|
import { UserEnvsUserResolver } from './user-environment/user.resolver';
|
||||||
import { UserHistoryUserResolver } from './user-history/user.resolver';
|
import { UserHistoryUserResolver } from './user-history/user.resolver';
|
||||||
import { UserSettingsUserResolver } from './user-settings/user.resolver';
|
import { UserSettingsUserResolver } from './user-settings/user.resolver';
|
||||||
|
import { InfraResolver } from './admin/infra.resolver';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All the resolvers present in the application.
|
* All the resolvers present in the application.
|
||||||
@@ -34,6 +35,7 @@ import { UserSettingsUserResolver } from './user-settings/user.resolver';
|
|||||||
* NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate
|
* NOTE: This needs to be KEPT UP-TO-DATE to keep the schema accurate
|
||||||
*/
|
*/
|
||||||
const RESOLVERS = [
|
const RESOLVERS = [
|
||||||
|
InfraResolver,
|
||||||
AdminResolver,
|
AdminResolver,
|
||||||
ShortcodeResolver,
|
ShortcodeResolver,
|
||||||
TeamResolver,
|
TeamResolver,
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import {
|
|||||||
} from 'src/team-request/team-request.model';
|
} from 'src/team-request/team-request.model';
|
||||||
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
|
||||||
import { InvitedUser } from '../admin/invited-user.model';
|
import { InvitedUser } from '../admin/invited-user.model';
|
||||||
import { UserCollection } from '@prisma/client';
|
|
||||||
import {
|
import {
|
||||||
|
UserCollection,
|
||||||
UserCollectionRemovedData,
|
UserCollectionRemovedData,
|
||||||
UserCollectionReorderData,
|
UserCollectionReorderData,
|
||||||
} from 'src/user-collection/user-collections.model';
|
} from 'src/user-collection/user-collections.model';
|
||||||
@@ -69,5 +69,7 @@ export type TopicDef = {
|
|||||||
[topic: `team_req/${string}/req_deleted`]: string;
|
[topic: `team_req/${string}/req_deleted`]: string;
|
||||||
[topic: `team/${string}/invite_added`]: TeamInvitation;
|
[topic: `team/${string}/invite_added`]: TeamInvitation;
|
||||||
[topic: `team/${string}/invite_removed`]: string;
|
[topic: `team/${string}/invite_removed`]: string;
|
||||||
[topic: `shortcode/${string}/${'created' | 'revoked'}`]: Shortcode;
|
[
|
||||||
|
topic: `shortcode/${string}/${'created' | 'revoked' | 'updated'}`
|
||||||
|
]: Shortcode;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
import { Field, ID, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { User } from 'src/user/user.model';
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class Shortcode {
|
export class Shortcode {
|
||||||
@Field(() => ID, {
|
@Field(() => ID, {
|
||||||
description: 'The shortcode. 12 digit alphanumeric.',
|
description: 'The 12 digit alphanumeric code',
|
||||||
})
|
})
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@@ -12,8 +13,57 @@ export class Shortcode {
|
|||||||
})
|
})
|
||||||
request: string;
|
request: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'JSON string representing the properties for an embed',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
properties: string;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Timestamp of when the Shortcode was created',
|
description: 'Timestamp of when the Shortcode was created',
|
||||||
})
|
})
|
||||||
createdOn: Date;
|
createdOn: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ShortcodeCreator {
|
||||||
|
@Field({
|
||||||
|
description: 'Uid of user who created the shortcode',
|
||||||
|
})
|
||||||
|
uid: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'Email of user who created the shortcode',
|
||||||
|
})
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class ShortcodeWithUserEmail {
|
||||||
|
@Field(() => ID, {
|
||||||
|
description: 'The 12 digit alphanumeric code',
|
||||||
|
})
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'JSON string representing the request data',
|
||||||
|
})
|
||||||
|
request: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'JSON string representing the properties for an embed',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
properties: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'Timestamp of when the Shortcode was created',
|
||||||
|
})
|
||||||
|
createdOn: Date;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'Details of user who created the shortcode',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
creator: ShortcodeCreator;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||||
import { UserModule } from 'src/user/user.module';
|
import { UserModule } from 'src/user/user.module';
|
||||||
@@ -7,14 +6,7 @@ import { ShortcodeResolver } from './shortcode.resolver';
|
|||||||
import { ShortcodeService } from './shortcode.service';
|
import { ShortcodeService } from './shortcode.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [PrismaModule, UserModule, PubSubModule],
|
||||||
PrismaModule,
|
|
||||||
UserModule,
|
|
||||||
PubSubModule,
|
|
||||||
JwtModule.register({
|
|
||||||
secret: process.env.JWT_SECRET,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
providers: [ShortcodeService, ShortcodeResolver],
|
providers: [ShortcodeService, ShortcodeResolver],
|
||||||
exports: [ShortcodeService],
|
exports: [ShortcodeService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
Args,
|
Args,
|
||||||
Context,
|
|
||||||
ID,
|
ID,
|
||||||
Mutation,
|
Mutation,
|
||||||
Query,
|
Query,
|
||||||
@@ -9,28 +8,25 @@ import {
|
|||||||
} from '@nestjs/graphql';
|
} from '@nestjs/graphql';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Shortcode } from './shortcode.model';
|
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||||
import { ShortcodeService } from './shortcode.service';
|
import { ShortcodeService } from './shortcode.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
|
||||||
import { throwErr } from 'src/utils';
|
import { throwErr } from 'src/utils';
|
||||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||||
import { User } from 'src/user/user.model';
|
import { User } from 'src/user/user.model';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from '../types/AuthUser';
|
import { AuthUser } from '../types/AuthUser';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
|
||||||
import { PaginationArgs } from 'src/types/input-types.args';
|
import { PaginationArgs } from 'src/types/input-types.args';
|
||||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||||
import { SkipThrottle } from '@nestjs/throttler';
|
import { SkipThrottle } from '@nestjs/throttler';
|
||||||
|
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
|
||||||
|
|
||||||
@UseGuards(GqlThrottlerGuard)
|
@UseGuards(GqlThrottlerGuard)
|
||||||
@Resolver(() => Shortcode)
|
@Resolver(() => Shortcode)
|
||||||
export class ShortcodeResolver {
|
export class ShortcodeResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly shortcodeService: ShortcodeService,
|
private readonly shortcodeService: ShortcodeService,
|
||||||
private readonly userService: UserService,
|
|
||||||
private readonly pubsub: PubSubService,
|
private readonly pubsub: PubSubService,
|
||||||
private jwtService: JwtService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/* Queries */
|
/* Queries */
|
||||||
@@ -64,20 +60,53 @@ export class ShortcodeResolver {
|
|||||||
@Mutation(() => Shortcode, {
|
@Mutation(() => Shortcode, {
|
||||||
description: 'Create a shortcode for the given request.',
|
description: 'Create a shortcode for the given request.',
|
||||||
})
|
})
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
async createShortcode(
|
async createShortcode(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
@Args({
|
@Args({
|
||||||
name: 'request',
|
name: 'request',
|
||||||
description: 'JSON string of the request object',
|
description: 'JSON string of the request object',
|
||||||
})
|
})
|
||||||
request: string,
|
request: string,
|
||||||
@Context() ctx: any,
|
@Args({
|
||||||
|
name: 'properties',
|
||||||
|
description: 'JSON string of the properties of the embed',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
properties: string,
|
||||||
) {
|
) {
|
||||||
const decodedAccessToken = this.jwtService.verify(
|
|
||||||
ctx.req.cookies['access_token'],
|
|
||||||
);
|
|
||||||
const result = await this.shortcodeService.createShortcode(
|
const result = await this.shortcodeService.createShortcode(
|
||||||
request,
|
request,
|
||||||
decodedAccessToken?.sub,
|
properties,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(result)) throwErr(result.left);
|
||||||
|
return result.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Shortcode, {
|
||||||
|
description: 'Update a user generated Shortcode',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
|
async updateEmbedProperties(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
|
@Args({
|
||||||
|
name: 'code',
|
||||||
|
type: () => ID,
|
||||||
|
description: 'The Shortcode to update',
|
||||||
|
})
|
||||||
|
code: string,
|
||||||
|
@Args({
|
||||||
|
name: 'properties',
|
||||||
|
description: 'JSON string of the properties of the embed',
|
||||||
|
})
|
||||||
|
properties: string,
|
||||||
|
) {
|
||||||
|
const result = await this.shortcodeService.updateEmbedProperties(
|
||||||
|
code,
|
||||||
|
user.uid,
|
||||||
|
properties,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(result)) throwErr(result.left);
|
if (E.isLeft(result)) throwErr(result.left);
|
||||||
@@ -93,7 +122,7 @@ export class ShortcodeResolver {
|
|||||||
@Args({
|
@Args({
|
||||||
name: 'code',
|
name: 'code',
|
||||||
type: () => ID,
|
type: () => ID,
|
||||||
description: 'The shortcode to resolve',
|
description: 'The shortcode to remove',
|
||||||
})
|
})
|
||||||
code: string,
|
code: string,
|
||||||
) {
|
) {
|
||||||
@@ -114,6 +143,16 @@ export class ShortcodeResolver {
|
|||||||
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
|
return this.pubsub.asyncIterator(`shortcode/${user.uid}/created`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Subscription(() => Shortcode, {
|
||||||
|
description: 'Listen for Shortcode updates',
|
||||||
|
resolve: (value) => value,
|
||||||
|
})
|
||||||
|
@SkipThrottle()
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
|
myShortcodesUpdated(@GqlUser() user: AuthUser) {
|
||||||
|
return this.pubsub.asyncIterator(`shortcode/${user.uid}/updated`);
|
||||||
|
}
|
||||||
|
|
||||||
@Subscription(() => Shortcode, {
|
@Subscription(() => Shortcode, {
|
||||||
description: 'Listen for shortcode deletion',
|
description: 'Listen for shortcode deletion',
|
||||||
resolve: (value) => value,
|
resolve: (value) => value,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import {
|
import {
|
||||||
SHORTCODE_ALREADY_EXISTS,
|
INVALID_EMAIL,
|
||||||
SHORTCODE_INVALID_JSON,
|
SHORTCODE_INVALID_PROPERTIES_JSON,
|
||||||
|
SHORTCODE_INVALID_REQUEST_JSON,
|
||||||
SHORTCODE_NOT_FOUND,
|
SHORTCODE_NOT_FOUND,
|
||||||
|
SHORTCODE_PROPERTIES_NOT_FOUND,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { Shortcode } from './shortcode.model';
|
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||||
import { ShortcodeService } from './shortcode.service';
|
import { ShortcodeService } from './shortcode.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ const mockFB = {
|
|||||||
doc: mockDocFunc,
|
doc: mockDocFunc,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const mockUserService = new UserService(mockFB as any, mockPubSub as any);
|
const mockUserService = new UserService(mockPrisma as any, mockPubSub as any);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -38,18 +41,34 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
const createdOn = new Date();
|
const createdOn = new Date();
|
||||||
|
|
||||||
const shortCodeWithOutUser = {
|
const user: AuthUser = {
|
||||||
id: '123',
|
uid: '123344',
|
||||||
request: '{}',
|
email: 'dwight@dundermifflin.com',
|
||||||
|
displayName: 'Dwight Schrute',
|
||||||
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
|
isAdmin: false,
|
||||||
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
createdOn: createdOn,
|
createdOn: createdOn,
|
||||||
creatorUid: null,
|
currentGQLSession: {},
|
||||||
|
currentRESTSession: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const shortCodeWithUser = {
|
const mockEmbed = {
|
||||||
id: '123',
|
id: '123',
|
||||||
request: '{}',
|
request: '{}',
|
||||||
|
embedProperties: '{}',
|
||||||
createdOn: createdOn,
|
createdOn: createdOn,
|
||||||
creatorUid: 'user_uid_1',
|
creatorUid: user.uid,
|
||||||
|
updatedOn: createdOn,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockShortcode = {
|
||||||
|
id: '123',
|
||||||
|
request: '{}',
|
||||||
|
embedProperties: null,
|
||||||
|
createdOn: createdOn,
|
||||||
|
creatorUid: user.uid,
|
||||||
|
updatedOn: createdOn,
|
||||||
};
|
};
|
||||||
|
|
||||||
const shortcodes = [
|
const shortcodes = [
|
||||||
@@ -58,33 +77,67 @@ const shortcodes = [
|
|||||||
request: {
|
request: {
|
||||||
hello: 'there',
|
hello: 'there',
|
||||||
},
|
},
|
||||||
creatorUid: 'testuser',
|
embedProperties: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
creatorUid: user.uid,
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
|
updatedOn: createdOn,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'blablabla1',
|
id: 'blablabla1',
|
||||||
request: {
|
request: {
|
||||||
hello: 'there',
|
hello: 'there',
|
||||||
},
|
},
|
||||||
creatorUid: 'testuser',
|
embedProperties: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
creatorUid: user.uid,
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
|
updatedOn: createdOn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const shortcodesWithUserEmail = [
|
||||||
|
{
|
||||||
|
id: 'blablabla',
|
||||||
|
request: {
|
||||||
|
hello: 'there',
|
||||||
|
},
|
||||||
|
embedProperties: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
creatorUid: user.uid,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: createdOn,
|
||||||
|
User: user,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'blablabla1',
|
||||||
|
request: {
|
||||||
|
hello: 'there',
|
||||||
|
},
|
||||||
|
embedProperties: {
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
creatorUid: user.uid,
|
||||||
|
createdOn: new Date(),
|
||||||
|
updatedOn: createdOn,
|
||||||
|
User: user,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('ShortcodeService', () => {
|
describe('ShortcodeService', () => {
|
||||||
describe('getShortCode', () => {
|
describe('getShortCode', () => {
|
||||||
test('should return a valid shortcode with valid shortcode ID', async () => {
|
test('should return a valid Shortcode with valid Shortcode ID', async () => {
|
||||||
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(
|
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed);
|
||||||
shortCodeWithOutUser,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await shortcodeService.getShortCode(
|
const result = await shortcodeService.getShortCode(mockEmbed.id);
|
||||||
shortCodeWithOutUser.id,
|
|
||||||
);
|
|
||||||
expect(result).toEqualRight(<Shortcode>{
|
expect(result).toEqualRight(<Shortcode>{
|
||||||
id: shortCodeWithOutUser.id,
|
id: mockEmbed.id,
|
||||||
createdOn: shortCodeWithOutUser.createdOn,
|
createdOn: mockEmbed.createdOn,
|
||||||
request: JSON.stringify(shortCodeWithOutUser.request),
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -99,10 +152,10 @@ describe('ShortcodeService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('fetchUserShortCodes', () => {
|
describe('fetchUserShortCodes', () => {
|
||||||
test('should return list of shortcodes with valid inputs and no cursor', async () => {
|
test('should return list of Shortcode with valid inputs and no cursor', async () => {
|
||||||
mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes);
|
mockPrisma.shortcode.findMany.mockResolvedValueOnce(shortcodes);
|
||||||
|
|
||||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||||
cursor: null,
|
cursor: null,
|
||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
@@ -110,20 +163,22 @@ describe('ShortcodeService', () => {
|
|||||||
{
|
{
|
||||||
id: shortcodes[0].id,
|
id: shortcodes[0].id,
|
||||||
request: JSON.stringify(shortcodes[0].request),
|
request: JSON.stringify(shortcodes[0].request),
|
||||||
|
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||||
createdOn: shortcodes[0].createdOn,
|
createdOn: shortcodes[0].createdOn,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: shortcodes[1].id,
|
id: shortcodes[1].id,
|
||||||
request: JSON.stringify(shortcodes[1].request),
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
createdOn: shortcodes[1].createdOn,
|
createdOn: shortcodes[1].createdOn,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return list of shortcodes with valid inputs and cursor', async () => {
|
test('should return list of Shortcode with valid inputs and cursor', async () => {
|
||||||
mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]);
|
mockPrisma.shortcode.findMany.mockResolvedValue([shortcodes[1]]);
|
||||||
|
|
||||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||||
cursor: 'blablabla',
|
cursor: 'blablabla',
|
||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
@@ -131,6 +186,7 @@ describe('ShortcodeService', () => {
|
|||||||
{
|
{
|
||||||
id: shortcodes[1].id,
|
id: shortcodes[1].id,
|
||||||
request: JSON.stringify(shortcodes[1].request),
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
createdOn: shortcodes[1].createdOn,
|
createdOn: shortcodes[1].createdOn,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -139,7 +195,7 @@ describe('ShortcodeService', () => {
|
|||||||
test('should return an empty array for an invalid cursor', async () => {
|
test('should return an empty array for an invalid cursor', async () => {
|
||||||
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||||
cursor: 'invalidcursor',
|
cursor: 'invalidcursor',
|
||||||
take: 10,
|
take: 10,
|
||||||
});
|
});
|
||||||
@@ -171,77 +227,111 @@ describe('ShortcodeService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('createShortcode', () => {
|
describe('createShortcode', () => {
|
||||||
test('should throw SHORTCODE_INVALID_JSON error if incoming request data is invalid', async () => {
|
test('should throw SHORTCODE_INVALID_REQUEST_JSON error if incoming request data is invalid', async () => {
|
||||||
const result = await shortcodeService.createShortcode(
|
const result = await shortcodeService.createShortcode(
|
||||||
'invalidRequest',
|
'invalidRequest',
|
||||||
'user_uid_1',
|
null,
|
||||||
|
user,
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(SHORTCODE_INVALID_JSON);
|
expect(result).toEqualLeft(SHORTCODE_INVALID_REQUEST_JSON);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully create a new shortcode with valid user uid', async () => {
|
test('should throw SHORTCODE_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => {
|
||||||
// generateUniqueShortCodeID --> getShortCode
|
const result = await shortcodeService.createShortcode(
|
||||||
|
'{}',
|
||||||
|
'invalid_data',
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully create a new Embed with valid user uid', async () => {
|
||||||
|
// generateUniqueShortCodeID --> getShortcode
|
||||||
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
'NotFoundError',
|
'NotFoundError',
|
||||||
);
|
);
|
||||||
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
|
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
|
||||||
|
|
||||||
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
|
const result = await shortcodeService.createShortcode('{}', '{}', user);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight(<Shortcode>{
|
||||||
id: shortCodeWithUser.id,
|
id: mockEmbed.id,
|
||||||
createdOn: shortCodeWithUser.createdOn,
|
createdOn: mockEmbed.createdOn,
|
||||||
request: JSON.stringify(shortCodeWithUser.request),
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully create a new shortcode with null user uid', async () => {
|
test('should successfully create a new ShortCode with valid user uid', async () => {
|
||||||
// generateUniqueShortCodeID --> getShortCode
|
// generateUniqueShortCodeID --> getShortcode
|
||||||
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
'NotFoundError',
|
'NotFoundError',
|
||||||
);
|
);
|
||||||
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
|
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode);
|
||||||
|
|
||||||
const result = await shortcodeService.createShortcode('{}', null);
|
const result = await shortcodeService.createShortcode('{}', null, user);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight(<Shortcode>{
|
||||||
id: shortCodeWithUser.id,
|
id: mockShortcode.id,
|
||||||
createdOn: shortCodeWithUser.createdOn,
|
createdOn: mockShortcode.createdOn,
|
||||||
request: JSON.stringify(shortCodeWithOutUser.request),
|
request: JSON.stringify(mockShortcode.request),
|
||||||
|
properties: mockShortcode.embedProperties,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => {
|
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of a Shortcode', async () => {
|
||||||
// generateUniqueShortCodeID --> getShortCode
|
// generateUniqueShortCodeID --> getShortcode
|
||||||
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
'NotFoundError',
|
'NotFoundError',
|
||||||
);
|
);
|
||||||
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
|
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode);
|
||||||
|
|
||||||
|
const result = await shortcodeService.createShortcode('{}', null, user);
|
||||||
|
|
||||||
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`shortcode/${shortCodeWithUser.creatorUid}/created`,
|
`shortcode/${mockShortcode.creatorUid}/created`,
|
||||||
{
|
<Shortcode>{
|
||||||
id: shortCodeWithUser.id,
|
id: mockShortcode.id,
|
||||||
createdOn: shortCodeWithUser.createdOn,
|
createdOn: mockShortcode.createdOn,
|
||||||
request: JSON.stringify(shortCodeWithUser.request),
|
request: JSON.stringify(mockShortcode.request),
|
||||||
|
properties: mockShortcode.embedProperties,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of an Embed', async () => {
|
||||||
|
// generateUniqueShortCodeID --> getShortcode
|
||||||
|
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||||
|
'NotFoundError',
|
||||||
|
);
|
||||||
|
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
|
||||||
|
|
||||||
|
const result = await shortcodeService.createShortcode('{}', '{}', user);
|
||||||
|
|
||||||
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
|
`shortcode/${mockEmbed.creatorUid}/created`,
|
||||||
|
<Shortcode>{
|
||||||
|
id: mockEmbed.id,
|
||||||
|
createdOn: mockEmbed.createdOn,
|
||||||
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('revokeShortCode', () => {
|
describe('revokeShortCode', () => {
|
||||||
test('should return true on successful deletion of shortcode with valid inputs', async () => {
|
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
|
||||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
|
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
|
||||||
|
|
||||||
const result = await shortcodeService.revokeShortCode(
|
const result = await shortcodeService.revokeShortCode(
|
||||||
shortCodeWithUser.id,
|
mockEmbed.id,
|
||||||
shortCodeWithUser.creatorUid,
|
mockEmbed.creatorUid,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
|
expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
|
||||||
where: {
|
where: {
|
||||||
creator_uid_shortcode_unique: {
|
creator_uid_shortcode_unique: {
|
||||||
creatorUid: shortCodeWithUser.creatorUid,
|
creatorUid: mockEmbed.creatorUid,
|
||||||
id: shortCodeWithUser.id,
|
id: mockEmbed.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -249,52 +339,53 @@ describe('ShortcodeService', () => {
|
|||||||
expect(result).toEqualRight(true);
|
expect(result).toEqualRight(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return SHORTCODE_NOT_FOUND error when shortcode is invalid and user uid is valid', async () => {
|
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid and user uid is valid', async () => {
|
||||||
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||||
expect(
|
expect(
|
||||||
shortcodeService.revokeShortCode('invalid', 'testuser'),
|
shortcodeService.revokeShortCode('invalid', 'testuser'),
|
||||||
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return SHORTCODE_NOT_FOUND error when shortcode is valid and user uid is invalid', async () => {
|
test('should return SHORTCODE_NOT_FOUND error when Shortcode is valid and user uid is invalid', async () => {
|
||||||
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||||
expect(
|
expect(
|
||||||
shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
|
shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
|
||||||
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return SHORTCODE_NOT_FOUND error when both shortcode and user uid are invalid', async () => {
|
test('should return SHORTCODE_NOT_FOUND error when both Shortcode and user uid are invalid', async () => {
|
||||||
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||||
expect(
|
expect(
|
||||||
shortcodeService.revokeShortCode('invalid', 'invalid'),
|
shortcodeService.revokeShortCode('invalid', 'invalid'),
|
||||||
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => {
|
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of Shortcode', async () => {
|
||||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
|
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
|
||||||
|
|
||||||
const result = await shortcodeService.revokeShortCode(
|
const result = await shortcodeService.revokeShortCode(
|
||||||
shortCodeWithUser.id,
|
mockEmbed.id,
|
||||||
shortCodeWithUser.creatorUid,
|
mockEmbed.creatorUid,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`shortcode/${shortCodeWithUser.creatorUid}/revoked`,
|
`shortcode/${mockEmbed.creatorUid}/revoked`,
|
||||||
{
|
{
|
||||||
id: shortCodeWithUser.id,
|
id: mockEmbed.id,
|
||||||
createdOn: shortCodeWithUser.createdOn,
|
createdOn: mockEmbed.createdOn,
|
||||||
request: JSON.stringify(shortCodeWithUser.request),
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteUserShortCodes', () => {
|
describe('deleteUserShortCodes', () => {
|
||||||
test('should successfully delete all users shortcodes with valid user uid', async () => {
|
test('should successfully delete all users Shortcodes with valid user uid', async () => {
|
||||||
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 });
|
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 1 });
|
||||||
|
|
||||||
const result = await shortcodeService.deleteUserShortCodes(
|
const result = await shortcodeService.deleteUserShortCodes(
|
||||||
shortCodeWithUser.creatorUid,
|
mockEmbed.creatorUid,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(1);
|
expect(result).toEqual(1);
|
||||||
});
|
});
|
||||||
@@ -303,9 +394,176 @@ describe('ShortcodeService', () => {
|
|||||||
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
|
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
|
||||||
|
|
||||||
const result = await shortcodeService.deleteUserShortCodes(
|
const result = await shortcodeService.deleteUserShortCodes(
|
||||||
shortCodeWithUser.creatorUid,
|
mockEmbed.creatorUid,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(0);
|
expect(result).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateShortcode', () => {
|
||||||
|
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid', async () => {
|
||||||
|
const result = await shortcodeService.updateEmbedProperties(
|
||||||
|
mockEmbed.id,
|
||||||
|
user.uid,
|
||||||
|
'',
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(SHORTCODE_PROPERTIES_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return SHORTCODE_PROPERTIES_NOT_FOUND error when updatedProps in invalid JSON format', async () => {
|
||||||
|
const result = await shortcodeService.updateEmbedProperties(
|
||||||
|
mockEmbed.id,
|
||||||
|
user.uid,
|
||||||
|
'{kk',
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(SHORTCODE_INVALID_PROPERTIES_JSON);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return SHORTCODE_NOT_FOUND error when Shortcode ID is invalid', async () => {
|
||||||
|
mockPrisma.shortcode.update.mockRejectedValue('RecordNotFound');
|
||||||
|
const result = await shortcodeService.updateEmbedProperties(
|
||||||
|
'invalidID',
|
||||||
|
user.uid,
|
||||||
|
'{}',
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully update a Shortcodes with valid inputs', async () => {
|
||||||
|
mockPrisma.shortcode.update.mockResolvedValueOnce({
|
||||||
|
...mockEmbed,
|
||||||
|
embedProperties: '{"foo":"bar"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await shortcodeService.updateEmbedProperties(
|
||||||
|
mockEmbed.id,
|
||||||
|
user.uid,
|
||||||
|
'{"foo":"bar"}',
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight({
|
||||||
|
id: mockEmbed.id,
|
||||||
|
createdOn: mockEmbed.createdOn,
|
||||||
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify('{"foo":"bar"}'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send pubsub message to `shortcode/{uid}/updated` on successful Update of Shortcode', async () => {
|
||||||
|
mockPrisma.shortcode.update.mockResolvedValueOnce({
|
||||||
|
...mockEmbed,
|
||||||
|
embedProperties: '{"foo":"bar"}',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await shortcodeService.updateEmbedProperties(
|
||||||
|
mockEmbed.id,
|
||||||
|
user.uid,
|
||||||
|
'{"foo":"bar"}',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
|
`shortcode/${mockEmbed.creatorUid}/updated`,
|
||||||
|
{
|
||||||
|
id: mockEmbed.id,
|
||||||
|
createdOn: mockEmbed.createdOn,
|
||||||
|
request: JSON.stringify(mockEmbed.request),
|
||||||
|
properties: JSON.stringify('{"foo":"bar"}'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteShortcode', () => {
|
||||||
|
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
|
||||||
|
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
|
||||||
|
|
||||||
|
const result = await shortcodeService.deleteShortcode(mockEmbed.id);
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return SHORTCODE_NOT_FOUND error when Shortcode is invalid', async () => {
|
||||||
|
mockPrisma.shortcode.delete.mockRejectedValue('RecordNotFound');
|
||||||
|
|
||||||
|
expect(shortcodeService.deleteShortcode('invalid')).resolves.toEqualLeft(
|
||||||
|
SHORTCODE_NOT_FOUND,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAllShortcodes', () => {
|
||||||
|
test('should return list of Shortcodes with valid inputs and no cursor', async () => {
|
||||||
|
mockPrisma.shortcode.findMany.mockResolvedValueOnce(
|
||||||
|
shortcodesWithUserEmail,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await shortcodeService.fetchAllShortcodes(
|
||||||
|
{
|
||||||
|
cursor: null,
|
||||||
|
take: 10,
|
||||||
|
},
|
||||||
|
user.email,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||||
|
{
|
||||||
|
id: shortcodes[0].id,
|
||||||
|
request: JSON.stringify(shortcodes[0].request),
|
||||||
|
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||||
|
createdOn: shortcodes[0].createdOn,
|
||||||
|
creator: {
|
||||||
|
uid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: shortcodes[1].id,
|
||||||
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
|
createdOn: shortcodes[1].createdOn,
|
||||||
|
creator: {
|
||||||
|
uid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return list of Shortcode with valid inputs and cursor', async () => {
|
||||||
|
mockPrisma.shortcode.findMany.mockResolvedValue([
|
||||||
|
shortcodesWithUserEmail[1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = await shortcodeService.fetchAllShortcodes(
|
||||||
|
{
|
||||||
|
cursor: 'blablabla',
|
||||||
|
take: 10,
|
||||||
|
},
|
||||||
|
user.email,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(<ShortcodeWithUserEmail[]>[
|
||||||
|
{
|
||||||
|
id: shortcodes[1].id,
|
||||||
|
request: JSON.stringify(shortcodes[1].request),
|
||||||
|
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||||
|
createdOn: shortcodes[1].createdOn,
|
||||||
|
creator: {
|
||||||
|
uid: user.uid,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return an empty array for an invalid cursor', async () => {
|
||||||
|
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||||
|
|
||||||
|
const result = await shortcodeService.fetchAllShortcodes(
|
||||||
|
{
|
||||||
|
cursor: 'invalidcursor',
|
||||||
|
take: 10,
|
||||||
|
},
|
||||||
|
user.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||||
import * as T from 'fp-ts/Task';
|
import * as T from 'fp-ts/Task';
|
||||||
import * as O from 'fp-ts/Option';
|
|
||||||
import * as TO from 'fp-ts/TaskOption';
|
import * as TO from 'fp-ts/TaskOption';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { SHORTCODE_INVALID_JSON, SHORTCODE_NOT_FOUND } from 'src/errors';
|
import {
|
||||||
|
SHORTCODE_INVALID_PROPERTIES_JSON,
|
||||||
|
SHORTCODE_INVALID_REQUEST_JSON,
|
||||||
|
SHORTCODE_NOT_FOUND,
|
||||||
|
SHORTCODE_PROPERTIES_NOT_FOUND,
|
||||||
|
} from 'src/errors';
|
||||||
import { UserDataHandler } from 'src/user/user.data.handler';
|
import { UserDataHandler } from 'src/user/user.data.handler';
|
||||||
import { Shortcode } from './shortcode.model';
|
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||||
import { Shortcode as DBShortCode } from '@prisma/client';
|
import { Shortcode as DBShortCode } from '@prisma/client';
|
||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { UserService } from 'src/user/user.service';
|
import { UserService } from 'src/user/user.service';
|
||||||
@@ -46,10 +50,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
* @param shortcodeInfo Prisma Shortcode type
|
* @param shortcodeInfo Prisma Shortcode type
|
||||||
* @returns GQL Shortcode
|
* @returns GQL Shortcode
|
||||||
*/
|
*/
|
||||||
private returnShortCode(shortcodeInfo: DBShortCode): Shortcode {
|
private cast(shortcodeInfo: DBShortCode): Shortcode {
|
||||||
return <Shortcode>{
|
return <Shortcode>{
|
||||||
id: shortcodeInfo.id,
|
id: shortcodeInfo.id,
|
||||||
request: JSON.stringify(shortcodeInfo.request),
|
request: JSON.stringify(shortcodeInfo.request),
|
||||||
|
properties:
|
||||||
|
shortcodeInfo.embedProperties != null
|
||||||
|
? JSON.stringify(shortcodeInfo.embedProperties)
|
||||||
|
: null,
|
||||||
createdOn: shortcodeInfo.createdOn,
|
createdOn: shortcodeInfo.createdOn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -94,7 +102,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
|
const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
|
||||||
where: { id: shortcode },
|
where: { id: shortcode },
|
||||||
});
|
});
|
||||||
return E.right(this.returnShortCode(shortcodeInfo));
|
return E.right(this.cast(shortcodeInfo));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return E.left(SHORTCODE_NOT_FOUND);
|
return E.left(SHORTCODE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -104,14 +112,22 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
* Create a new ShortCode
|
* Create a new ShortCode
|
||||||
*
|
*
|
||||||
* @param request JSON string of request details
|
* @param request JSON string of request details
|
||||||
* @param userUID user UID, if present
|
* @param userInfo user UI
|
||||||
|
* @param properties JSON string of embed properties, if present
|
||||||
* @returns Either of ShortCode or error
|
* @returns Either of ShortCode or error
|
||||||
*/
|
*/
|
||||||
async createShortcode(request: string, userUID: string | null) {
|
async createShortcode(
|
||||||
const shortcodeData = stringToJson(request);
|
request: string,
|
||||||
if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON);
|
properties: string | null = null,
|
||||||
|
userInfo: AuthUser,
|
||||||
|
) {
|
||||||
|
const requestData = stringToJson(request);
|
||||||
|
if (E.isLeft(requestData) || !requestData.right)
|
||||||
|
return E.left(SHORTCODE_INVALID_REQUEST_JSON);
|
||||||
|
|
||||||
const user = await this.userService.findUserById(userUID);
|
const parsedProperties = stringToJson(properties);
|
||||||
|
if (E.isLeft(parsedProperties))
|
||||||
|
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
|
||||||
|
|
||||||
const generatedShortCode = await this.generateUniqueShortCodeID();
|
const generatedShortCode = await this.generateUniqueShortCodeID();
|
||||||
if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left);
|
if (E.isLeft(generatedShortCode)) return E.left(generatedShortCode.left);
|
||||||
@@ -119,8 +135,9 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
const createdShortCode = await this.prisma.shortcode.create({
|
const createdShortCode = await this.prisma.shortcode.create({
|
||||||
data: {
|
data: {
|
||||||
id: generatedShortCode.right,
|
id: generatedShortCode.right,
|
||||||
request: shortcodeData.right,
|
request: requestData.right,
|
||||||
creatorUid: O.isNone(user) ? null : user.value.uid,
|
embedProperties: parsedProperties.right ?? undefined,
|
||||||
|
creatorUid: userInfo.uid,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,11 +145,11 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
if (createdShortCode.creatorUid) {
|
if (createdShortCode.creatorUid) {
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`shortcode/${createdShortCode.creatorUid}/created`,
|
`shortcode/${createdShortCode.creatorUid}/created`,
|
||||||
this.returnShortCode(createdShortCode),
|
this.cast(createdShortCode),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return E.right(this.returnShortCode(createdShortCode));
|
return E.right(this.cast(createdShortCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,14 +173,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
|
const fetchedShortCodes: Shortcode[] = shortCodes.map((code) =>
|
||||||
this.returnShortCode(code),
|
this.cast(code),
|
||||||
);
|
);
|
||||||
|
|
||||||
return fetchedShortCodes;
|
return fetchedShortCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a ShortCode
|
* Delete a ShortCode created by User of uid
|
||||||
*
|
*
|
||||||
* @param shortcode ShortCode
|
* @param shortcode ShortCode
|
||||||
* @param uid User Uid
|
* @param uid User Uid
|
||||||
@@ -182,7 +199,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`shortcode/${deletedShortCodes.creatorUid}/revoked`,
|
`shortcode/${deletedShortCodes.creatorUid}/revoked`,
|
||||||
this.returnShortCode(deletedShortCodes),
|
this.cast(deletedShortCodes),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -205,4 +222,118 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
|||||||
|
|
||||||
return deletedShortCodes.count;
|
return deletedShortCodes.count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Shortcode
|
||||||
|
*
|
||||||
|
* @param shortcodeID ID of Shortcode being deleted
|
||||||
|
* @returns Boolean on successful deletion
|
||||||
|
*/
|
||||||
|
async deleteShortcode(shortcodeID: string) {
|
||||||
|
try {
|
||||||
|
await this.prisma.shortcode.delete({
|
||||||
|
where: {
|
||||||
|
id: shortcodeID,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return E.right(true);
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(SHORTCODE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a created Shortcode
|
||||||
|
* @param shortcodeID Shortcode ID
|
||||||
|
* @param uid User Uid
|
||||||
|
* @returns Updated Shortcode
|
||||||
|
*/
|
||||||
|
async updateEmbedProperties(
|
||||||
|
shortcodeID: string,
|
||||||
|
uid: string,
|
||||||
|
updatedProps: string,
|
||||||
|
) {
|
||||||
|
if (!updatedProps) return E.left(SHORTCODE_PROPERTIES_NOT_FOUND);
|
||||||
|
|
||||||
|
const parsedProperties = stringToJson(updatedProps);
|
||||||
|
if (E.isLeft(parsedProperties) || !parsedProperties.right)
|
||||||
|
return E.left(SHORTCODE_INVALID_PROPERTIES_JSON);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedShortcode = await this.prisma.shortcode.update({
|
||||||
|
where: {
|
||||||
|
creator_uid_shortcode_unique: {
|
||||||
|
creatorUid: uid,
|
||||||
|
id: shortcodeID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
embedProperties: parsedProperties.right,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`shortcode/${updatedShortcode.creatorUid}/updated`,
|
||||||
|
this.cast(updatedShortcode),
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(this.cast(updatedShortcode));
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(SHORTCODE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all created ShortCodes
|
||||||
|
*
|
||||||
|
* @param args Pagination arguments
|
||||||
|
* @param userEmail User email
|
||||||
|
* @returns ShortcodeWithUserEmail
|
||||||
|
*/
|
||||||
|
async fetchAllShortcodes(
|
||||||
|
args: PaginationArgs,
|
||||||
|
userEmail: string | null = null,
|
||||||
|
) {
|
||||||
|
const shortCodes = await this.prisma.shortcode.findMany({
|
||||||
|
where: userEmail
|
||||||
|
? {
|
||||||
|
User: {
|
||||||
|
email: userEmail,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
orderBy: {
|
||||||
|
createdOn: 'desc',
|
||||||
|
},
|
||||||
|
skip: args.cursor ? 1 : 0,
|
||||||
|
take: args.take,
|
||||||
|
cursor: args.cursor ? { id: args.cursor } : undefined,
|
||||||
|
include: {
|
||||||
|
User: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchedShortCodes: ShortcodeWithUserEmail[] = shortCodes.map(
|
||||||
|
(code) => {
|
||||||
|
return <ShortcodeWithUserEmail>{
|
||||||
|
id: code.id,
|
||||||
|
request: JSON.stringify(code.request),
|
||||||
|
properties:
|
||||||
|
code.embedProperties != null
|
||||||
|
? JSON.stringify(code.embedProperties)
|
||||||
|
: null,
|
||||||
|
createdOn: code.createdOn,
|
||||||
|
creator: code.User
|
||||||
|
? {
|
||||||
|
uid: code.User.uid,
|
||||||
|
email: code.User.email,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return fetchedShortCodes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ export class CreateRootTeamCollectionArgs {
|
|||||||
|
|
||||||
@Field({ name: 'title', description: 'Title of the new collection' })
|
@Field({ name: 'title', description: 'Title of the new collection' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -26,6 +33,13 @@ export class CreateChildTeamCollectionArgs {
|
|||||||
|
|
||||||
@Field({ name: 'childTitle', description: 'Title of the new collection' })
|
@Field({ name: 'childTitle', description: 'Title of the new collection' })
|
||||||
childTitle: string;
|
childTitle: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -33,12 +47,14 @@ export class RenameTeamCollectionArgs {
|
|||||||
@Field(() => ID, {
|
@Field(() => ID, {
|
||||||
name: 'collectionID',
|
name: 'collectionID',
|
||||||
description: 'ID of the collection',
|
description: 'ID of the collection',
|
||||||
|
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
||||||
})
|
})
|
||||||
collectionID: string;
|
collectionID: string;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
name: 'newTitle',
|
name: 'newTitle',
|
||||||
description: 'The updated title of the collection',
|
description: 'The updated title of the collection',
|
||||||
|
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
||||||
})
|
})
|
||||||
newTitle: string;
|
newTitle: string;
|
||||||
}
|
}
|
||||||
@@ -98,3 +114,26 @@ export class ReplaceTeamCollectionArgs {
|
|||||||
})
|
})
|
||||||
parentCollectionID?: string;
|
parentCollectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class UpdateTeamCollectionArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'collectionID',
|
||||||
|
description: 'ID of the collection',
|
||||||
|
})
|
||||||
|
collectionID: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'newTitle',
|
||||||
|
description: 'The updated title of the collection',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
newTitle: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ export class TeamCollection {
|
|||||||
})
|
})
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
|
|
||||||
@Field(() => ID, {
|
@Field(() => ID, {
|
||||||
description: 'ID of the collection',
|
description: 'ID of the collection',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
parentID: string;
|
parentID: string;
|
||||||
teamID: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
MoveTeamCollectionArgs,
|
MoveTeamCollectionArgs,
|
||||||
RenameTeamCollectionArgs,
|
RenameTeamCollectionArgs,
|
||||||
ReplaceTeamCollectionArgs,
|
ReplaceTeamCollectionArgs,
|
||||||
|
UpdateTeamCollectionArgs,
|
||||||
UpdateTeamCollectionOrderArgs,
|
UpdateTeamCollectionOrderArgs,
|
||||||
} from './input-type.args';
|
} from './input-type.args';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
@@ -141,7 +142,14 @@ export class TeamCollectionResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
|
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
|
||||||
return teamCollections.right;
|
return <TeamCollection>{
|
||||||
|
id: teamCollections.right.id,
|
||||||
|
title: teamCollections.right.title,
|
||||||
|
parentID: teamCollections.right.parentID,
|
||||||
|
data: !teamCollections.right.data
|
||||||
|
? null
|
||||||
|
: JSON.stringify(teamCollections.right.data),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mutations
|
// Mutations
|
||||||
@@ -155,6 +163,7 @@ export class TeamCollectionResolver {
|
|||||||
const teamCollection = await this.teamCollectionService.createCollection(
|
const teamCollection = await this.teamCollectionService.createCollection(
|
||||||
args.teamID,
|
args.teamID,
|
||||||
args.title,
|
args.title,
|
||||||
|
args.data,
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -230,6 +239,7 @@ export class TeamCollectionResolver {
|
|||||||
const teamCollection = await this.teamCollectionService.createCollection(
|
const teamCollection = await this.teamCollectionService.createCollection(
|
||||||
team.right.id,
|
team.right.id,
|
||||||
args.childTitle,
|
args.childTitle,
|
||||||
|
args.data,
|
||||||
args.collectionID,
|
args.collectionID,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -239,6 +249,7 @@ export class TeamCollectionResolver {
|
|||||||
|
|
||||||
@Mutation(() => TeamCollection, {
|
@Mutation(() => TeamCollection, {
|
||||||
description: 'Rename a collection',
|
description: 'Rename a collection',
|
||||||
|
deprecationReason: 'Switch to updateTeamCollection mutation instead',
|
||||||
})
|
})
|
||||||
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
||||||
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
@@ -303,6 +314,23 @@ export class TeamCollectionResolver {
|
|||||||
return request.right;
|
return request.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => TeamCollection, {
|
||||||
|
description: 'Update Team Collection details',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
|
||||||
|
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
|
||||||
|
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
|
||||||
|
const updatedTeamCollection =
|
||||||
|
await this.teamCollectionService.updateTeamCollection(
|
||||||
|
args.collectionID,
|
||||||
|
args.data,
|
||||||
|
args.newTitle,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
|
||||||
|
return updatedTeamCollection.right;
|
||||||
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
|
|
||||||
@Subscription(() => TeamCollection, {
|
@Subscription(() => TeamCollection, {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
|
TEAM_COLL_DATA_INVALID,
|
||||||
TEAM_COLL_DEST_SAME,
|
TEAM_COLL_DEST_SAME,
|
||||||
TEAM_COLL_INVALID_JSON,
|
TEAM_COLL_INVALID_JSON,
|
||||||
TEAM_COLL_IS_PARENT_COLL,
|
TEAM_COLL_IS_PARENT_COLL,
|
||||||
@@ -17,6 +18,7 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
|||||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
import { TeamCollectionService } from './team-collection.service';
|
import { TeamCollectionService } from './team-collection.service';
|
||||||
|
import { TeamCollection } from './team-collection.model';
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
@@ -51,35 +53,60 @@ const rootTeamCollection: DBTeamCollection = {
|
|||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
data: {},
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rootTeamCollectionsCasted: TeamCollection = {
|
||||||
|
id: rootTeamCollection.id,
|
||||||
|
title: rootTeamCollection.title,
|
||||||
|
parentID: rootTeamCollection.parentID,
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
};
|
||||||
|
|
||||||
const rootTeamCollection_2: DBTeamCollection = {
|
const rootTeamCollection_2: DBTeamCollection = {
|
||||||
id: 'erv',
|
id: 'erv',
|
||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
data: {},
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rootTeamCollection_2Casted: TeamCollection = {
|
||||||
|
id: 'erv',
|
||||||
|
parentID: null,
|
||||||
|
data: JSON.stringify(rootTeamCollection_2.data),
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
};
|
||||||
|
|
||||||
const childTeamCollection: DBTeamCollection = {
|
const childTeamCollection: DBTeamCollection = {
|
||||||
id: 'rfe',
|
id: 'rfe',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
title: 'Child Collection 1',
|
title: 'Child Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const childTeamCollectionCasted: TeamCollection = {
|
||||||
|
id: 'rfe',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify(childTeamCollection.data),
|
||||||
|
title: 'Child Collection 1',
|
||||||
|
};
|
||||||
|
|
||||||
const childTeamCollection_2: DBTeamCollection = {
|
const childTeamCollection_2: DBTeamCollection = {
|
||||||
id: 'bgdz',
|
id: 'bgdz',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
|
data: {},
|
||||||
parentID: rootTeamCollection_2.id,
|
parentID: rootTeamCollection_2.id,
|
||||||
title: 'Child Collection 1',
|
title: 'Child Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
@@ -87,11 +114,20 @@ const childTeamCollection_2: DBTeamCollection = {
|
|||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const childTeamCollection_2Casted: TeamCollection = {
|
||||||
|
id: 'bgdz',
|
||||||
|
data: JSON.stringify(childTeamCollection_2.data),
|
||||||
|
parentID: rootTeamCollection_2.id,
|
||||||
|
title: 'Child Collection 1',
|
||||||
|
};
|
||||||
|
|
||||||
const rootTeamCollectionList: DBTeamCollection[] = [
|
const rootTeamCollectionList: DBTeamCollection[] = [
|
||||||
{
|
{
|
||||||
id: 'fdv',
|
id: 'fdv',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -102,6 +138,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -111,6 +149,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 3,
|
orderIndex: 3,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -119,6 +159,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: 'bre3',
|
id: 'bre3',
|
||||||
orderIndex: 4,
|
orderIndex: 4,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -129,6 +171,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 5,
|
orderIndex: 5,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -139,6 +183,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
@@ -148,6 +194,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
@@ -156,6 +204,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 8,
|
orderIndex: 8,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -165,6 +214,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 9,
|
orderIndex: 9,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -175,17 +225,83 @@ const rootTeamCollectionList: DBTeamCollection[] = [
|
|||||||
parentID: null,
|
parentID: null,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
|
data: {},
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const rootTeamCollectionListCasted: TeamCollection[] = [
|
||||||
|
{
|
||||||
|
id: 'fdv',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fbbg',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fgbfg',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bre3',
|
||||||
|
parentID: null,
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hghgf',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '54tyh',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '234re',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '34rtg',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '45tgh',
|
||||||
|
parentID: null,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify(rootTeamCollection.data),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const childTeamCollectionList: DBTeamCollection[] = [
|
const childTeamCollectionList: DBTeamCollection[] = [
|
||||||
{
|
{
|
||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -195,6 +311,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 2,
|
orderIndex: 2,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -204,6 +322,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
orderIndex: 3,
|
orderIndex: 3,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
|
data: {},
|
||||||
|
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
updatedOn: currentTime,
|
updatedOn: currentTime,
|
||||||
@@ -212,6 +332,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '567',
|
id: '567',
|
||||||
orderIndex: 4,
|
orderIndex: 4,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -221,6 +343,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '123',
|
id: '123',
|
||||||
orderIndex: 5,
|
orderIndex: 5,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -230,6 +354,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '678',
|
id: '678',
|
||||||
orderIndex: 6,
|
orderIndex: 6,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -239,6 +365,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '789',
|
id: '789',
|
||||||
orderIndex: 7,
|
orderIndex: 7,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -248,6 +376,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '890',
|
id: '890',
|
||||||
orderIndex: 8,
|
orderIndex: 8,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -257,6 +387,7 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '012',
|
id: '012',
|
||||||
orderIndex: 9,
|
orderIndex: 9,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -266,6 +397,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
id: '0bhu',
|
id: '0bhu',
|
||||||
orderIndex: 10,
|
orderIndex: 10,
|
||||||
parentID: rootTeamCollection.id,
|
parentID: rootTeamCollection.id,
|
||||||
|
data: {},
|
||||||
|
|
||||||
title: 'Root Collection 1',
|
title: 'Root Collection 1',
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
@@ -273,6 +406,75 @@ const childTeamCollectionList: DBTeamCollection[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const childTeamCollectionListCasted: TeamCollection[] = [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '345',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '456',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '567',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '678',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '789',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '890',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '012',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '0bhu',
|
||||||
|
parentID: rootTeamCollection.id,
|
||||||
|
data: JSON.stringify({}),
|
||||||
|
|
||||||
|
title: 'Root Collection 1',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockReset(mockPrisma);
|
mockReset(mockPrisma);
|
||||||
mockPubSub.publish.mockClear();
|
mockPubSub.publish.mockClear();
|
||||||
@@ -311,7 +513,7 @@ describe('getParentOfCollection', () => {
|
|||||||
const result = await teamCollectionService.getParentOfCollection(
|
const result = await teamCollectionService.getParentOfCollection(
|
||||||
childTeamCollection.id,
|
childTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(rootTeamCollection);
|
expect(result).toEqual(rootTeamCollectionsCasted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return null successfully for a root collection with valid collectionID', async () => {
|
test('should return null successfully for a root collection with valid collectionID', async () => {
|
||||||
@@ -347,7 +549,7 @@ describe('getChildrenOfCollection', () => {
|
|||||||
null,
|
null,
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(childTeamCollectionList);
|
expect(result).toEqual(childTeamCollectionListCasted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
|
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||||
@@ -363,9 +565,9 @@ describe('getChildrenOfCollection', () => {
|
|||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ ...childTeamCollectionList[7] },
|
{ ...childTeamCollectionListCasted[7] },
|
||||||
{ ...childTeamCollectionList[8] },
|
{ ...childTeamCollectionListCasted[8] },
|
||||||
{ ...childTeamCollectionList[9] },
|
{ ...childTeamCollectionListCasted[9] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,7 +594,7 @@ describe('getTeamRootCollections', () => {
|
|||||||
null,
|
null,
|
||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual(rootTeamCollectionList);
|
expect(result).toEqual(rootTeamCollectionListCasted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
|
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
|
||||||
@@ -408,9 +610,9 @@ describe('getTeamRootCollections', () => {
|
|||||||
10,
|
10,
|
||||||
);
|
);
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ ...rootTeamCollectionList[7] },
|
{ ...rootTeamCollectionListCasted[7] },
|
||||||
{ ...rootTeamCollectionList[8] },
|
{ ...rootTeamCollectionListCasted[8] },
|
||||||
{ ...rootTeamCollectionList[9] },
|
{ ...rootTeamCollectionListCasted[9] },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -464,6 +666,7 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'ab',
|
'ab',
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
||||||
@@ -478,11 +681,27 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcd',
|
'abcd',
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualLeft(TEAM_NOT_OWNER);
|
expect(result).toEqualLeft(TEAM_NOT_OWNER);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
|
||||||
|
// isOwnerCheck
|
||||||
|
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
|
rootTeamCollection,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await teamCollectionService.createCollection(
|
||||||
|
rootTeamCollection.teamID,
|
||||||
|
'abcd',
|
||||||
|
'{',
|
||||||
|
rootTeamCollection.id,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
|
||||||
|
});
|
||||||
|
|
||||||
test('should successfully create a new root TeamCollection with valid inputs', async () => {
|
test('should successfully create a new root TeamCollection with valid inputs', async () => {
|
||||||
// isOwnerCheck
|
// isOwnerCheck
|
||||||
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
@@ -496,9 +715,10 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcdefg',
|
'abcdefg',
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight(rootTeamCollection);
|
expect(result).toEqualRight(rootTeamCollectionsCasted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should successfully create a new child TeamCollection with valid inputs', async () => {
|
test('should successfully create a new child TeamCollection with valid inputs', async () => {
|
||||||
@@ -514,9 +734,10 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
childTeamCollection.teamID,
|
childTeamCollection.teamID,
|
||||||
childTeamCollection.title,
|
childTeamCollection.title,
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight(childTeamCollection);
|
expect(result).toEqualRight(childTeamCollectionCasted);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
|
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
|
||||||
@@ -532,11 +753,13 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
childTeamCollection.teamID,
|
childTeamCollection.teamID,
|
||||||
childTeamCollection.title,
|
childTeamCollection.title,
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_added`,
|
`team_coll/${childTeamCollection.teamID}/coll_added`,
|
||||||
childTeamCollection,
|
childTeamCollectionCasted,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -553,11 +776,13 @@ describe('createCollection', () => {
|
|||||||
const result = await teamCollectionService.createCollection(
|
const result = await teamCollectionService.createCollection(
|
||||||
rootTeamCollection.teamID,
|
rootTeamCollection.teamID,
|
||||||
'abcdefg',
|
'abcdefg',
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
|
|
||||||
rootTeamCollection.id,
|
rootTeamCollection.id,
|
||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollection,
|
rootTeamCollectionsCasted,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -587,7 +812,7 @@ describe('renameCollection', () => {
|
|||||||
'NewTitle',
|
'NewTitle',
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...rootTeamCollection,
|
...rootTeamCollectionsCasted,
|
||||||
title: 'NewTitle',
|
title: 'NewTitle',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -625,7 +850,7 @@ describe('renameCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
||||||
{
|
{
|
||||||
...rootTeamCollection,
|
...rootTeamCollectionsCasted,
|
||||||
title: 'NewTitle',
|
title: 'NewTitle',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -832,9 +1057,8 @@ describe('moveCollection', () => {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...childTeamCollection,
|
...childTeamCollectionCasted,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
orderIndex: 2,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -890,9 +1114,8 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...childTeamCollection,
|
...childTeamCollectionCasted,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
orderIndex: 2,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -931,9 +1154,8 @@ describe('moveCollection', () => {
|
|||||||
childTeamCollection_2.id,
|
childTeamCollection_2.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...rootTeamCollection,
|
...rootTeamCollectionsCasted,
|
||||||
parentID: childTeamCollection_2.id,
|
parentID: childTeamCollection_2Casted.id,
|
||||||
orderIndex: 1,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -973,9 +1195,8 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...rootTeamCollection,
|
...rootTeamCollectionsCasted,
|
||||||
parentID: childTeamCollection_2.id,
|
parentID: childTeamCollection_2Casted.id,
|
||||||
orderIndex: 1,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1014,9 +1235,8 @@ describe('moveCollection', () => {
|
|||||||
childTeamCollection_2.id,
|
childTeamCollection_2.id,
|
||||||
);
|
);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
...childTeamCollection,
|
...childTeamCollectionCasted,
|
||||||
parentID: childTeamCollection_2.id,
|
parentID: childTeamCollection_2Casted.id,
|
||||||
orderIndex: 1,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1056,9 +1276,8 @@ describe('moveCollection', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
`team_coll/${childTeamCollection.teamID}/coll_moved`,
|
||||||
{
|
{
|
||||||
...childTeamCollection,
|
...childTeamCollectionCasted,
|
||||||
parentID: childTeamCollection_2.id,
|
parentID: childTeamCollection_2Casted.id,
|
||||||
orderIndex: 1,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1154,7 +1373,7 @@ describe('updateCollectionOrder', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
|
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
|
||||||
{
|
{
|
||||||
collection: rootTeamCollectionList[4],
|
collection: rootTeamCollectionListCasted[4],
|
||||||
nextCollection: null,
|
nextCollection: null,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1235,8 +1454,8 @@ describe('updateCollectionOrder', () => {
|
|||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
|
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
|
||||||
{
|
{
|
||||||
collection: childTeamCollectionList[4],
|
collection: childTeamCollectionListCasted[4],
|
||||||
nextCollection: childTeamCollectionList[2],
|
nextCollection: childTeamCollectionListCasted[2],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1302,7 +1521,7 @@ describe('importCollectionsFromJSON', () => {
|
|||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollection,
|
rootTeamCollectionsCasted,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1421,7 +1640,7 @@ describe('replaceCollectionsWithJSON', () => {
|
|||||||
);
|
);
|
||||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
`team_coll/${rootTeamCollection.teamID}/coll_added`,
|
||||||
rootTeamCollection,
|
rootTeamCollectionsCasted,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1458,4 +1677,64 @@ describe('totalCollectionsInTeam', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateTeamCollection', () => {
|
||||||
|
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
|
||||||
|
const result = await teamCollectionService.updateTeamCollection(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
|
'de',
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
|
||||||
|
const result = await teamCollectionService.updateTeamCollection(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
'{',
|
||||||
|
rootTeamCollection.title,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
|
||||||
|
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
|
||||||
|
|
||||||
|
const result = await teamCollectionService.updateTeamCollection(
|
||||||
|
'invalid_id',
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
|
rootTeamCollection.title,
|
||||||
|
);
|
||||||
|
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should successfully update a collection', async () => {
|
||||||
|
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
|
||||||
|
|
||||||
|
const result = await teamCollectionService.updateTeamCollection(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
JSON.stringify({ foo: 'bar' }),
|
||||||
|
'new_title',
|
||||||
|
);
|
||||||
|
expect(result).toEqualRight({
|
||||||
|
data: JSON.stringify({ foo: 'bar' }),
|
||||||
|
title: 'new_title',
|
||||||
|
...rootTeamCollectionsCasted,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
|
||||||
|
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
|
||||||
|
|
||||||
|
const result = await teamCollectionService.updateTeamCollection(
|
||||||
|
rootTeamCollection.id,
|
||||||
|
JSON.stringify(rootTeamCollection.data),
|
||||||
|
rootTeamCollection.title,
|
||||||
|
);
|
||||||
|
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||||
|
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
|
||||||
|
rootTeamCollectionsCasted,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//ToDo: write test cases for exportCollectionsToJSON
|
//ToDo: write test cases for exportCollectionsToJSON
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
TEAM_COLL_IS_PARENT_COLL,
|
TEAM_COLL_IS_PARENT_COLL,
|
||||||
TEAM_COL_SAME_NEXT_COLL,
|
TEAM_COL_SAME_NEXT_COLL,
|
||||||
TEAM_COL_REORDERING_FAILED,
|
TEAM_COL_REORDERING_FAILED,
|
||||||
|
TEAM_COLL_DATA_INVALID,
|
||||||
} from '../errors';
|
} from '../errors';
|
||||||
import { PubSubService } from '../pubsub/pubsub.service';
|
import { PubSubService } from '../pubsub/pubsub.service';
|
||||||
import { isValidLength } from 'src/utils';
|
import { isValidLength } from 'src/utils';
|
||||||
@@ -69,6 +70,7 @@ export class TeamCollectionService {
|
|||||||
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
|
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
data: folder.data ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +120,7 @@ export class TeamCollectionService {
|
|||||||
name: collection.right.title,
|
name: collection.right.title,
|
||||||
folders: childrenCollectionObjects,
|
folders: childrenCollectionObjects,
|
||||||
requests: requests.map((x) => x.request),
|
requests: requests.map((x) => x.request),
|
||||||
|
data: JSON.stringify(collection.right.data),
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
@@ -198,8 +201,11 @@ export class TeamCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
teamCollections.forEach((x) =>
|
teamCollections.forEach((collection) =>
|
||||||
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
this.pubsub.publish(
|
||||||
|
`team_coll/${destTeamID}/coll_added`,
|
||||||
|
this.cast(collection),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -268,8 +274,11 @@ export class TeamCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
teamCollections.forEach((x) =>
|
teamCollections.forEach((collections) =>
|
||||||
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
|
this.pubsub.publish(
|
||||||
|
`team_coll/${destTeamID}/coll_added`,
|
||||||
|
this.cast(collections),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
@@ -277,11 +286,17 @@ export class TeamCollectionService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Typecast a database TeamCollection to a TeamCollection model
|
* Typecast a database TeamCollection to a TeamCollection model
|
||||||
|
*
|
||||||
* @param teamCollection database TeamCollection
|
* @param teamCollection database TeamCollection
|
||||||
* @returns TeamCollection model
|
* @returns TeamCollection model
|
||||||
*/
|
*/
|
||||||
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
private cast(teamCollection: DBTeamCollection): TeamCollection {
|
||||||
return <TeamCollection>{ ...teamCollection };
|
return <TeamCollection>{
|
||||||
|
id: teamCollection.id,
|
||||||
|
title: teamCollection.title,
|
||||||
|
parentID: teamCollection.parentID,
|
||||||
|
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,7 +339,7 @@ export class TeamCollectionService {
|
|||||||
});
|
});
|
||||||
if (!teamCollection) return null;
|
if (!teamCollection) return null;
|
||||||
|
|
||||||
return teamCollection.parent;
|
return !teamCollection.parent ? null : this.cast(teamCollection.parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,12 +350,12 @@ export class TeamCollectionService {
|
|||||||
* @param take Number of items we want returned
|
* @param take Number of items we want returned
|
||||||
* @returns A list of child collections
|
* @returns A list of child collections
|
||||||
*/
|
*/
|
||||||
getChildrenOfCollection(
|
async getChildrenOfCollection(
|
||||||
collectionID: string,
|
collectionID: string,
|
||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
take: number,
|
take: number,
|
||||||
) {
|
) {
|
||||||
return this.prisma.teamCollection.findMany({
|
const res = await this.prisma.teamCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
parentID: collectionID,
|
parentID: collectionID,
|
||||||
},
|
},
|
||||||
@@ -351,6 +366,12 @@ export class TeamCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const childCollections = res.map((teamCollection) =>
|
||||||
|
this.cast(teamCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return childCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,7 +387,7 @@ export class TeamCollectionService {
|
|||||||
cursor: string | null,
|
cursor: string | null,
|
||||||
take: number,
|
take: number,
|
||||||
) {
|
) {
|
||||||
return this.prisma.teamCollection.findMany({
|
const res = await this.prisma.teamCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
teamID,
|
teamID,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
@@ -378,6 +399,12 @@ export class TeamCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const teamCollections = res.map((teamCollection) =>
|
||||||
|
this.cast(teamCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return teamCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -470,6 +497,7 @@ export class TeamCollectionService {
|
|||||||
async createCollection(
|
async createCollection(
|
||||||
teamID: string,
|
teamID: string,
|
||||||
title: string,
|
title: string,
|
||||||
|
data: string | null = null,
|
||||||
parentTeamCollectionID: string | null,
|
parentTeamCollectionID: string | null,
|
||||||
) {
|
) {
|
||||||
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
||||||
@@ -481,6 +509,13 @@ export class TeamCollectionService {
|
|||||||
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
|
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
|
||||||
|
if (data) {
|
||||||
|
const jsonReq = stringToJson(data);
|
||||||
|
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
|
||||||
|
data = jsonReq.right;
|
||||||
|
}
|
||||||
|
|
||||||
const isParent = parentTeamCollectionID
|
const isParent = parentTeamCollectionID
|
||||||
? {
|
? {
|
||||||
connect: {
|
connect: {
|
||||||
@@ -498,18 +533,23 @@ export class TeamCollectionService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
parent: isParent,
|
parent: isParent,
|
||||||
|
data: data ?? undefined,
|
||||||
orderIndex: !parentTeamCollectionID
|
orderIndex: !parentTeamCollectionID
|
||||||
? (await this.getRootCollectionsCount(teamID)) + 1
|
? (await this.getRootCollectionsCount(teamID)) + 1
|
||||||
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
|
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
|
this.pubsub.publish(
|
||||||
|
`team_coll/${teamID}/coll_added`,
|
||||||
|
this.cast(teamCollection),
|
||||||
|
);
|
||||||
|
|
||||||
return E.right(this.cast(teamCollection));
|
return E.right(this.cast(teamCollection));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated Use updateTeamCollection method instead
|
||||||
* Update the title of a TeamCollection
|
* Update the title of a TeamCollection
|
||||||
*
|
*
|
||||||
* @param collectionID The Collection ID
|
* @param collectionID The Collection ID
|
||||||
@@ -532,10 +572,10 @@ export class TeamCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
||||||
updatedTeamCollection,
|
this.cast(updatedTeamCollection),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(updatedTeamCollection);
|
return E.right(this.cast(updatedTeamCollection));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return E.left(TEAM_COLL_NOT_FOUND);
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -694,8 +734,8 @@ export class TeamCollectionService {
|
|||||||
* @returns An Option of boolean, is parent or not
|
* @returns An Option of boolean, is parent or not
|
||||||
*/
|
*/
|
||||||
private async isParent(
|
private async isParent(
|
||||||
collection: TeamCollection,
|
collection: DBTeamCollection,
|
||||||
destCollection: TeamCollection,
|
destCollection: DBTeamCollection,
|
||||||
): Promise<O.Option<boolean>> {
|
): Promise<O.Option<boolean>> {
|
||||||
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
|
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
|
||||||
//* Valid condition, isParent returns false
|
//* Valid condition, isParent returns false
|
||||||
@@ -971,4 +1011,49 @@ export class TeamCollectionService {
|
|||||||
const teamCollectionsCount = this.prisma.teamCollection.count();
|
const teamCollectionsCount = this.prisma.teamCollection.count();
|
||||||
return teamCollectionsCount;
|
return teamCollectionsCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update Team Collection details
|
||||||
|
*
|
||||||
|
* @param collectionID Collection ID
|
||||||
|
* @param collectionData new header data in a JSONified string form
|
||||||
|
* @param newTitle New title of the collection
|
||||||
|
* @returns Updated TeamCollection
|
||||||
|
*/
|
||||||
|
async updateTeamCollection(
|
||||||
|
collectionID: string,
|
||||||
|
collectionData: string = null,
|
||||||
|
newTitle: string = null,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (newTitle != null) {
|
||||||
|
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
|
||||||
|
if (collectionData) {
|
||||||
|
const jsonReq = stringToJson(collectionData);
|
||||||
|
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
|
||||||
|
collectionData = jsonReq.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedTeamCollection = await this.prisma.teamCollection.update({
|
||||||
|
where: { id: collectionID },
|
||||||
|
data: {
|
||||||
|
data: collectionData ?? undefined,
|
||||||
|
title: newTitle ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
|
||||||
|
this.cast(updatedTeamCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(this.cast(updatedTeamCollection));
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(TEAM_COLL_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const teamCollection: DbTeamCollection = {
|
|||||||
id: 'team-coll-1',
|
id: 'team-coll-1',
|
||||||
parentID: null,
|
parentID: null,
|
||||||
teamID: team.id,
|
teamID: team.id,
|
||||||
|
data: {},
|
||||||
title: 'Team Collection 1',
|
title: 'Team Collection 1',
|
||||||
orderIndex: 1,
|
orderIndex: 1,
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
|
||||||
export interface CollectionFolder {
|
export interface CollectionFolder {
|
||||||
id?: string;
|
id?: string;
|
||||||
folders: CollectionFolder[];
|
folders: CollectionFolder[];
|
||||||
requests: any[];
|
requests: any[];
|
||||||
name: string;
|
name: string;
|
||||||
|
data?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ import { PaginationArgs } from 'src/types/input-types.args';
|
|||||||
export class CreateRootUserCollectionArgs {
|
export class CreateRootUserCollectionArgs {
|
||||||
@Field({ name: 'title', description: 'Title of the new user collection' })
|
@Field({ name: 'title', description: 'Title of the new user collection' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
}
|
}
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class CreateChildUserCollectionArgs {
|
export class CreateChildUserCollectionArgs {
|
||||||
@@ -17,6 +24,13 @@ export class CreateChildUserCollectionArgs {
|
|||||||
description: 'ID of the parent to the new user collection',
|
description: 'ID of the parent to the new user collection',
|
||||||
})
|
})
|
||||||
parentUserCollectionID: string;
|
parentUserCollectionID: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
@@ -95,3 +109,26 @@ export class ImportUserCollectionsFromJSONArgs {
|
|||||||
})
|
})
|
||||||
parentCollectionID?: string;
|
parentCollectionID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class UpdateUserCollectionsArgs {
|
||||||
|
@Field(() => ID, {
|
||||||
|
name: 'userCollectionID',
|
||||||
|
description: 'ID of the user collection',
|
||||||
|
})
|
||||||
|
userCollectionID: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'newTitle',
|
||||||
|
description: 'The updated title of the user collection',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
newTitle: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
name: 'data',
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
MoveUserCollectionArgs,
|
MoveUserCollectionArgs,
|
||||||
RenameUserCollectionsArgs,
|
RenameUserCollectionsArgs,
|
||||||
UpdateUserCollectionArgs,
|
UpdateUserCollectionArgs,
|
||||||
|
UpdateUserCollectionsArgs,
|
||||||
} from './input-type.args';
|
} from './input-type.args';
|
||||||
import { ReqType } from 'src/types/RequestTypes';
|
import { ReqType } from 'src/types/RequestTypes';
|
||||||
import * as E from 'fp-ts/Either';
|
import * as E from 'fp-ts/Either';
|
||||||
@@ -142,7 +143,13 @@ export class UserCollectionResolver {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
if (E.isLeft(userCollection)) throwErr(userCollection.left);
|
||||||
return userCollection.right;
|
return <UserCollection>{
|
||||||
|
...userCollection.right,
|
||||||
|
userID: userCollection.right.userUid,
|
||||||
|
data: !userCollection.right.data
|
||||||
|
? null
|
||||||
|
: JSON.stringify(userCollection.right.data),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Query(() => UserCollectionExportJSONData, {
|
@Query(() => UserCollectionExportJSONData, {
|
||||||
@@ -191,6 +198,7 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
|
args.data,
|
||||||
null,
|
null,
|
||||||
ReqType.REST,
|
ReqType.REST,
|
||||||
);
|
);
|
||||||
@@ -212,6 +220,7 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
|
args.data,
|
||||||
null,
|
null,
|
||||||
ReqType.GQL,
|
ReqType.GQL,
|
||||||
);
|
);
|
||||||
@@ -232,6 +241,7 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
|
args.data,
|
||||||
args.parentUserCollectionID,
|
args.parentUserCollectionID,
|
||||||
ReqType.GQL,
|
ReqType.GQL,
|
||||||
);
|
);
|
||||||
@@ -252,6 +262,7 @@ export class UserCollectionResolver {
|
|||||||
await this.userCollectionService.createUserCollection(
|
await this.userCollectionService.createUserCollection(
|
||||||
user,
|
user,
|
||||||
args.title,
|
args.title,
|
||||||
|
args.data,
|
||||||
args.parentUserCollectionID,
|
args.parentUserCollectionID,
|
||||||
ReqType.REST,
|
ReqType.REST,
|
||||||
);
|
);
|
||||||
@@ -359,6 +370,26 @@ export class UserCollectionResolver {
|
|||||||
return importedCollection.right;
|
return importedCollection.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mutation(() => UserCollection, {
|
||||||
|
description: 'Update a UserCollection',
|
||||||
|
})
|
||||||
|
@UseGuards(GqlAuthGuard)
|
||||||
|
async updateUserCollection(
|
||||||
|
@GqlUser() user: AuthUser,
|
||||||
|
@Args() args: UpdateUserCollectionsArgs,
|
||||||
|
) {
|
||||||
|
const updatedUserCollection =
|
||||||
|
await this.userCollectionService.updateUserCollection(
|
||||||
|
args.newTitle,
|
||||||
|
args.data,
|
||||||
|
args.userCollectionID,
|
||||||
|
user.uid,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
|
||||||
|
return updatedUserCollection.right;
|
||||||
|
}
|
||||||
|
|
||||||
// Subscriptions
|
// Subscriptions
|
||||||
@Subscription(() => UserCollection, {
|
@Subscription(() => UserCollection, {
|
||||||
description: 'Listen for User Collection Creation',
|
description: 'Listen for User Collection Creation',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import {
|
|||||||
USER_NOT_FOUND,
|
USER_NOT_FOUND,
|
||||||
USER_NOT_OWNER,
|
USER_NOT_OWNER,
|
||||||
USER_COLL_INVALID_JSON,
|
USER_COLL_INVALID_JSON,
|
||||||
|
USER_COLL_DATA_INVALID,
|
||||||
} from 'src/errors';
|
} from 'src/errors';
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from 'src/prisma/prisma.service';
|
||||||
import { AuthUser } from 'src/types/AuthUser';
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
@@ -43,8 +44,12 @@ export class UserCollectionService {
|
|||||||
*/
|
*/
|
||||||
private cast(collection: UserCollection) {
|
private cast(collection: UserCollection) {
|
||||||
return <UserCollectionModel>{
|
return <UserCollectionModel>{
|
||||||
...collection,
|
id: collection.id,
|
||||||
|
title: collection.title,
|
||||||
|
type: collection.type,
|
||||||
|
parentID: collection.parentID,
|
||||||
userID: collection.userUid,
|
userID: collection.userUid,
|
||||||
|
data: !collection.data ? null : JSON.stringify(collection.data),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +151,7 @@ export class UserCollectionService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return parent;
|
return !parent ? null : this.cast(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,7 +169,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
return this.prisma.userCollection.findMany({
|
const res = await this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
parentID: collectionID,
|
parentID: collectionID,
|
||||||
type: type,
|
type: type,
|
||||||
@@ -176,6 +181,12 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const childCollections = res.map((childCollection) =>
|
||||||
|
this.cast(childCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return childCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,12 +222,20 @@ export class UserCollectionService {
|
|||||||
async createUserCollection(
|
async createUserCollection(
|
||||||
user: AuthUser,
|
user: AuthUser,
|
||||||
title: string,
|
title: string,
|
||||||
|
data: string | null = null,
|
||||||
parentUserCollectionID: string | null,
|
parentUserCollectionID: string | null,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
|
||||||
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
||||||
|
|
||||||
|
if (data === '') return E.left(USER_COLL_DATA_INVALID);
|
||||||
|
if (data) {
|
||||||
|
const jsonReq = stringToJson(data);
|
||||||
|
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
|
||||||
|
data = jsonReq.right;
|
||||||
|
}
|
||||||
|
|
||||||
// If creating a child collection
|
// If creating a child collection
|
||||||
if (parentUserCollectionID !== null) {
|
if (parentUserCollectionID !== null) {
|
||||||
const parentCollection = await this.getUserCollection(
|
const parentCollection = await this.getUserCollection(
|
||||||
@@ -251,15 +270,19 @@ export class UserCollectionService {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
parent: isParent,
|
parent: isParent,
|
||||||
|
data: data ?? undefined,
|
||||||
orderIndex: !parentUserCollectionID
|
orderIndex: !parentUserCollectionID
|
||||||
? (await this.getRootCollectionsCount(user.uid)) + 1
|
? (await this.getRootCollectionsCount(user.uid)) + 1
|
||||||
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
|
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
|
await this.pubsub.publish(
|
||||||
|
`user_coll/${user.uid}/created`,
|
||||||
|
this.cast(userCollection),
|
||||||
|
);
|
||||||
|
|
||||||
return E.right(userCollection);
|
return E.right(this.cast(userCollection));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -276,7 +299,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
return this.prisma.userCollection.findMany({
|
const res = await this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
userUid: user.uid,
|
userUid: user.uid,
|
||||||
parentID: null,
|
parentID: null,
|
||||||
@@ -289,6 +312,12 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userCollections = res.map((childCollection) =>
|
||||||
|
this.cast(childCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return userCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,7 +336,7 @@ export class UserCollectionService {
|
|||||||
take: number,
|
take: number,
|
||||||
type: ReqType,
|
type: ReqType,
|
||||||
) {
|
) {
|
||||||
return this.prisma.userCollection.findMany({
|
const res = await this.prisma.userCollection.findMany({
|
||||||
where: {
|
where: {
|
||||||
userUid: user.uid,
|
userUid: user.uid,
|
||||||
parentID: userCollectionID,
|
parentID: userCollectionID,
|
||||||
@@ -317,9 +346,16 @@ export class UserCollectionService {
|
|||||||
skip: cursor ? 1 : 0,
|
skip: cursor ? 1 : 0,
|
||||||
cursor: cursor ? { id: cursor } : undefined,
|
cursor: cursor ? { id: cursor } : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const childCollections = res.map((childCollection) =>
|
||||||
|
this.cast(childCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return childCollections;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @deprecated Use updateUserCollection method instead
|
||||||
* Update the title of a UserCollection
|
* Update the title of a UserCollection
|
||||||
*
|
*
|
||||||
* @param newTitle The new title of collection
|
* @param newTitle The new title of collection
|
||||||
@@ -351,10 +387,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${updatedUserCollection.userUid}/updated`,
|
`user_coll/${updatedUserCollection.userUid}/updated`,
|
||||||
updatedUserCollection,
|
this.cast(updatedUserCollection),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(updatedUserCollection);
|
return E.right(this.cast(updatedUserCollection));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return E.left(USER_COLL_NOT_FOUND);
|
return E.left(USER_COLL_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -591,10 +627,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${collection.right.userUid}/moved`,
|
`user_coll/${collection.right.userUid}/moved`,
|
||||||
updatedCollection.right,
|
this.cast(updatedCollection.right),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(updatedCollection.right);
|
return E.right(this.cast(updatedCollection.right));
|
||||||
}
|
}
|
||||||
|
|
||||||
// destCollectionID != null i.e move into another collection
|
// destCollectionID != null i.e move into another collection
|
||||||
@@ -642,10 +678,10 @@ export class UserCollectionService {
|
|||||||
|
|
||||||
this.pubsub.publish(
|
this.pubsub.publish(
|
||||||
`user_coll/${collection.right.userUid}/moved`,
|
`user_coll/${collection.right.userUid}/moved`,
|
||||||
updatedCollection.right,
|
this.cast(updatedCollection.right),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(updatedCollection.right);
|
return E.right(this.cast(updatedCollection.right));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -846,6 +882,7 @@ export class UserCollectionService {
|
|||||||
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
data: JSON.stringify(collection.right.data),
|
||||||
};
|
};
|
||||||
|
|
||||||
return E.right(result);
|
return E.right(result);
|
||||||
@@ -918,6 +955,7 @@ export class UserCollectionService {
|
|||||||
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
data: JSON.stringify(parentCollection.right.data),
|
||||||
}),
|
}),
|
||||||
collectionType: parentCollection.right.type,
|
collectionType: parentCollection.right.type,
|
||||||
});
|
});
|
||||||
@@ -971,6 +1009,7 @@ export class UserCollectionService {
|
|||||||
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
|
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
data: folder.data ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,10 +1079,63 @@ export class UserCollectionService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
userCollections.forEach((x) =>
|
userCollections.forEach((collection) =>
|
||||||
this.pubsub.publish(`user_coll/${userID}/created`, x),
|
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
|
||||||
);
|
);
|
||||||
|
|
||||||
return E.right(true);
|
return E.right(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a UserCollection
|
||||||
|
*
|
||||||
|
* @param newTitle The new title of collection
|
||||||
|
* @param userCollectionID The Collection Id
|
||||||
|
* @param userID The User UID
|
||||||
|
* @returns An Either of the updated UserCollection
|
||||||
|
*/
|
||||||
|
async updateUserCollection(
|
||||||
|
newTitle: string = null,
|
||||||
|
collectionData: string | null = null,
|
||||||
|
userCollectionID: string,
|
||||||
|
userID: string,
|
||||||
|
) {
|
||||||
|
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
|
||||||
|
|
||||||
|
if (collectionData) {
|
||||||
|
const jsonReq = stringToJson(collectionData);
|
||||||
|
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
|
||||||
|
collectionData = jsonReq.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTitle != null) {
|
||||||
|
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
|
||||||
|
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see is the collection belongs to the user
|
||||||
|
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
|
||||||
|
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedUserCollection = await this.prisma.userCollection.update({
|
||||||
|
where: {
|
||||||
|
id: userCollectionID,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
data: collectionData ?? undefined,
|
||||||
|
title: newTitle ?? undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.pubsub.publish(
|
||||||
|
`user_coll/${updatedUserCollection.userUid}/updated`,
|
||||||
|
this.cast(updatedUserCollection),
|
||||||
|
);
|
||||||
|
|
||||||
|
return E.right(this.cast(updatedUserCollection));
|
||||||
|
} catch (error) {
|
||||||
|
return E.left(USER_COLL_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ export class UserCollection {
|
|||||||
})
|
})
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
description: 'JSON string representing the collection data',
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
data: string;
|
||||||
|
|
||||||
@Field(() => ReqType, {
|
@Field(() => ReqType, {
|
||||||
description: 'Type of the user collection',
|
description: 'Type of the user collection',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.3.3",
|
"version": "0.4.0",
|
||||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||||
"homepage": "https://hoppscotch.io",
|
"homepage": "https://hoppscotch.io",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
@@ -10,6 +10,9 @@
|
|||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "pnpm exec tsup",
|
"build": "pnpm exec tsup",
|
||||||
"dev": "pnpm exec tsup --watch",
|
"dev": "pnpm exec tsup --watch",
|
||||||
@@ -38,24 +41,24 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@relmify/jest-fp-ts": "^2.0.2",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@swc/core": "^1.2.181",
|
"@swc/core": "^1.3.92",
|
||||||
"@types/jest": "^27.4.1",
|
"@types/jest": "^29.5.5",
|
||||||
"@types/lodash": "^4.14.181",
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/qs": "^6.9.7",
|
"@types/qs": "^6.9.8",
|
||||||
"axios": "^0.21.4",
|
"axios": "^0.21.4",
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.2",
|
||||||
"commander": "^8.0.0",
|
"commander": "^11.0.0",
|
||||||
"esm": "^3.2.25",
|
"esm": "^3.2.25",
|
||||||
"fp-ts": "^2.12.1",
|
"fp-ts": "^2.16.1",
|
||||||
"io-ts": "^2.2.16",
|
"io-ts": "^2.2.20",
|
||||||
"jest": "^27.5.1",
|
"jest": "^29.7.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^3.0.3",
|
||||||
"qs": "^6.10.3",
|
"qs": "^6.11.2",
|
||||||
"ts-jest": "^27.1.4",
|
"ts-jest": "^29.1.1",
|
||||||
"tsup": "^5.12.7",
|
"tsup": "^7.2.0",
|
||||||
"typescript": "^4.6.4",
|
"typescript": "^5.2.2",
|
||||||
"zod": "^3.22.2"
|
"zod": "^3.22.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
name: "localStorage",
|
name: "localStorage",
|
||||||
message:
|
message:
|
||||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
// window.localStorage block
|
// window.localStorage block
|
||||||
@@ -66,8 +66,10 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
selector: "CallExpression[callee.object.property.name='localStorage']",
|
selector: "CallExpression[callee.object.property.name='localStorage']",
|
||||||
message:
|
message:
|
||||||
"Do not use 'localStorage' directly. Please use localpersistence.ts functions or stores",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eqeqeq: 1,
|
||||||
|
"no-else-return": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ module.exports = {
|
|||||||
singleQuote: false,
|
singleQuote: false,
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 2
|
tabWidth: 2,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* Write hoppscotch-common related custom styles in this file.
|
||||||
|
* If styles are sharable across all package then write into hoppscotch-ui/assets/scss/styles.scss file.
|
||||||
|
*/
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@apply backface-hidden;
|
backface-visibility: hidden;
|
||||||
@apply before:backface-hidden;
|
-moz-backface-visibility: hidden;
|
||||||
@apply after:backface-hidden;
|
-webkit-backface-visibility: hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-moz-backface-visibility: hidden;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
backface-visibility: hidden;
|
||||||
|
-moz-backface-visibility: hidden;
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
@apply selection:bg-accentDark;
|
@apply selection:bg-accentDark;
|
||||||
@apply selection:text-accentContrast;
|
@apply selection:text-accentContrast;
|
||||||
@apply overscroll-none;
|
@apply overscroll-none;
|
||||||
@@ -15,13 +33,13 @@
|
|||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply border-solid border-l border-dividerLight border-t-0 border-b-0 border-r-0;
|
@apply border-b-0 border-l border-r-0 border-t-0 border-solid border-dividerLight;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-divider bg-clip-content;
|
@apply bg-divider bg-clip-content;
|
||||||
@apply rounded-full;
|
@apply rounded-full;
|
||||||
@apply border-solid border-transparent border-4;
|
@apply border-4 border-solid border-transparent;
|
||||||
@apply hover:bg-dividerDark;
|
@apply hover:bg-dividerDark;
|
||||||
@apply hover:bg-clip-content;
|
@apply hover:bg-clip-content;
|
||||||
}
|
}
|
||||||
@@ -39,7 +57,7 @@ input::placeholder,
|
|||||||
textarea::placeholder,
|
textarea::placeholder,
|
||||||
.cm-placeholder {
|
.cm-placeholder {
|
||||||
@apply text-secondary;
|
@apply text-secondary;
|
||||||
@apply opacity-50;
|
@apply opacity-50 #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
@@ -54,11 +72,11 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-primary;
|
@apply bg-primary;
|
||||||
@apply text-secondary text-body;
|
@apply text-body text-secondary;
|
||||||
@apply font-medium;
|
@apply font-medium;
|
||||||
@apply select-none;
|
@apply select-none;
|
||||||
@apply overflow-x-hidden;
|
@apply overflow-x-hidden;
|
||||||
@apply leading-body;
|
@apply leading-body #{!important};
|
||||||
animation: fade 300ms forwards;
|
animation: fade 300ms forwards;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
@@ -124,8 +142,8 @@ a {
|
|||||||
|
|
||||||
&.link {
|
&.link {
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply py-0.5 px-1;
|
@apply px-1 py-0.5;
|
||||||
@apply -my-0.5 -mx-1;
|
@apply -mx-1 -my-0.5;
|
||||||
@apply text-accent;
|
@apply text-accent;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply hover:text-accentDark;
|
@apply hover:text-accentDark;
|
||||||
@@ -140,7 +158,7 @@ a {
|
|||||||
@apply shadow-none #{!important};
|
@apply shadow-none #{!important};
|
||||||
@apply fixed;
|
@apply fixed;
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply -mt-7.5;
|
@apply -mt-8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,15 +172,15 @@ a {
|
|||||||
@apply flex;
|
@apply flex;
|
||||||
@apply text-tiny text-primary;
|
@apply text-tiny text-primary;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply py-1 px-2;
|
@apply px-2 py-1;
|
||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply leading-normal;
|
@apply leading-body;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
@apply hidden;
|
@apply hidden;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
@apply bg-gray-500/45;
|
background-color: rgba(107, 114, 128, 0.45);
|
||||||
@apply text-primaryLight;
|
@apply text-primaryLight;
|
||||||
@apply rounded-sm;
|
@apply rounded-sm;
|
||||||
@apply px-1;
|
@apply px-1;
|
||||||
@@ -170,6 +188,12 @@ a {
|
|||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply sm:inline-flex;
|
@apply sm:inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.env-icon {
|
||||||
|
@apply transition;
|
||||||
|
@apply inline-flex;
|
||||||
|
@apply items-center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tippy-svg-arrow {
|
.tippy-svg-arrow {
|
||||||
@@ -195,9 +219,9 @@ a {
|
|||||||
@apply max-h-[45vh];
|
@apply max-h-[45vh];
|
||||||
@apply items-stretch;
|
@apply items-stretch;
|
||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-secondary text-body;
|
@apply text-body text-secondary;
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
@apply leading-normal;
|
@apply leading-body;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
@@ -229,12 +253,12 @@ a {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-b border-dividerLight;
|
@apply border-b border-dividerLight;
|
||||||
@apply my-2;
|
@apply my-2 #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
@apply font-bold;
|
@apply font-bold;
|
||||||
@apply text-secondaryDark text-lg;
|
@apply text-lg text-secondaryDark;
|
||||||
@apply tracking-tight;
|
@apply tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +267,7 @@ hr {
|
|||||||
.textarea {
|
.textarea {
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply w-full;
|
@apply w-full;
|
||||||
@apply py-2 px-4;
|
@apply px-4 py-2;
|
||||||
@apply bg-transparent;
|
@apply bg-transparent;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@@ -284,7 +308,7 @@ button {
|
|||||||
@apply transform;
|
@apply transform;
|
||||||
@apply origin-top-left;
|
@apply origin-top-left;
|
||||||
@apply scale-75;
|
@apply scale-75;
|
||||||
@apply translate-x-1 -translate-y-4;
|
@apply -translate-y-4 translate-x-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-input:focus-within ~ label {
|
.floating-input:focus-within ~ label {
|
||||||
@@ -293,7 +317,7 @@ button {
|
|||||||
|
|
||||||
.floating-input ~ .end-actions {
|
.floating-input ~ .end-actions {
|
||||||
@apply absolute;
|
@apply absolute;
|
||||||
@apply right-0.2;
|
@apply right-[.05rem];
|
||||||
@apply inset-y-0;
|
@apply inset-y-0;
|
||||||
@apply flex;
|
@apply flex;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@@ -318,44 +342,28 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
@apply flex flex-1;
|
|
||||||
@apply relative;
|
|
||||||
@apply after:absolute;
|
|
||||||
@apply after:flex;
|
|
||||||
@apply after:inset-y-0;
|
|
||||||
@apply after:items-center;
|
|
||||||
@apply after:justify-center;
|
|
||||||
@apply after:pointer-events-none;
|
|
||||||
@apply after:font-icon;
|
|
||||||
@apply after:text-current;
|
|
||||||
@apply after:right-3;
|
|
||||||
@apply after:content-["\e5cf"];
|
|
||||||
@apply after:text-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-response {
|
.info-response {
|
||||||
@apply text-pink-500;
|
color: var(--status-info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-response {
|
.success-response {
|
||||||
@apply text-green-500;
|
color: var(--status-success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redir-response {
|
.redirect-response {
|
||||||
@apply text-yellow-500;
|
color: var(--status-redirect-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-error-response {
|
.critical-error-response {
|
||||||
@apply text-red-500;
|
color: var(--status-critical-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sv-error-response {
|
.server-error-response {
|
||||||
@apply text-red-600;
|
color: var(--status-server-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-data-response {
|
.missing-data-response {
|
||||||
@apply text-secondaryLight;
|
color: var(--status-missing-data-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toasted-container {
|
.toasted-container {
|
||||||
@@ -366,7 +374,7 @@ pre.ace_editor {
|
|||||||
@apply px-4 py-2;
|
@apply px-4 py-2;
|
||||||
@apply bg-tooltip;
|
@apply bg-tooltip;
|
||||||
@apply border-secondaryDark;
|
@apply border-secondaryDark;
|
||||||
@apply text-primary text-body;
|
@apply text-body text-primary;
|
||||||
@apply justify-between;
|
@apply justify-between;
|
||||||
@apply shadow-lg;
|
@apply shadow-lg;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@@ -394,7 +402,7 @@ pre.ace_editor {
|
|||||||
@apply before:opacity-10;
|
@apply before:opacity-10;
|
||||||
@apply before:inset-0;
|
@apply before:inset-0;
|
||||||
@apply before:transition;
|
@apply before:transition;
|
||||||
@apply before:content-DEFAULT;
|
@apply before:content-[''];
|
||||||
@apply hover:no-underline;
|
@apply hover:no-underline;
|
||||||
@apply hover:before:opacity-20;
|
@apply hover:before:opacity-20;
|
||||||
}
|
}
|
||||||
@@ -428,7 +436,7 @@ pre.ace_editor {
|
|||||||
@apply before:opacity-0;
|
@apply before:opacity-0;
|
||||||
@apply before:z-20;
|
@apply before:z-20;
|
||||||
@apply before:transition;
|
@apply before:transition;
|
||||||
@apply before:content-DEFAULT;
|
@apply before:content-[''];
|
||||||
@apply hover:before:opacity-100;
|
@apply hover:before:opacity-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,32 +509,16 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-panel.cm-search [name="close"] {
|
|
||||||
@apply flex;
|
|
||||||
@apply items-center;
|
|
||||||
@apply justify-center;
|
|
||||||
@apply min-h-5;
|
|
||||||
@apply min-w-5;
|
|
||||||
@apply bg-primaryDark #{!important};
|
|
||||||
@apply sticky #{!important};
|
|
||||||
@apply right-0 #{!important};
|
|
||||||
@apply ml-auto #{!important};
|
|
||||||
@apply my-auto #{!important};
|
|
||||||
@apply rounded #{!important};
|
|
||||||
@apply outline #{!important};
|
|
||||||
@apply outline-divider #{!important};
|
|
||||||
}
|
|
||||||
|
|
||||||
.shortcut-key {
|
.shortcut-key {
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
@apply text-tiny;
|
@apply text-tiny;
|
||||||
@apply bg-divider;
|
@apply bg-dividerLight;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply ml-2;
|
@apply ml-2;
|
||||||
@apply px-1;
|
@apply px-1;
|
||||||
@apply min-w-5;
|
@apply min-w-[1.25rem];
|
||||||
@apply min-h-5;
|
@apply min-h-[1.25rem];
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply justify-center;
|
@apply justify-center;
|
||||||
@apply border border-dividerDark;
|
@apply border border-dividerDark;
|
||||||
|
|||||||
3
packages/hoppscotch-common/assets/scss/tailwind.scss
Normal file
3
packages/hoppscotch-common/assets/scss/tailwind.scss
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
@mixin base-theme {
|
|
||||||
--font-sans: "Inter Variable", sans-serif;
|
|
||||||
--font-icon: "Material Symbols Rounded Variable";
|
|
||||||
--font-mono: "Roboto Mono Variable", monospace;
|
|
||||||
--font-size-body: 0.75rem;
|
|
||||||
--font-size-tiny: 0.688rem;
|
|
||||||
--line-height-body: 1rem;
|
|
||||||
--upper-primary-sticky-fold: 4.125rem;
|
|
||||||
--upper-secondary-sticky-fold: 6.188rem;
|
|
||||||
--upper-tertiary-sticky-fold: 8.25rem;
|
|
||||||
--upper-fourth-sticky-fold: 10.2rem;
|
|
||||||
--upper-mobile-primary-sticky-fold: 6.625rem;
|
|
||||||
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
|
||||||
--upper-mobile-sticky-fold: 10.75rem;
|
|
||||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
|
||||||
--lower-primary-sticky-fold: 3rem;
|
|
||||||
--lower-secondary-sticky-fold: 5.063rem;
|
|
||||||
--lower-tertiary-sticky-fold: 7.125rem;
|
|
||||||
--lower-fourth-sticky-fold: 9.188rem;
|
|
||||||
--sidebar-primary-sticky-fold: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin dark-theme {
|
|
||||||
--primary-color: theme("colors.dark.800");
|
|
||||||
--primary-light-color: theme("colors.dark.600");
|
|
||||||
--primary-dark-color: theme("colors.neutral.800");
|
|
||||||
--primary-contrast-color: theme("colors.neutral.900");
|
|
||||||
|
|
||||||
--secondary-color: theme("colors.neutral.400");
|
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
|
||||||
--secondary-dark-color: theme("colors.neutral.50");
|
|
||||||
|
|
||||||
--divider-color: theme("colors.neutral.800");
|
|
||||||
--divider-light-color: theme("colors.dark.500");
|
|
||||||
--divider-dark-color: theme("colors.dark.300");
|
|
||||||
|
|
||||||
--error-color: theme("colors.stone.800");
|
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
|
||||||
--popover-color: theme("colors.dark.700");
|
|
||||||
--editor-theme: "merbivore_soft";
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin light-theme {
|
|
||||||
--primary-color: theme("colors.white");
|
|
||||||
--primary-light-color: theme("colors.gray.50");
|
|
||||||
--primary-dark-color: theme("colors.gray.100");
|
|
||||||
--primary-contrast-color: theme("colors.light.50");
|
|
||||||
|
|
||||||
--secondary-color: theme("colors.gray.500");
|
|
||||||
--secondary-light-color: theme("colors.gray.400");
|
|
||||||
--secondary-dark-color: theme("colors.gray.900");
|
|
||||||
|
|
||||||
--divider-color: theme("colors.gray.100");
|
|
||||||
--divider-light-color: theme("colors.gray.100");
|
|
||||||
--divider-dark-color: theme("colors.gray.300");
|
|
||||||
|
|
||||||
--error-color: theme("colors.yellow.100");
|
|
||||||
--tooltip-color: theme("colors.neutral.800");
|
|
||||||
--popover-color: theme("colors.white");
|
|
||||||
--editor-theme: "textmate";
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin black-theme {
|
|
||||||
--primary-color: theme("colors.dark.900");
|
|
||||||
--primary-light-color: theme("colors.neutral.900");
|
|
||||||
--primary-dark-color: theme("colors.dark.800");
|
|
||||||
--primary-contrast-color: theme("colors.dark.900");
|
|
||||||
|
|
||||||
--secondary-color: theme("colors.neutral.400");
|
|
||||||
--secondary-light-color: theme("colors.neutral.500");
|
|
||||||
--secondary-dark-color: theme("colors.neutral.100");
|
|
||||||
|
|
||||||
--divider-color: theme("colors.dark.600");
|
|
||||||
--divider-light-color: theme("colors.dark.800");
|
|
||||||
--divider-dark-color: theme("colors.dark.200");
|
|
||||||
|
|
||||||
--error-color: theme("colors.stone.900");
|
|
||||||
--tooltip-color: theme("colors.neutral.100");
|
|
||||||
--popover-color: theme("colors.dark.900");
|
|
||||||
--editor-theme: "twilight";
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin dark-editor-theme {
|
|
||||||
--editor-type-color: theme("colors.purple.400");
|
|
||||||
--editor-name-color: theme("colors.blue.400");
|
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
|
||||||
--editor-variable-color: theme("colors.green.400");
|
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
|
||||||
--editor-process-color: theme("colors.fuchsia.400");
|
|
||||||
--editor-constant-color: theme("colors.violet.400");
|
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin light-editor-theme {
|
|
||||||
--editor-type-color: theme("colors.purple.600");
|
|
||||||
--editor-name-color: theme("colors.red.600");
|
|
||||||
--editor-operator-color: theme("colors.indigo.600");
|
|
||||||
--editor-invalid-color: theme("colors.red.600");
|
|
||||||
--editor-separator-color: theme("colors.gray.600");
|
|
||||||
--editor-meta-color: theme("colors.gray.600");
|
|
||||||
--editor-variable-color: theme("colors.green.600");
|
|
||||||
--editor-link-color: theme("colors.cyan.600");
|
|
||||||
--editor-process-color: theme("colors.blue.600");
|
|
||||||
--editor-constant-color: theme("colors.fuchsia.600");
|
|
||||||
--editor-keyword-color: theme("colors.pink.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin black-editor-theme {
|
|
||||||
--editor-type-color: theme("colors.purple.400");
|
|
||||||
--editor-name-color: theme("colors.fuchsia.400");
|
|
||||||
--editor-operator-color: theme("colors.indigo.400");
|
|
||||||
--editor-invalid-color: theme("colors.red.400");
|
|
||||||
--editor-separator-color: theme("colors.gray.400");
|
|
||||||
--editor-meta-color: theme("colors.gray.400");
|
|
||||||
--editor-variable-color: theme("colors.green.400");
|
|
||||||
--editor-link-color: theme("colors.cyan.400");
|
|
||||||
--editor-process-color: theme("colors.violet.400");
|
|
||||||
--editor-constant-color: theme("colors.blue.400");
|
|
||||||
--editor-keyword-color: theme("colors.pink.400");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin green-theme {
|
|
||||||
--accent-color: theme("colors.green.500");
|
|
||||||
--accent-light-color: theme("colors.green.400");
|
|
||||||
--accent-dark-color: theme("colors.green.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.green.200");
|
|
||||||
--gradient-via-color: theme("colors.green.400");
|
|
||||||
--gradient-to-color: theme("colors.green.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin teal-theme {
|
|
||||||
--accent-color: theme("colors.teal.500");
|
|
||||||
--accent-light-color: theme("colors.teal.400");
|
|
||||||
--accent-dark-color: theme("colors.teal.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.teal.200");
|
|
||||||
--gradient-via-color: theme("colors.teal.400");
|
|
||||||
--gradient-to-color: theme("colors.teal.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin blue-theme {
|
|
||||||
--accent-color: theme("colors.blue.500");
|
|
||||||
--accent-light-color: theme("colors.blue.400");
|
|
||||||
--accent-dark-color: theme("colors.blue.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.blue.200");
|
|
||||||
--gradient-via-color: theme("colors.blue.400");
|
|
||||||
--gradient-to-color: theme("colors.blue.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin indigo-theme {
|
|
||||||
--accent-color: theme("colors.indigo.500");
|
|
||||||
--accent-light-color: theme("colors.indigo.400");
|
|
||||||
--accent-dark-color: theme("colors.indigo.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.indigo.200");
|
|
||||||
--gradient-via-color: theme("colors.indigo.400");
|
|
||||||
--gradient-to-color: theme("colors.indigo.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin purple-theme {
|
|
||||||
--accent-color: theme("colors.purple.500");
|
|
||||||
--accent-light-color: theme("colors.purple.400");
|
|
||||||
--accent-dark-color: theme("colors.purple.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.purple.200");
|
|
||||||
--gradient-via-color: theme("colors.purple.400");
|
|
||||||
--gradient-to-color: theme("colors.purple.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin yellow-theme {
|
|
||||||
--accent-color: theme("colors.yellow.500");
|
|
||||||
--accent-light-color: theme("colors.yellow.400");
|
|
||||||
--accent-dark-color: theme("colors.yellow.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.yellow.200");
|
|
||||||
--gradient-via-color: theme("colors.yellow.400");
|
|
||||||
--gradient-to-color: theme("colors.yellow.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin orange-theme {
|
|
||||||
--accent-color: theme("colors.orange.500");
|
|
||||||
--accent-light-color: theme("colors.orange.400");
|
|
||||||
--accent-dark-color: theme("colors.orange.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.orange.200");
|
|
||||||
--gradient-via-color: theme("colors.orange.400");
|
|
||||||
--gradient-to-color: theme("colors.orange.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin red-theme {
|
|
||||||
--accent-color: theme("colors.red.500");
|
|
||||||
--accent-light-color: theme("colors.red.400");
|
|
||||||
--accent-dark-color: theme("colors.red.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.red.200");
|
|
||||||
--gradient-via-color: theme("colors.red.400");
|
|
||||||
--gradient-to-color: theme("colors.red.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin pink-theme {
|
|
||||||
--accent-color: theme("colors.pink.500");
|
|
||||||
--accent-light-color: theme("colors.pink.400");
|
|
||||||
--accent-dark-color: theme("colors.pink.600");
|
|
||||||
--accent-contrast-color: theme("colors.white");
|
|
||||||
--gradient-from-color: theme("colors.pink.200");
|
|
||||||
--gradient-via-color: theme("colors.pink.400");
|
|
||||||
--gradient-to-color: theme("colors.pink.600");
|
|
||||||
}
|
|
||||||
|
|
||||||
:root {
|
|
||||||
@include base-theme;
|
|
||||||
@include dark-theme;
|
|
||||||
@include dark-editor-theme;
|
|
||||||
@include green-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.light {
|
|
||||||
@include light-theme;
|
|
||||||
@include light-editor-theme;
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.dark {
|
|
||||||
@include dark-theme;
|
|
||||||
@include dark-editor-theme;
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root.black {
|
|
||||||
@include black-theme;
|
|
||||||
@include black-editor-theme;
|
|
||||||
color-scheme: dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="blue"] {
|
|
||||||
@include blue-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="green"] {
|
|
||||||
@include green-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="teal"] {
|
|
||||||
@include teal-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="indigo"] {
|
|
||||||
@include indigo-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="purple"] {
|
|
||||||
@include purple-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="orange"] {
|
|
||||||
@include orange-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="pink"] {
|
|
||||||
@include pink-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="red"] {
|
|
||||||
@include red-theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
:root[data-accent="yellow"] {
|
|
||||||
@include yellow-theme;
|
|
||||||
}
|
|
||||||
89
packages/hoppscotch-common/assets/themes/accent-themes.scss
Normal file
89
packages/hoppscotch-common/assets/themes/accent-themes.scss
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@mixin green-theme {
|
||||||
|
--accent-color: theme("colors.emerald.500");
|
||||||
|
--accent-light-color: theme("colors.emerald.400");
|
||||||
|
--accent-dark-color: theme("colors.emerald.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.emerald.400");
|
||||||
|
--gradient-via-color: theme("colors.emerald.500");
|
||||||
|
--gradient-to-color: theme("colors.emerald.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin teal-theme {
|
||||||
|
--accent-color: theme("colors.teal.500");
|
||||||
|
--accent-light-color: theme("colors.teal.400");
|
||||||
|
--accent-dark-color: theme("colors.teal.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.teal.400");
|
||||||
|
--gradient-via-color: theme("colors.teal.500");
|
||||||
|
--gradient-to-color: theme("colors.teal.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin blue-theme {
|
||||||
|
--accent-color: theme("colors.blue.500");
|
||||||
|
--accent-light-color: theme("colors.blue.400");
|
||||||
|
--accent-dark-color: theme("colors.blue.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.blue.400");
|
||||||
|
--gradient-via-color: theme("colors.blue.500");
|
||||||
|
--gradient-to-color: theme("colors.blue.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin indigo-theme {
|
||||||
|
--accent-color: theme("colors.indigo.500");
|
||||||
|
--accent-light-color: theme("colors.indigo.400");
|
||||||
|
--accent-dark-color: theme("colors.indigo.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.indigo.400");
|
||||||
|
--gradient-via-color: theme("colors.indigo.500");
|
||||||
|
--gradient-to-color: theme("colors.indigo.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin purple-theme {
|
||||||
|
--accent-color: theme("colors.purple.500");
|
||||||
|
--accent-light-color: theme("colors.purple.400");
|
||||||
|
--accent-dark-color: theme("colors.purple.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.purple.400");
|
||||||
|
--gradient-via-color: theme("colors.purple.500");
|
||||||
|
--gradient-to-color: theme("colors.purple.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin yellow-theme {
|
||||||
|
--accent-color: theme("colors.amber.500");
|
||||||
|
--accent-light-color: theme("colors.amber.400");
|
||||||
|
--accent-dark-color: theme("colors.amber.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.amber.400");
|
||||||
|
--gradient-via-color: theme("colors.amber.500");
|
||||||
|
--gradient-to-color: theme("colors.amber.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin orange-theme {
|
||||||
|
--accent-color: theme("colors.orange.500");
|
||||||
|
--accent-light-color: theme("colors.orange.400");
|
||||||
|
--accent-dark-color: theme("colors.orange.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.orange.400");
|
||||||
|
--gradient-via-color: theme("colors.orange.500");
|
||||||
|
--gradient-to-color: theme("colors.orange.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin red-theme {
|
||||||
|
--accent-color: theme("colors.red.500");
|
||||||
|
--accent-light-color: theme("colors.red.400");
|
||||||
|
--accent-dark-color: theme("colors.red.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.red.400");
|
||||||
|
--gradient-via-color: theme("colors.red.500");
|
||||||
|
--gradient-to-color: theme("colors.red.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin pink-theme {
|
||||||
|
--accent-color: theme("colors.pink.500");
|
||||||
|
--accent-light-color: theme("colors.pink.400");
|
||||||
|
--accent-dark-color: theme("colors.pink.600");
|
||||||
|
--accent-contrast-color: theme("colors.white");
|
||||||
|
--gradient-from-color: theme("colors.pink.400");
|
||||||
|
--gradient-via-color: theme("colors.pink.500");
|
||||||
|
--gradient-to-color: theme("colors.pink.600");
|
||||||
|
}
|
||||||
140
packages/hoppscotch-common/assets/themes/base-themes.scss
Normal file
140
packages/hoppscotch-common/assets/themes/base-themes.scss
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
@mixin base-theme {
|
||||||
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
|
--font-mono: "Roboto Mono Variable", monospace;
|
||||||
|
--font-size-body: 0.75rem;
|
||||||
|
--font-size-tiny: 0.625rem;
|
||||||
|
--line-height-body: 1rem;
|
||||||
|
--upper-primary-sticky-fold: 4.125rem;
|
||||||
|
--upper-secondary-sticky-fold: 6.188rem;
|
||||||
|
--upper-tertiary-sticky-fold: 8.25rem;
|
||||||
|
--upper-fourth-sticky-fold: 10.2rem;
|
||||||
|
--upper-mobile-primary-sticky-fold: 6.75rem;
|
||||||
|
--upper-mobile-secondary-sticky-fold: 8.813rem;
|
||||||
|
--upper-mobile-sticky-fold: 10.875rem;
|
||||||
|
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||||
|
--lower-primary-sticky-fold: 3rem;
|
||||||
|
--lower-secondary-sticky-fold: 5.063rem;
|
||||||
|
--lower-tertiary-sticky-fold: 7.125rem;
|
||||||
|
--lower-fourth-sticky-fold: 9.188rem;
|
||||||
|
--sidebar-primary-sticky-fold: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light-theme {
|
||||||
|
--primary-color: theme("colors.white");
|
||||||
|
--primary-light-color: theme("colors.gray.50");
|
||||||
|
--primary-dark-color: theme("colors.gray.100");
|
||||||
|
--primary-contrast-color: #fdfdfd;
|
||||||
|
|
||||||
|
--secondary-color: theme("colors.gray.500");
|
||||||
|
--secondary-light-color: theme("colors.gray.400");
|
||||||
|
--secondary-dark-color: theme("colors.gray.900");
|
||||||
|
|
||||||
|
--divider-color: theme("colors.gray.100");
|
||||||
|
--divider-light-color: theme("colors.gray.100");
|
||||||
|
--divider-dark-color: theme("colors.gray.300");
|
||||||
|
|
||||||
|
--banner-info-color: theme("colors.stone.100");
|
||||||
|
--banner-warning-color: theme("colors.yellow.100");
|
||||||
|
--banner-error-color: theme("colors.red.100");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.800");
|
||||||
|
--popover-color: theme("colors.white");
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.green.500");
|
||||||
|
--method-post-color: theme("colors.amber.500");
|
||||||
|
--method-put-color: theme("colors.blue.500");
|
||||||
|
--method-patch-color: theme("colors.purple.500");
|
||||||
|
--method-delete-color: theme("colors.red.500");
|
||||||
|
--method-head-color: theme("colors.lime.500");
|
||||||
|
--method-options-color: theme("colors.pink.500");
|
||||||
|
--method-default-color: theme("colors.gray.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
|
--editor-theme: "textmate";
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark-theme {
|
||||||
|
--primary-color: #181818;
|
||||||
|
--primary-light-color: #1c1c1e;
|
||||||
|
--primary-dark-color: theme("colors.neutral.800");
|
||||||
|
--primary-contrast-color: theme("colors.neutral.900");
|
||||||
|
|
||||||
|
--secondary-color: theme("colors.neutral.400");
|
||||||
|
--secondary-light-color: theme("colors.neutral.500");
|
||||||
|
--secondary-dark-color: theme("colors.zinc.50");
|
||||||
|
|
||||||
|
--divider-color: #1f1f1f;
|
||||||
|
--divider-light-color: #1f1f1f;
|
||||||
|
--divider-dark-color: theme("colors.zinc.800");
|
||||||
|
|
||||||
|
--banner-info-color: theme("colors.stone.800");
|
||||||
|
--banner-warning-color: theme("colors.yellow.800");
|
||||||
|
--banner-error-color: theme("colors.red.800");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.100");
|
||||||
|
--popover-color: #1b1b1b;
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.emerald.500");
|
||||||
|
--method-post-color: theme("colors.yellow.500");
|
||||||
|
--method-put-color: theme("colors.sky.500");
|
||||||
|
--method-patch-color: theme("colors.violet.500");
|
||||||
|
--method-delete-color: theme("colors.rose.500");
|
||||||
|
--method-head-color: theme("colors.teal.500");
|
||||||
|
--method-options-color: theme("colors.indigo.500");
|
||||||
|
--method-default-color: theme("colors.neutral.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
|
--editor-theme: "merbivore_soft";
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin black-theme {
|
||||||
|
--primary-color: #0f0f0f;
|
||||||
|
--primary-light-color: theme("colors.neutral.900");
|
||||||
|
--primary-dark-color: #181818;
|
||||||
|
--primary-contrast-color: #0f0f0f;
|
||||||
|
|
||||||
|
--secondary-color: theme("colors.neutral.400");
|
||||||
|
--secondary-light-color: theme("colors.neutral.500");
|
||||||
|
--secondary-dark-color: theme("colors.neutral.50");
|
||||||
|
|
||||||
|
--divider-color: theme("colors.neutral.900");
|
||||||
|
--divider-light-color: theme("colors.neutral.900");
|
||||||
|
--divider-dark-color: theme("colors.zinc.800");
|
||||||
|
|
||||||
|
--banner-info-color: theme("colors.stone.900");
|
||||||
|
--banner-warning-color: theme("colors.yellow.900");
|
||||||
|
--banner-error-color: theme("colors.red.900");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.100");
|
||||||
|
--popover-color: theme("colors.stone.950");
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.emerald.500");
|
||||||
|
--method-post-color: theme("colors.yellow.500");
|
||||||
|
--method-put-color: theme("colors.sky.500");
|
||||||
|
--method-patch-color: theme("colors.violet.500");
|
||||||
|
--method-delete-color: theme("colors.rose.500");
|
||||||
|
--method-head-color: theme("colors.teal.500");
|
||||||
|
--method-options-color: theme("colors.indigo.500");
|
||||||
|
--method-default-color: theme("colors.zinc.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
|
--editor-theme: "twilight";
|
||||||
|
}
|
||||||
41
packages/hoppscotch-common/assets/themes/editor-themes.scss
Normal file
41
packages/hoppscotch-common/assets/themes/editor-themes.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@mixin light-editor-theme {
|
||||||
|
--editor-type-color: theme("colors.violet.600");
|
||||||
|
--editor-name-color: theme("colors.red.600");
|
||||||
|
--editor-operator-color: theme("colors.indigo.600");
|
||||||
|
--editor-invalid-color: theme("colors.red.600");
|
||||||
|
--editor-separator-color: theme("colors.gray.600");
|
||||||
|
--editor-meta-color: theme("colors.gray.600");
|
||||||
|
--editor-variable-color: theme("colors.emerald.600");
|
||||||
|
--editor-link-color: theme("colors.cyan.600");
|
||||||
|
--editor-process-color: theme("colors.blue.600");
|
||||||
|
--editor-constant-color: theme("colors.fuchsia.600");
|
||||||
|
--editor-keyword-color: theme("colors.pink.600");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin dark-editor-theme {
|
||||||
|
--editor-type-color: theme("colors.violet.400");
|
||||||
|
--editor-name-color: theme("colors.blue.400");
|
||||||
|
--editor-operator-color: theme("colors.indigo.400");
|
||||||
|
--editor-invalid-color: theme("colors.red.400");
|
||||||
|
--editor-separator-color: theme("colors.gray.400");
|
||||||
|
--editor-meta-color: theme("colors.gray.400");
|
||||||
|
--editor-variable-color: theme("colors.emerald.400");
|
||||||
|
--editor-link-color: theme("colors.cyan.400");
|
||||||
|
--editor-process-color: theme("colors.fuchsia.400");
|
||||||
|
--editor-constant-color: theme("colors.violet.400");
|
||||||
|
--editor-keyword-color: theme("colors.pink.400");
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin black-editor-theme {
|
||||||
|
--editor-type-color: theme("colors.violet.400");
|
||||||
|
--editor-name-color: theme("colors.fuchsia.400");
|
||||||
|
--editor-operator-color: theme("colors.indigo.400");
|
||||||
|
--editor-invalid-color: theme("colors.red.400");
|
||||||
|
--editor-separator-color: theme("colors.gray.400");
|
||||||
|
--editor-meta-color: theme("colors.gray.400");
|
||||||
|
--editor-variable-color: theme("colors.emerald.400");
|
||||||
|
--editor-link-color: theme("colors.cyan.400");
|
||||||
|
--editor-process-color: theme("colors.violet.400");
|
||||||
|
--editor-constant-color: theme("colors.blue.400");
|
||||||
|
--editor-keyword-color: theme("colors.pink.400");
|
||||||
|
}
|
||||||
64
packages/hoppscotch-common/assets/themes/themes.scss
Normal file
64
packages/hoppscotch-common/assets/themes/themes.scss
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@import "./base-themes.scss";
|
||||||
|
@import "./editor-themes.scss";
|
||||||
|
@import "./accent-themes.scss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
@include base-theme;
|
||||||
|
@include dark-theme;
|
||||||
|
@include green-theme;
|
||||||
|
@include dark-editor-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light {
|
||||||
|
@include light-theme;
|
||||||
|
@include light-editor-theme;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.dark {
|
||||||
|
@include dark-theme;
|
||||||
|
@include dark-editor-theme;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.black {
|
||||||
|
@include black-theme;
|
||||||
|
@include black-editor-theme;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="blue"] {
|
||||||
|
@include blue-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="green"] {
|
||||||
|
@include green-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="teal"] {
|
||||||
|
@include teal-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="indigo"] {
|
||||||
|
@include indigo-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="purple"] {
|
||||||
|
@include purple-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="orange"] {
|
||||||
|
@include orange-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="pink"] {
|
||||||
|
@include pink-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="red"] {
|
||||||
|
@include red-theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root[data-accent="yellow"] {
|
||||||
|
@include yellow-theme;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"connecting": "Connecting",
|
"connecting": "Connecting",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"create": "Create",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"scroll_to_top": "Scroll to top",
|
"scroll_to_top": "Scroll to top",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
|
"share": "Share",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
"contact_us": "Contact us",
|
"contact_us": "Contact us",
|
||||||
"cookies": "Cookies",
|
"cookies": "Cookies",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"copy_interface_type": "Copy interface type",
|
||||||
"copy_user_id": "Copy User Auth Token",
|
"copy_user_id": "Copy User Auth Token",
|
||||||
"developer_option": "Developer options",
|
"developer_option": "Developer options",
|
||||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||||
@@ -93,6 +96,7 @@
|
|||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
"new_version_found": "New version found. Refresh to update.",
|
"new_version_found": "New version found. Refresh to update.",
|
||||||
|
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"proxy_privacy_policy": "Proxy privacy policy",
|
"proxy_privacy_policy": "Proxy privacy policy",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
@@ -139,7 +143,21 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"token": "Token",
|
"token": "Token",
|
||||||
"type": "Authorization Type",
|
"type": "Authorization Type",
|
||||||
"username": "Username"
|
"username": "Username",
|
||||||
|
"oauth": {
|
||||||
|
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
|
||||||
|
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
|
||||||
|
"redirect_auth_server_returned_error": "Auth Server returned an error state",
|
||||||
|
"redirect_no_auth_code": "No Authorization Code present in the redirect",
|
||||||
|
"redirect_invalid_state": "Invalid State value present in the redirect",
|
||||||
|
"redirect_no_token_endpoint": "No Token Endpoint Defined",
|
||||||
|
"redirect_no_client_id": "No Client ID defined",
|
||||||
|
"redirect_no_client_secret": "No Client Secret Defined",
|
||||||
|
"redirect_no_code_verifier": "No Code Verifier Defined",
|
||||||
|
"redirect_auth_token_request_failed": "Request to get the auth token failed",
|
||||||
|
"redirect_auth_token_request_invalid_response": "Invalid Response from the Token Endpoint when requesting for an auth token",
|
||||||
|
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"collection": {
|
"collection": {
|
||||||
"created": "Collection created",
|
"created": "Collection created",
|
||||||
@@ -173,6 +191,7 @@
|
|||||||
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
||||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||||
|
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||||
"remove_team": "Are you sure you want to delete this team?",
|
"remove_team": "Are you sure you want to delete this team?",
|
||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
@@ -214,7 +233,8 @@
|
|||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||||
|
"shared_requests": "Shared requests are empty",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Team name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any teams",
|
||||||
@@ -254,6 +274,9 @@
|
|||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
|
"graphql_collections": {
|
||||||
|
"title": "GraphQL Collections"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||||
"check_console_details": "Check console log for details.",
|
"check_console_details": "Check console log for details.",
|
||||||
@@ -289,7 +312,8 @@
|
|||||||
"create_secret_gist": "Create secret Gist",
|
"create_secret_gist": "Create secret Gist",
|
||||||
"gist_created": "Gist created",
|
"gist_created": "Gist created",
|
||||||
"require_github": "Login with GitHub to create secret gist",
|
"require_github": "Login with GitHub to create secret gist",
|
||||||
"title": "Export"
|
"title": "Export",
|
||||||
|
"failed": "Something went wrong while exporting"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
@@ -326,8 +350,8 @@
|
|||||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||||
"generate_documentation_first": "Generate documentation first",
|
"generate_documentation_first": "Generate documentation first",
|
||||||
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
||||||
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
|
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
||||||
"offline_short": "You seem to be offline.",
|
"offline_short": "You're using Hoppscotch offline.",
|
||||||
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
||||||
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
||||||
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
||||||
@@ -356,6 +380,7 @@
|
|||||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||||
"from_postman": "Import from Postman",
|
"from_postman": "Import from Postman",
|
||||||
"from_postman_description": "Import from Postman collection",
|
"from_postman_description": "Import from Postman collection",
|
||||||
|
"from_file": "Import from File",
|
||||||
"from_url": "Import from URL",
|
"from_url": "Import from URL",
|
||||||
"gist_url": "Enter Gist URL",
|
"gist_url": "Enter Gist URL",
|
||||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||||
@@ -363,7 +388,14 @@
|
|||||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||||
"import_from_url_success": "Collections Imported",
|
"import_from_url_success": "Collections Imported",
|
||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"title": "Import"
|
"title": "Import",
|
||||||
|
"hoppscotch_environment": "Hoppscotch Environment",
|
||||||
|
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||||
|
"postman_environment": "Postman Environment",
|
||||||
|
"postman_environment_description": "Import Postman Environment JSON file",
|
||||||
|
"environments_from_gist": "Import From Gist",
|
||||||
|
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||||
|
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
@@ -400,7 +432,9 @@
|
|||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "You have unsaved changes",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"customize_request": "Customize Request",
|
||||||
"edit_request": "Edit Request",
|
"edit_request": "Edit Request",
|
||||||
|
"share_request": "Share Request",
|
||||||
"import_export": "Import / Export"
|
"import_export": "Import / Export"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
@@ -476,14 +510,14 @@
|
|||||||
"structured": "Structured",
|
"structured": "Structured",
|
||||||
"text": "Text"
|
"text": "Text"
|
||||||
},
|
},
|
||||||
"copy_link": "Copy link",
|
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Cannot reorder requests from different collections",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
"enter_curl": "Enter cURL command",
|
"enter_curl": "Enter cURL command",
|
||||||
"generate_code": "Generate code",
|
"generate_code": "Generate code",
|
||||||
"generated_code": "Generated code",
|
"generated_code": "Generated code",
|
||||||
"go_to_authorization_tab": "Go to Authorization",
|
"go_to_authorization_tab": "Go to Authorization tab",
|
||||||
|
"go_to_body_tab": "Go to Body tab",
|
||||||
"header_list": "Header List",
|
"header_list": "Header List",
|
||||||
"invalid_name": "Please provide a name for the request",
|
"invalid_name": "Please provide a name for the request",
|
||||||
"method": "Method",
|
"method": "Method",
|
||||||
@@ -508,6 +542,7 @@
|
|||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_description": "Share Hoppscotch with your friends",
|
"share_description": "Share Hoppscotch with your friends",
|
||||||
|
"share_request": "Share Request",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
"title": "Request",
|
"title": "Request",
|
||||||
"type": "Request type",
|
"type": "Request type",
|
||||||
@@ -585,16 +620,34 @@
|
|||||||
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
|
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"verified_email": "Verified email",
|
"verified_email": "Verified email",
|
||||||
|
"additional": "Additional Settings",
|
||||||
"verify_email": "Verify email"
|
"verify_email": "Verify email"
|
||||||
},
|
},
|
||||||
"shortcodes": {
|
"shared_requests": {
|
||||||
"actions": "Actions",
|
"button": "Button",
|
||||||
"created_on": "Created on",
|
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||||
"deleted": "Shortcode deleted",
|
"customize": "Customize",
|
||||||
"method": "Method",
|
"creating_widget": "Creating widget",
|
||||||
"not_found": "Shortcode not found",
|
"copy_html": "Copy HTML",
|
||||||
"short_code": "Short code",
|
"copy_link": "Copy Link",
|
||||||
"url": "URL"
|
"copy_markdown": "Copy Markdown",
|
||||||
|
"deleted": "Shared request deleted",
|
||||||
|
"description": "Select a widget, you can change and customize this later",
|
||||||
|
"embed": "Embed",
|
||||||
|
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||||
|
"link": "Link",
|
||||||
|
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||||
|
"modified": "Shared request modified",
|
||||||
|
"not_found": "Shared request not found",
|
||||||
|
"open_new_tab": "Open in new tab",
|
||||||
|
"preview": "Preview",
|
||||||
|
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||||
|
"theme": {
|
||||||
|
"dark": "Dark",
|
||||||
|
"light": "Light",
|
||||||
|
"system": "System",
|
||||||
|
"title": "Theme"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"shortcut": {
|
"shortcut": {
|
||||||
"general": {
|
"general": {
|
||||||
@@ -624,7 +677,6 @@
|
|||||||
"title": "Others"
|
"title": "Others"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"copy_request_link": "Copy Request Link",
|
|
||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
@@ -640,6 +692,7 @@
|
|||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "Generate code snippet",
|
||||||
|
"share_request": "Share Request",
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -764,6 +817,7 @@
|
|||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "Connection lost",
|
||||||
"copied_to_clipboard": "Copied to clipboard",
|
"copied_to_clipboard": "Copied to clipboard",
|
||||||
|
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
"deprecated": "DEPRECATED",
|
"deprecated": "DEPRECATED",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@@ -822,6 +876,7 @@
|
|||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
|
"shared_requests": "Shared Requests",
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.8.3-1",
|
"version": "2023.8.4-1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||||
"test": "vitest --run",
|
"test": "vitest --run",
|
||||||
@@ -22,45 +22,41 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.10.2",
|
"@codemirror/autocomplete": "^6.11.0",
|
||||||
"@codemirror/commands": "^6.3.0",
|
"@codemirror/commands": "^6.3.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "6.9.0",
|
"@codemirror/language": "6.9.2",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.3.3",
|
||||||
"@codemirror/lint": "^6.4.2",
|
"@codemirror/lint": "^6.4.2",
|
||||||
"@codemirror/search": "^6.5.4",
|
"@codemirror/search": "^6.5.4",
|
||||||
"@codemirror/state": "^6.3.1",
|
"@codemirror/state": "^6.3.1",
|
||||||
"@codemirror/view": "^6.22.0",
|
"@codemirror/view": "^6.22.0",
|
||||||
"@fontsource-variable/inter": "^5.0.8",
|
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
|
||||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "workspace:^",
|
"@hoppscotch/ui": "workspace:^",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
"@lezer/highlight": "1.1.4",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@urql/core": "^4.1.1",
|
"@unhead/vue": "^1.8.8",
|
||||||
|
"@urql/core": "^4.2.0",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
"@urql/exchange-graphcache": "^6.3.2",
|
"@urql/exchange-graphcache": "^6.3.3",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.6.1",
|
||||||
"@vueuse/head": "^1.3.1",
|
"acorn-walk": "^8.3.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"axios": "^1.6.2",
|
||||||
"axios": "^1.4.0",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "^1.0.0",
|
||||||
"dioc": "workspace:^",
|
"dioc": "workspace:^",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
"fuse.js": "^6.6.2",
|
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "^1.0.3",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.1",
|
||||||
"graphql-language-service-interface": "^2.9.1",
|
"graphql-language-service-interface": "^2.10.2",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^3.0.1",
|
"httpsnippet": "^3.0.1",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "^3.6.0",
|
||||||
@@ -68,14 +64,15 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonpath-plus": "^7.2.0",
|
"jsonpath-plus": "^7.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^2.0.11",
|
"lossless-json": "^3.0.2",
|
||||||
"minisearch": "^6.1.0",
|
"minisearch": "^6.3.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postman-collection": "^4.2.0",
|
"postman-collection": "^4.3.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
|
"quicktype-core": "^23.0.79",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"set-cookie-parser-es": "^1.0.5",
|
"set-cookie-parser-es": "^1.0.5",
|
||||||
@@ -89,18 +86,19 @@
|
|||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"url": "^0.11.1",
|
"url": "^0.11.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^9.0.0",
|
"verzod": "^0.2.0",
|
||||||
"vue": "^3.3.4",
|
"uuid": "^9.0.1",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue": "^3.3.8",
|
||||||
"vue-pdf-embed": "^1.1.6",
|
"vue-i18n": "^9.7.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-pdf-embed": "^1.2.1",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
"vue-tippy": "6.3.1",
|
"vue-tippy": "6.3.1",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "^4.1.1",
|
||||||
"wonka": "^6.3.4",
|
"wonka": "^6.3.4",
|
||||||
"workbox-window": "^7.0.0",
|
"workbox-window": "^7.0.0",
|
||||||
"xml-formatter": "^3.5.0",
|
"xml-formatter": "^3.6.0",
|
||||||
"yargs-parser": "^21.1.1",
|
"yargs-parser": "^21.1.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
@@ -112,54 +110,58 @@
|
|||||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
"@graphql-codegen/typed-document-node": "^5.0.1",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@graphql-codegen/urql-introspection": "^3.0.0",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
"@iconify-json/lucide": "^1.1.119",
|
"@iconify-json/lucide": "^1.1.141",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
"@rushstack/eslint-patch": "^1.6.0",
|
||||||
"@types/har-format": "^1.2.12",
|
"@types/har-format": "^1.2.15",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/lossless-json": "^1.0.1",
|
"@types/lossless-json": "^1.0.4",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/paho-mqtt": "^1.0.7",
|
"@types/paho-mqtt": "^1.0.10",
|
||||||
"@types/postman-collection": "^3.5.7",
|
"@types/postman-collection": "^3.5.10",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.6",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.7",
|
||||||
"@types/yargs-parser": "^21.0.0",
|
"@types/yargs-parser": "^21.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@typescript-eslint/parser": "^6.4.0",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@vue/compiler-sfc": "^3.3.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
"@vue/runtime-core": "^3.3.8",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.18.1",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
"postcss": "^8.4.23",
|
||||||
"sass": "^1.66.0",
|
"prettier": "^3.1.0",
|
||||||
"typescript": "^5.1.6",
|
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||||
"unplugin-fonts": "^1.0.3",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
"unplugin-icons": "^0.16.5",
|
"sass": "^1.69.5",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"tailwindcss": "^3.3.2",
|
||||||
"vite": "^4.4.9",
|
"typescript": "^5.3.2",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"unplugin-fonts": "^1.1.1",
|
||||||
|
"unplugin-icons": "^0.17.4",
|
||||||
|
"unplugin-vue-components": "^0.25.2",
|
||||||
|
"vite": "^4.5.0",
|
||||||
|
"vite-plugin-checker": "^0.6.2",
|
||||||
|
"vite-plugin-fonts": "^0.7.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.42",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.17.0",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "^0.8.0",
|
||||||
"vite-plugin-windicss": "^1.9.1",
|
"vitest": "^0.34.6",
|
||||||
"vitest": "^0.34.2",
|
"vue-tsc": "^1.8.22"
|
||||||
"vue-tsc": "^1.8.8",
|
|
||||||
"windicss": "^3.5.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
v-if="isLoadingInitialRoute"
|
v-if="isLoadingInitialRoute"
|
||||||
class="flex flex-col items-center justify-center min-h-screen"
|
class="flex min-h-screen flex-col items-center justify-center"
|
||||||
>
|
>
|
||||||
<HoppSmartSpinner />
|
<HoppSmartSpinner />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
22
packages/hoppscotch-common/src/components.d.ts
vendored
22
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -9,6 +9,7 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
|
||||||
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
AppAnnouncement: typeof import('./components/app/Announcement.vue')['default']
|
||||||
|
AppBanner: typeof import('./components/app/Banner.vue')['default']
|
||||||
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
|
||||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
|
||||||
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
AppFooter: typeof import('./components/app/Footer.vue')['default']
|
||||||
@@ -60,6 +61,7 @@ declare module 'vue' {
|
|||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||||
|
Embeds: typeof import('./components/embeds/index.vue')['default']
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
@@ -92,11 +94,13 @@ declare module 'vue' {
|
|||||||
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
|
||||||
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
|
||||||
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
|
||||||
|
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
|
||||||
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
|
||||||
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
|
||||||
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
|
||||||
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
|
||||||
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
|
||||||
|
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
|
||||||
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
|
||||||
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
|
||||||
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
|
||||||
@@ -105,6 +109,7 @@ declare module 'vue' {
|
|||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
|
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
@@ -140,9 +145,11 @@ declare module 'vue' {
|
|||||||
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
HttpTests: typeof import('./components/http/Tests.vue')['default']
|
||||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||||
|
IconLucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
|
||||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||||
|
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
|
||||||
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
|
||||||
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
|
||||||
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
|
||||||
@@ -152,8 +159,11 @@ declare module 'vue' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
|
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||||
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
||||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
@@ -177,6 +187,16 @@ declare module 'vue' {
|
|||||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||||
|
Share: typeof import('./components/share/index.vue')['default']
|
||||||
|
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
|
||||||
|
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
|
||||||
|
ShareModal: typeof import('./components/share/Modal.vue')['default']
|
||||||
|
ShareRequest: typeof import('./components/share/Request.vue')['default']
|
||||||
|
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
|
||||||
|
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
|
||||||
|
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
|
||||||
|
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
|
||||||
|
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
@@ -197,9 +217,11 @@ declare module 'vue' {
|
|||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
|
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
|
||||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
|
SmartTable: typeof import('./../../hoppscotch-ui/src/components/smart/Table.vue')['default']
|
||||||
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
SmartTabs: typeof import('./../../hoppscotch-ui/src/components/smart/Tabs.vue')['default']
|
||||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||||
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="relative flex items-center px-4 py-2 transition bg-error text-tiny group"
|
|
||||||
role="alert"
|
|
||||||
>
|
|
||||||
<icon-lucide-info class="mr-2" />
|
|
||||||
<span class="text-secondaryDark">
|
|
||||||
<span class="md:hidden">
|
|
||||||
{{ t("helpers.offline_short") }}
|
|
||||||
</span>
|
|
||||||
<span class="<md:hidden">
|
|
||||||
{{ t("helpers.offline") }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
</script>
|
|
||||||
70
packages/hoppscotch-common/src/components/app/Banner.vue
Normal file
70
packages/hoppscotch-common/src/components/app/Banner.vue
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:role="bannerRole"
|
||||||
|
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
|
||||||
|
:class="bannerColor"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<component :is="bannerIcon" class="mr-2" />
|
||||||
|
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
|
||||||
|
{{ banner.text(t) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
|
||||||
|
{{ banner.alternateText(t) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<icon-lucide-x
|
||||||
|
v-if="dismissible"
|
||||||
|
class="opacity-50 hover:cursor-pointer hover:opacity-100"
|
||||||
|
@click="emit('dismiss')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { BannerContent, BannerType } from "~/services/banner.service"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
import IconAlertCircle from "~icons/lucide/alert-circle"
|
||||||
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
|
import IconInfo from "~icons/lucide/info"
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
banner: BannerContent
|
||||||
|
dismissible?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "dismiss"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const ariaRoles: Record<BannerType, string> = {
|
||||||
|
info: "status",
|
||||||
|
warning: "status",
|
||||||
|
error: "alert",
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgColors: Record<BannerType, string> = {
|
||||||
|
info: "bg-bannerInfo",
|
||||||
|
warning: "bg-bannerWarning",
|
||||||
|
error: "bg-bannerError",
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
info: IconInfo,
|
||||||
|
warning: IconAlertCircle,
|
||||||
|
error: IconAlertTriangle,
|
||||||
|
}
|
||||||
|
|
||||||
|
const bannerColor = computed(() => bgColors[props.banner.type])
|
||||||
|
const bannerIcon = computed(() => icons[props.banner.type])
|
||||||
|
const bannerRole = computed(() => ariaRoles[props.banner.type])
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="contextMenuRef"
|
ref="contextMenuRef"
|
||||||
class="fixed bg-popover shadow-lg transform translate-y-8 border border-dividerDark p-2 rounded"
|
class="fixed translate-y-8 transform rounded border border-dividerDark bg-popover p-2 shadow-lg"
|
||||||
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
|
||||||
>
|
>
|
||||||
<div v-if="contextMenuOptions" class="flex flex-col">
|
<div v-if="contextMenuOptions" class="flex flex-col">
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<p class="px-2 mb-4 text-secondaryLight">
|
<p class="mb-4 px-2 text-secondaryLight">
|
||||||
{{ t("app.developer_option_description") }}
|
{{ t("app.developer_option_description") }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
|
|||||||
@@ -181,7 +181,7 @@
|
|||||||
@click="COLUMN_LAYOUT = !COLUMN_LAYOUT"
|
@click="COLUMN_LAYOUT = !COLUMN_LAYOUT"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
class="transition transform"
|
class="transform transition"
|
||||||
:class="{
|
:class="{
|
||||||
'rotate-180': SIDEBAR_ON_LEFT,
|
'rotate-180': SIDEBAR_ON_LEFT,
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -2,29 +2,31 @@
|
|||||||
<div>
|
<div>
|
||||||
<header
|
<header
|
||||||
ref="headerRef"
|
ref="headerRef"
|
||||||
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
|
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
|
||||||
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
|
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="inline-flex items-center justify-start flex-1 space-x-2"
|
class="col-span-2 flex items-center justify-between space-x-2"
|
||||||
:style="{
|
:style="{
|
||||||
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
||||||
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<div class="flex">
|
||||||
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
<HoppButtonSecondary
|
||||||
:label="t('app.name')"
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
to="/"
|
:label="t('app.name')"
|
||||||
/>
|
to="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex flex-1 items-center justify-between px-2 py-1 self-stretch bg-primaryDark transition text-secondaryLight cursor-text rounded border border-dividerDark max-w-60 hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
<icon-lucide-search class="mr-2 svg-icons" />
|
<icon-lucide-search class="svg-icons mr-2" />
|
||||||
{{ t("app.search") }}
|
{{ t("app.search") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex space-x-1">
|
<span class="flex space-x-1">
|
||||||
@@ -32,192 +34,189 @@
|
|||||||
<kbd class="shortcut-key">K</kbd>
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="showInstallButton"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('header.install_pwa')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="installPWA()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
|
||||||
:title="`${
|
|
||||||
mdAndLarger ? t('support.title') : t('app.options')
|
|
||||||
} <kbd>?</kbd>`"
|
|
||||||
:icon="IconLifeBuoy"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="invokeAction('modals.support.toggle')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||||
<div
|
<div class="flex">
|
||||||
v-if="currentUser === null"
|
|
||||||
class="inline-flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconUploadCloud"
|
v-if="showInstallButton"
|
||||||
:label="t('header.save_workspace')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="hidden md:flex bg-green-500/15 py-1.75 border border-green-600/25 !text-green-500 hover:bg-green-400/10 focus-visible:bg-green-400/10 focus-visible:border-green-800/50 !focus-visible:text-green-600 hover:border-green-800/50 !hover:text-green-600"
|
:title="t('header.install_pwa')"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
:icon="IconDownload"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="installPWA()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonPrimary
|
<HoppButtonSecondary
|
||||||
:label="t('header.login')"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
:title="`${
|
||||||
|
mdAndLarger ? t('support.title') : t('app.options')
|
||||||
|
} <kbd>?</kbd>`"
|
||||||
|
:icon="IconLifeBuoy"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="inline-flex items-center space-x-2">
|
<div class="flex">
|
||||||
<TeamsMemberStack
|
|
||||||
v-if="
|
|
||||||
workspace.type === 'team' &&
|
|
||||||
selectedTeam &&
|
|
||||||
selectedTeam.teamMembers.length > 1
|
|
||||||
"
|
|
||||||
:team-members="selectedTeam.teamMembers"
|
|
||||||
show-count
|
|
||||||
class="mx-2"
|
|
||||||
@handle-click="handleTeamEdit()"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="flex border divide-x rounded bg-green-500/15 divide-green-600/25 border-green-600/25 focus-within:bg-green-400/10 focus-within:border-green-800/50 focus-within:divide-green-800/50 hover:bg-green-400/10 hover:border-green-800/50 hover:divide-green-800/50"
|
v-if="currentUser === null"
|
||||||
|
class="inline-flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:icon="IconUploadCloud"
|
||||||
:title="t('team.invite_tooltip')"
|
:label="t('header.save_workspace')"
|
||||||
:icon="IconUserPlus"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex"
|
||||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
@click="handleInvite()"
|
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonPrimary
|
||||||
|
:label="t('header.login')"
|
||||||
|
class="h-8"
|
||||||
|
@click="invokeAction('modals.login.toggle')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="inline-flex items-center space-x-2">
|
||||||
|
<TeamsMemberStack
|
||||||
v-if="
|
v-if="
|
||||||
workspace.type === 'team' &&
|
workspace.type === 'team' &&
|
||||||
selectedTeam &&
|
selectedTeam &&
|
||||||
selectedTeam?.myRole === 'OWNER'
|
selectedTeam.teamMembers.length > 1
|
||||||
"
|
"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:team-members="selectedTeam.teamMembers"
|
||||||
:title="t('team.edit')"
|
show-count
|
||||||
:icon="IconSettings"
|
class="mx-2"
|
||||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
@handle-click="handleTeamEdit()"
|
||||||
@click="handleTeamEdit()"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div
|
||||||
<tippy
|
class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20"
|
||||||
interactive
|
>
|
||||||
trigger="click"
|
<HoppButtonSecondary
|
||||||
theme="popover"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:on-shown="() => accountActions.focus()"
|
:title="t('team.invite_tooltip')"
|
||||||
>
|
:icon="IconUserPlus"
|
||||||
<HoppButtonSecondary
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
@click="handleInvite()"
|
||||||
:title="t('workspace.change')"
|
/>
|
||||||
:label="mdAndLarger ? workspaceName : ``"
|
<HoppButtonSecondary
|
||||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
v-if="
|
||||||
class="pr-8 select-wrapper rounded bg-blue-500/15 py-1.75 border border-blue-600/25 !text-blue-500 focus-visible:bg-blue-400/10 focus-visible:border-blue-800/50 !focus-visible:text-blue-600 hover:bg-blue-400/10 hover:border-blue-800/50 !hover:text-blue-600"
|
workspace.type === 'team' &&
|
||||||
/>
|
selectedTeam &&
|
||||||
<template #content="{ hide }">
|
selectedTeam?.myRole === 'OWNER'
|
||||||
<div
|
"
|
||||||
ref="accountActions"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="flex flex-col focus:outline-none"
|
:title="t('team.edit')"
|
||||||
tabindex="0"
|
:icon="IconSettings"
|
||||||
@keyup.escape="hide()"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
@click="hide()"
|
@click="handleTeamEdit()"
|
||||||
>
|
/>
|
||||||
<WorkspaceSelector />
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
<span class="px-2">
|
|
||||||
<tippy
|
<tippy
|
||||||
interactive
|
interactive
|
||||||
trigger="click"
|
trigger="click"
|
||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => accountActions.focus()"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<HoppSmartSelectWrapper
|
||||||
v-if="currentUser.photoURL"
|
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
|
||||||
v-tippy="{
|
>
|
||||||
theme: 'tooltip',
|
<HoppButtonSecondary
|
||||||
}"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:url="currentUser.photoURL"
|
:title="t('workspace.change')"
|
||||||
:alt="
|
:label="mdAndLarger ? workspaceName : ``"
|
||||||
currentUser.displayName ||
|
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||||
t('profile.default_hopp_displayname')
|
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
|
||||||
"
|
/>
|
||||||
:title="
|
</HoppSmartSelectWrapper>
|
||||||
currentUser.displayName ||
|
|
||||||
currentUser.email ||
|
|
||||||
t('profile.default_hopp_displayname')
|
|
||||||
"
|
|
||||||
indicator
|
|
||||||
:indicator-styles="
|
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartPicture
|
|
||||||
v-else
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
currentUser.displayName ||
|
|
||||||
currentUser.email ||
|
|
||||||
t('profile.default_hopp_displayname')
|
|
||||||
"
|
|
||||||
:initial="currentUser.displayName || currentUser.email"
|
|
||||||
indicator
|
|
||||||
:indicator-styles="
|
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="accountActions"
|
||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.p="profile.$el.click()"
|
|
||||||
@keyup.s="settings.$el.click()"
|
|
||||||
@keyup.l="logout.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
|
@click="hide()"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col px-2 text-tiny">
|
<WorkspaceSelector />
|
||||||
<span class="inline-flex font-semibold truncate">
|
|
||||||
{{
|
|
||||||
currentUser.displayName ||
|
|
||||||
t("profile.default_hopp_displayname")
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span class="inline-flex truncate text-secondaryLight">
|
|
||||||
{{ currentUser.email }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="profile"
|
|
||||||
to="/profile"
|
|
||||||
:icon="IconUser"
|
|
||||||
:label="t('navigation.profile')"
|
|
||||||
:shortcut="['P']"
|
|
||||||
@click="hide()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="settings"
|
|
||||||
to="/settings"
|
|
||||||
:icon="IconSettings"
|
|
||||||
:label="t('navigation.settings')"
|
|
||||||
:shortcut="['S']"
|
|
||||||
@click="hide()"
|
|
||||||
/>
|
|
||||||
<FirebaseLogout
|
|
||||||
ref="logout"
|
|
||||||
:shortcut="['L']"
|
|
||||||
@confirm-logout="hide()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
</span>
|
<span class="px-2">
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppSmartPicture
|
||||||
|
v-tippy="{
|
||||||
|
theme: 'tooltip',
|
||||||
|
}"
|
||||||
|
:name="currentUser.uid"
|
||||||
|
:title="
|
||||||
|
currentUser.displayName ||
|
||||||
|
currentUser.email ||
|
||||||
|
t('profile.default_hopp_displayname')
|
||||||
|
"
|
||||||
|
indicator
|
||||||
|
:indicator-styles="
|
||||||
|
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.p="profile.$el.click()"
|
||||||
|
@keyup.s="settings.$el.click()"
|
||||||
|
@keyup.l="logout.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col px-2">
|
||||||
|
<span class="inline-flex truncate font-semibold">
|
||||||
|
{{
|
||||||
|
currentUser.displayName ||
|
||||||
|
t("profile.default_hopp_displayname")
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex truncate text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ currentUser.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="profile"
|
||||||
|
to="/profile"
|
||||||
|
:icon="IconUser"
|
||||||
|
:label="t('navigation.profile')"
|
||||||
|
:shortcut="['P']"
|
||||||
|
@click="hide()"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="settings"
|
||||||
|
to="/settings"
|
||||||
|
:icon="IconSettings"
|
||||||
|
:label="t('navigation.settings')"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="hide()"
|
||||||
|
/>
|
||||||
|
<FirebaseLogout
|
||||||
|
ref="logout"
|
||||||
|
:shortcut="['L']"
|
||||||
|
@confirm-logout="hide()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<AppAnnouncement v-if="!network.isOnline" />
|
<AppBanner
|
||||||
|
v-if="bannerContent"
|
||||||
|
:banner="bannerContent"
|
||||||
|
:dismissible="true"
|
||||||
|
@dismiss="dismissOfflineBanner"
|
||||||
|
/>
|
||||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||||
<TeamsInvite
|
<TeamsInvite
|
||||||
v-if="workspace.type === 'team' && workspace.teamID"
|
v-if="workspace.type === 'team' && workspace.teamID"
|
||||||
@@ -233,7 +232,6 @@
|
|||||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||||
@refetch-teams="refetchTeams"
|
@refetch-teams="refetchTeams"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
:title="t('confirm.remove_team')"
|
:title="t('confirm.remove_team')"
|
||||||
@@ -266,6 +264,11 @@ import IconUsers from "~icons/lucide/users"
|
|||||||
import { pipe } from "fp-ts/function"
|
import { pipe } from "fp-ts/function"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
import * as TE from "fp-ts/TaskEither"
|
||||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||||
|
import {
|
||||||
|
BannerService,
|
||||||
|
BannerContent,
|
||||||
|
BANNER_PRIORITY_HIGH,
|
||||||
|
} from "~/services/banner.service"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -283,7 +286,33 @@ const showTeamsModal = ref(false)
|
|||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
const mdAndLarger = breakpoints.greater("md")
|
const mdAndLarger = breakpoints.greater("md")
|
||||||
|
|
||||||
|
const banner = useService(BannerService)
|
||||||
|
const bannerContent = computed(() => banner.content.value?.content)
|
||||||
|
let bannerID: number | null = null
|
||||||
|
|
||||||
|
const offlineBanner: BannerContent = {
|
||||||
|
type: "warning",
|
||||||
|
text: (t) => t("helpers.offline"),
|
||||||
|
alternateText: (t) => t("helpers.offline_short"),
|
||||||
|
score: BANNER_PRIORITY_HIGH,
|
||||||
|
}
|
||||||
|
|
||||||
const network = reactive(useNetwork())
|
const network = reactive(useNetwork())
|
||||||
|
const isOnline = computed(() => network.isOnline)
|
||||||
|
|
||||||
|
// Show the offline banner if the user is offline
|
||||||
|
watch(isOnline, () => {
|
||||||
|
if (!isOnline.value) {
|
||||||
|
bannerID = banner.showBanner(offlineBanner)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if (banner.content && bannerID) {
|
||||||
|
banner.removeBanner(bannerID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getProbableUserStream(),
|
platform.auth.getProbableUserStream(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="inspectionResults && inspectionResults.length > 0">
|
<div v-if="inspectionResults && inspectionResults.length > 0">
|
||||||
<tippy interactive trigger="click" theme="popover">
|
<tippy interactive trigger="click" theme="popover">
|
||||||
<div class="flex justify-center items-center flex-1 flex-col">
|
<div class="flex flex-1 flex-col items-center justify-center">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:icon="IconAlertTriangle"
|
:icon="IconAlertTriangle"
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div class="flex flex-col space-y-2 items-start flex-1">
|
<div class="flex flex-1 flex-col items-start space-y-2">
|
||||||
<div
|
<div
|
||||||
class="flex justify-between border rounded pl-2 border-divider bg-popover sticky top-0 self-stretch"
|
class="sticky top-0 flex justify-between self-stretch rounded border border-divider bg-popover pl-2"
|
||||||
>
|
>
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex flex-1 items-center">
|
||||||
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
<icon-lucide-activity class="svg-icons mr-2 text-accent" />
|
||||||
<span class="font-bold">
|
<span class="font-bold">
|
||||||
{{ t("inspections.title") }}
|
{{ t("inspections.title") }}
|
||||||
</span>
|
</span>
|
||||||
@@ -31,10 +31,10 @@
|
|||||||
<div
|
<div
|
||||||
v-for="(inspector, index) in inspectionResults"
|
v-for="(inspector, index) in inspectionResults"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex self-stretch max-w-md w-full"
|
class="flex w-full max-w-md self-stretch"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col flex-1 rounded border border-dashed border-dividerDark divide-y divide-dashed divide-dividerDark"
|
class="flex flex-1 flex-col divide-y divide-dashed divide-dividerDark rounded border border-dashed border-dividerDark"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="inspector.text.type === 'text'"
|
v-if="inspector.text.type === 'text'"
|
||||||
@@ -44,13 +44,13 @@
|
|||||||
<HoppSmartLink
|
<HoppSmartLink
|
||||||
blank
|
blank
|
||||||
:to="inspector.doc.link"
|
:to="inspector.doc.link"
|
||||||
class="text-accent hover:text-accentDark transition"
|
class="text-accent transition hover:text-accentDark"
|
||||||
>
|
>
|
||||||
{{ inspector.doc.text }}
|
{{ inspector.doc.text }}
|
||||||
<icon-lucide-arrow-up-right class="svg-icons" />
|
<icon-lucide-arrow-up-right class="svg-icons" />
|
||||||
</HoppSmartLink>
|
</HoppSmartLink>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="inspector.action" class="flex p-2 space-x-2">
|
<span v-if="inspector.action" class="flex space-x-2 p-2">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="inspector.action.text"
|
:label="inspector.action.text"
|
||||||
outline
|
outline
|
||||||
@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
|
|||||||
},
|
},
|
||||||
{ severity: 0 }
|
{ severity: 0 }
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return { severity: 0 }
|
|
||||||
}
|
}
|
||||||
|
return { severity: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const severityColor = (severity: number) => {
|
const severityColor = (severity: number) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
<h2 class="p-4 font-bold font-semibold text-secondaryDark">
|
||||||
{{ t("layout.name") }}
|
{{ t("layout.name") }}
|
||||||
</h2>
|
</h2>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
active
|
active
|
||||||
@click="expandCollection"
|
@click="expandCollection"
|
||||||
/>
|
/>
|
||||||
<h2 class="p-4 font-semibold font-bold text-secondaryDark">
|
<h2 class="p-4 font-bold font-semibold text-secondaryDark">
|
||||||
{{ t("support.title") }}
|
{{ t("support.title") }}
|
||||||
</h2>
|
</h2>
|
||||||
<template
|
<template
|
||||||
|
|||||||
@@ -47,14 +47,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Splitpanes, Pane } from "splitpanes"
|
import { Pane, Splitpanes } from "splitpanes"
|
||||||
|
|
||||||
import "splitpanes/dist/splitpanes.css"
|
import "splitpanes/dist/splitpanes.css"
|
||||||
|
|
||||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
|
||||||
import { computed, useSlots, ref } from "vue"
|
|
||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { setLocalConfig, getLocalConfig } from "~/newstore/localpersistence"
|
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { computed, ref, useSlots } from "vue"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ const SIDEBAR = useSetting("SIDEBAR")
|
|||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
const hasSidebar = computed(() => !!slots.sidebar)
|
const hasSidebar = computed(() => !!slots.sidebar)
|
||||||
const hasSecondary = computed(() => !!slots.secondary)
|
const hasSecondary = computed(() => !!slots.secondary)
|
||||||
|
|
||||||
@@ -96,7 +99,7 @@ if (!COLUMN_LAYOUT.value) {
|
|||||||
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
|
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
|
||||||
if (!props.layoutId) return
|
if (!props.layoutId) return
|
||||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||||
setLocalConfig(storageKey, JSON.stringify(event))
|
persistenceService.setLocalConfig(storageKey, JSON.stringify(event))
|
||||||
}
|
}
|
||||||
|
|
||||||
function populatePaneEvent() {
|
function populatePaneEvent() {
|
||||||
@@ -119,7 +122,7 @@ function populatePaneEvent() {
|
|||||||
|
|
||||||
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
|
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
|
||||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||||
const paneEvent = getLocalConfig(storageKey)
|
const paneEvent = persistenceService.getLocalConfig(storageKey)
|
||||||
if (!paneEvent) return null
|
if (!paneEvent) return null
|
||||||
return JSON.parse(paneEvent)
|
return JSON.parse(paneEvent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
class="share-link"
|
class="share-link"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
<component :is="platform.icon" class="w-6 h-6" />
|
<component :is="platform.icon" class="h-6 w-6" />
|
||||||
<span class="mt-3">
|
<span class="mt-3">
|
||||||
{{ platform.name }}
|
{{ platform.name }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<button class="share-link" @click="copyAppLink">
|
<button class="share-link" @click="copyAppLink">
|
||||||
<component :is="copyIcon" class="w-6 h-6 text-xl" />
|
<component :is="copyIcon" class="h-6 w-6 text-xl" />
|
||||||
<span class="mt-3">
|
<span class="mt-3">
|
||||||
{{ t("app.copy") }}
|
{{ t("app.copy") }}
|
||||||
</span>
|
</span>
|
||||||
@@ -119,14 +119,14 @@ const hideModal = () => {
|
|||||||
.share-link {
|
.share-link {
|
||||||
@apply border border-dividerLight;
|
@apply border border-dividerLight;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply flex-col flex;
|
@apply flex flex-col;
|
||||||
@apply p-4;
|
@apply p-4;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply justify-center;
|
@apply justify-center;
|
||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply hover: (bg-primaryLight text-secondaryDark);
|
@apply hover:bg-primaryLight hover:text-secondaryDark;
|
||||||
@apply focus: outline-none;
|
@apply focus:outline-none;
|
||||||
@apply focus-visible: border-divider;
|
@apply focus-visible:border-divider;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
@apply opacity-80;
|
@apply opacity-80;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
|
<HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary"
|
||||||
>
|
>
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
v-model="filterText"
|
v-model="filterText"
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
v-if="isEmpty(shortcutsResults)"
|
v-if="isEmpty(shortcutsResults)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
<details
|
<details
|
||||||
@@ -28,16 +28,16 @@
|
|||||||
open
|
open
|
||||||
>
|
>
|
||||||
<summary
|
<summary
|
||||||
class="flex items-center flex-1 min-w-0 px-6 py-4 font-semibold transition cursor-pointer focus:outline-none text-secondaryLight hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer items-center px-6 py-4 font-semibold text-secondaryLight transition hover:text-secondaryDark focus:outline-none"
|
||||||
>
|
>
|
||||||
<icon-lucide-chevron-right class="mr-2 indicator" />
|
<icon-lucide-chevron-right class="indicator mr-2" />
|
||||||
<span
|
<span
|
||||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
class="capitalize-first truncate font-semibold text-secondaryDark"
|
||||||
>
|
>
|
||||||
{{ sectionTitle }}
|
{{ sectionTitle }}
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="flex flex-col px-6 pb-4 space-y-2">
|
<div class="flex flex-col space-y-2 px-6 pb-4">
|
||||||
<AppShortcutsEntry
|
<AppShortcutsEntry
|
||||||
v-for="(shortcut, index) in sectionResults"
|
v-for="(shortcut, index) in sectionResults"
|
||||||
:key="`shortcut-${index}`"
|
:key="`shortcut-${index}`"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center py-1">
|
<div class="flex items-center py-1">
|
||||||
<span class="flex flex-1 mr-4">
|
<span class="mr-4 flex flex-1">
|
||||||
{{ shortcut.label }}
|
{{ shortcut.label }}
|
||||||
</span>
|
</span>
|
||||||
<kbd
|
<kbd
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
<div class="flex flex-col items-center justify-center text-secondaryLight">
|
||||||
<div class="flex mb-4 space-x-2">
|
<div class="mb-4 flex space-x-2">
|
||||||
<div class="flex flex-col items-end space-y-4 text-right">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex flex-1 items-center">
|
||||||
{{ t("shortcut.request.send_request") }}
|
{{ t("shortcut.request.send_request") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex flex-1 items-center">
|
||||||
{{ t("shortcut.general.show_all") }}
|
{{ t("shortcut.general.show_all") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex flex-1 items-center">
|
||||||
{{ t("shortcut.general.command_menu") }}
|
{{ t("shortcut.general.command_menu") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex flex-1 items-center">
|
||||||
{{ t("shortcut.general.help_menu") }}
|
{{ t("shortcut.general.help_menu") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<aside class="flex justify-between h-full md:flex-col">
|
<aside class="flex h-full justify-between md:flex-col">
|
||||||
<nav class="flex flex-1 flex-nowrap md:flex-col md:flex-none bg-primary">
|
<nav class="flex flex-1 flex-nowrap bg-primary md:flex-none md:flex-col">
|
||||||
<HoppSmartLink
|
<HoppSmartLink
|
||||||
v-for="(navigation, index) in primaryNavigation"
|
v-for="(navigation, index) in primaryNavigation"
|
||||||
:key="`navigation-${index}`"
|
:key="`navigation-${index}`"
|
||||||
@@ -73,25 +73,25 @@ const primaryNavigation = [
|
|||||||
.nav-link {
|
.nav-link {
|
||||||
@apply relative;
|
@apply relative;
|
||||||
@apply p-4;
|
@apply p-4;
|
||||||
@apply flex flex-col flex-1;
|
@apply flex flex-1 flex-col;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply justify-center;
|
@apply justify-center;
|
||||||
@apply hover: (bg-primaryDark text-secondaryDark);
|
@apply hover:bg-primaryDark hover:text-secondaryDark;
|
||||||
@apply focus-visible: text-secondaryDark;
|
@apply focus-visible:text-secondaryDark;
|
||||||
@apply after:absolute;
|
@apply after:absolute;
|
||||||
@apply after:inset-x-0;
|
@apply after:inset-x-0;
|
||||||
@apply after:md: inset-x-auto;
|
@apply after:md:inset-x-auto;
|
||||||
@apply after:md: inset-y-0;
|
@apply after:md:inset-y-0;
|
||||||
@apply after:bottom-0;
|
@apply after:bottom-0;
|
||||||
@apply after:md: bottom-auto;
|
@apply after:md:bottom-auto;
|
||||||
@apply after:md: left-0;
|
@apply after:md:left-0;
|
||||||
@apply after:z-2;
|
@apply after:z-10;
|
||||||
@apply after:h-0.5;
|
@apply after:h-0.5;
|
||||||
@apply after:md: h-full;
|
@apply after:md:h-full;
|
||||||
@apply after:w-full;
|
@apply after:w-full;
|
||||||
@apply after:md: w-0.5;
|
@apply after:md:w-0.5;
|
||||||
@apply after:content-DEFAULT;
|
@apply after:content-[""];
|
||||||
@apply focus: after: bg-divider;
|
@apply focus:after:bg-divider;
|
||||||
|
|
||||||
.svg-icons {
|
.svg-icons {
|
||||||
@apply opacity-75;
|
@apply opacity-75;
|
||||||
@@ -105,7 +105,7 @@ const primaryNavigation = [
|
|||||||
&.router-link-active {
|
&.router-link-active {
|
||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@apply bg-primaryLight;
|
@apply bg-primaryLight;
|
||||||
@apply hover: text-secondaryDark;
|
@apply hover:text-secondaryDark;
|
||||||
@apply after:bg-accent;
|
@apply after:bg-accent;
|
||||||
|
|
||||||
.svg-icons {
|
.svg-icons {
|
||||||
@@ -116,7 +116,7 @@ const primaryNavigation = [
|
|||||||
&.exact-active-link {
|
&.exact-active-link {
|
||||||
@apply text-secondaryDark;
|
@apply text-secondaryDark;
|
||||||
@apply bg-primaryLight;
|
@apply bg-primaryLight;
|
||||||
@apply hover: text-secondaryDark;
|
@apply hover:text-secondaryDark;
|
||||||
@apply after:bg-accent;
|
@apply after:bg-accent;
|
||||||
|
|
||||||
.svg-icons {
|
.svg-icons {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
ref="el"
|
ref="el"
|
||||||
class="flex items-center flex-1 px-6 py-4 font-medium space-x-4 transition cursor-pointer relative search-entry focus:outline-none"
|
class="search-entry relative flex flex-1 cursor-pointer items-center space-x-4 px-6 py-4 font-medium transition focus:outline-none"
|
||||||
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
|
:class="{ 'active bg-primaryLight text-secondaryDark': active }"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@click="emit('action')"
|
@click="emit('action')"
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="entry.icon"
|
:is="entry.icon"
|
||||||
class="opacity-50 svg-icons"
|
class="svg-icons opacity-80"
|
||||||
:class="{ 'opacity-100': active }"
|
:class="{ 'opacity-25': active }"
|
||||||
/>
|
/>
|
||||||
<template
|
<template
|
||||||
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
||||||
@@ -82,9 +82,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const formattedShortcutKeys = computed(
|
const formattedShortcutKeys = computed(
|
||||||
() =>
|
() =>
|
||||||
props.entry.meta?.keyboardShortcut?.map((key) => {
|
props.entry.meta?.keyboardShortcut?.map(
|
||||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||||
})
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -112,9 +112,9 @@ watch(
|
|||||||
@apply after:left-0;
|
@apply after:left-0;
|
||||||
@apply after:bottom-0;
|
@apply after:bottom-0;
|
||||||
@apply after:bg-transparent;
|
@apply after:bg-transparent;
|
||||||
@apply after:z-2;
|
@apply after:z-10;
|
||||||
@apply after:w-0.5;
|
@apply after:w-0.5;
|
||||||
@apply after:content-DEFAULT;
|
@apply after:content-[''];
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@apply after:bg-accentLight;
|
@apply after:bg-accentLight;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{{ historyEntry.request.url }}
|
{{ historyEntry.request.url }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
|
||||||
>
|
>
|
||||||
{{ historyEntry.request.query.split("\n")[0] }}
|
{{ historyEntry.request.query.split("\n")[0] }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<span class="flex flex-1 space-x-2 items-center">
|
<span class="flex flex-1 items-center space-x-2">
|
||||||
<template v-for="(folder, index) in pathFolders" :key="index">
|
<template v-for="(folder, index) in pathFolders" :key="index">
|
||||||
<span class="block" :class="{ truncate: index !== 0 }">
|
<span class="block" :class="{ truncate: index !== 0 }">
|
||||||
{{ folder.name }}
|
{{ folder.name }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
||||||
<span
|
<span
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
|
||||||
:class="entryStatus.className"
|
:class="entryStatus.className"
|
||||||
>
|
>
|
||||||
{{ historyEntry.request.method }}
|
{{ historyEntry.request.method }}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</template>
|
</template>
|
||||||
<span
|
<span
|
||||||
v-if="request"
|
v-if="request"
|
||||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
|
||||||
:class="getMethodLabelColorClassOf(request)"
|
:style="{ color: getMethodLabelColorClassOf(request) }"
|
||||||
>
|
>
|
||||||
{{ request.method.toUpperCase() }}
|
{{ request.method.toUpperCase() }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
@close="emit('hide-modal')"
|
@close="emit('hide-modal')"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-col border-b transition border-divider">
|
<div class="flex flex-col border-b border-divider transition">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<input
|
<input
|
||||||
id="command"
|
id="command"
|
||||||
@@ -16,14 +16,14 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
name="command"
|
name="command"
|
||||||
:placeholder="`${t('app.type_a_command_search')}`"
|
:placeholder="`${t('app.type_a_command_search')}`"
|
||||||
class="flex flex-1 text-base bg-transparent text-secondaryDark px-6 py-5"
|
class="flex flex-1 bg-transparent px-6 pt-5 pb-3 text-base text-secondaryDark"
|
||||||
/>
|
/>
|
||||||
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="searchSession && search.length > 0"
|
v-if="searchSession && search.length > 0"
|
||||||
class="flex flex-col flex-1 overflow-y-auto border-b border-divider divide-y divide-dividerLight"
|
class="flex flex-1 flex-col divide-y divide-dividerLight overflow-y-auto border-b border-divider"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
|
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
>
|
>
|
||||||
<h5
|
<h5
|
||||||
class="px-6 py-2 bg-primaryContrast z-10 text-secondaryLight sticky top-0"
|
class="sticky top-0 z-10 bg-primaryContrast px-6 py-2 text-secondaryLight"
|
||||||
>
|
>
|
||||||
{{ sectionResult.title }}
|
{{ sectionResult.title }}
|
||||||
</h5>
|
</h5>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
:text="`${t('state.nothing_found')} ‟${search}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="t('action.clear')"
|
:label="t('action.clear')"
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-shrink-0 text-tiny text-secondaryLight p-4 justify-between whitespace-nowrap overflow-auto <sm:hidden"
|
class="flex flex-shrink-0 justify-between overflow-auto whitespace-nowrap p-4 text-tiny text-secondaryLight <sm:hidden"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<kbd class="shortcut-key">↑</kbd>
|
<kbd class="shortcut-key">↑</kbd>
|
||||||
|
|||||||
@@ -12,16 +12,16 @@
|
|||||||
@dragleave="ordering = false"
|
@dragleave="ordering = false"
|
||||||
@dragend="resetDragState"
|
@dragend="resetDragState"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex flex-col relative">
|
<div class="relative flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="absolute bg-accent opacity-0 pointer-events-none inset-0 z-1 transition"
|
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-25':
|
'opacity-25':
|
||||||
dragging && notSameDestination && notSameParentDestination,
|
dragging && notSameDestination && notSameParentDestination,
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group relative z-3 cursor-pointer pointer-events-auto"
|
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch"
|
||||||
:draggable="!hasNoTeamAccess"
|
:draggable="!hasNoTeamAccess"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@drop="handelDrop($event)"
|
@drop="handelDrop($event)"
|
||||||
@@ -36,11 +36,11 @@
|
|||||||
@contextmenu.prevent="options?.tippy.show()"
|
@contextmenu.prevent="options?.tippy.show()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center flex-1 min-w-0"
|
class="flex min-w-0 flex-1 items-center justify-center"
|
||||||
@click="emit('toggle-children')"
|
@click="emit('toggle-children')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center px-4 pointer-events-none"
|
class="pointer-events-none flex items-center justify-center px-4"
|
||||||
>
|
>
|
||||||
<HoppSmartSpinner v-if="isCollLoading" />
|
<HoppSmartSpinner v-if="isCollLoading" />
|
||||||
<component
|
<component
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
class="pointer-events-none flex min-w-0 flex-1 py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
{{ collectionName }}
|
{{ collectionName }}
|
||||||
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
|
|||||||
if (props.isSelected) return IconCheckCircle
|
if (props.isSelected) return IconCheckCircle
|
||||||
else if (!props.isOpen) return IconFolder
|
else if (!props.isOpen) return IconFolder
|
||||||
else if (props.isOpen) return IconFolderOpen
|
else if (props.isOpen) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const collectionName = computed(() => {
|
const collectionName = computed(() => {
|
||||||
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||||
return (props.data as HoppCollection<HoppRESTRequest>).name
|
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||||
else return (props.data as TeamCollection).title
|
return (props.data as TeamCollection).title
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -424,9 +424,8 @@ const isCollLoading = computed(() => {
|
|||||||
props.data.id
|
props.data.id
|
||||||
) {
|
) {
|
||||||
return collectionMoveLoading.includes(props.data.id)
|
return collectionMoveLoading.includes(props.data.id)
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetDragState = () => {
|
const resetDragState = () => {
|
||||||
|
|||||||
@@ -1,361 +1,568 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="modal.collections"
|
||||||
:title="t('modal.collections')"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="importerType !== null"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.go_back')"
|
|
||||||
:icon="IconArrowLeft"
|
|
||||||
@click="resetImport"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="importerType !== null" class="flex flex-col">
|
|
||||||
<div class="flex flex-col pb-4">
|
|
||||||
<div
|
|
||||||
v-for="(step, index) in importerSteps"
|
|
||||||
:key="`step-${index}`"
|
|
||||||
class="flex flex-col space-y-8"
|
|
||||||
>
|
|
||||||
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
|
|
||||||
<p class="flex items-center">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
|
||||||
:class="{
|
|
||||||
'!text-green-500': hasFile,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<icon-lucide-check-circle class="svg-icons" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ t(`${step.metadata.caption}`) }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="inputChooseFileToImportFrom"
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
name="inputChooseFileToImportFrom"
|
|
||||||
type="file"
|
|
||||||
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
|
||||||
:accept="step.metadata.acceptedFileTypes"
|
|
||||||
@change="onFileChange"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
|
|
||||||
<p class="flex items-center">
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
|
||||||
:class="{
|
|
||||||
'!text-green-500': hasGist,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<icon-lucide-check-circle class="svg-icons" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ t(`${step.metadata.caption}`) }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="flex flex-col ml-10">
|
|
||||||
<input
|
|
||||||
v-model="inputChooseGistToImportFrom"
|
|
||||||
type="url"
|
|
||||||
class="input"
|
|
||||||
:placeholder="`${t('import.gist_url')}`"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
|
|
||||||
class="flex flex-col"
|
|
||||||
>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select
|
|
||||||
v-model="mySelectedCollectionID"
|
|
||||||
autocomplete="off"
|
|
||||||
class="select"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<option :key="undefined" :value="undefined" disabled selected>
|
|
||||||
{{ t("collection.select") }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="(collection, collectionIndex) in myCollections"
|
|
||||||
:key="`collection-${collectionIndex}`"
|
|
||||||
:value="collectionIndex"
|
|
||||||
class="bg-primary"
|
|
||||||
>
|
|
||||||
{{ collection.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HoppButtonPrimary
|
|
||||||
:label="t('import.title')"
|
|
||||||
:disabled="enableImportButton"
|
|
||||||
:loading="importingMyCollections"
|
|
||||||
@click="finishImport"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col">
|
|
||||||
<HoppSmartExpand>
|
|
||||||
<template #body>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-for="(importer, index) in importerModules"
|
|
||||||
:key="`importer-${index}`"
|
|
||||||
:icon="importer.icon"
|
|
||||||
:label="t(`${importer.name}`)"
|
|
||||||
@click="importerType = index"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartExpand>
|
|
||||||
<hr />
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:loading="exportingTeamCollections"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="emit('export-json-collection')"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="platform.platformFeatureFlags.exportAsGIST"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
class="flex"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:loading="creatingGistCollection"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="emit('create-collection-gist')"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
|
||||||
import { computed, PropType, ref, watch } from "vue"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
import IconFile from "~icons/lucide/file"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
import {
|
||||||
|
hoppRESTImporter,
|
||||||
|
hoppInsomniaImporter,
|
||||||
|
hoppPostmanImporter,
|
||||||
|
toTeamsImporter,
|
||||||
|
hoppOpenAPIImporter,
|
||||||
|
} from "~/helpers/import-export/import/importers"
|
||||||
|
|
||||||
|
import { defineStep } from "~/composables/step-components"
|
||||||
|
import { PropType, computed, ref } from "vue"
|
||||||
|
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { HoppCollection } from "@hoppscotch/data"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
|
||||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconOpenAPI from "~icons/lucide/file"
|
||||||
|
import IconPostman from "~icons/hopp/postman"
|
||||||
|
import IconInsomnia from "~icons/hopp/insomnia"
|
||||||
|
import IconGithub from "~icons/lucide/github"
|
||||||
|
import IconLink from "~icons/lucide/link"
|
||||||
|
|
||||||
|
import IconUser from "~icons/lucide/user"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
|
||||||
|
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||||
|
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
|
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
|
||||||
|
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
|
||||||
|
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
|
||||||
|
|
||||||
|
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||||
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
type CollectionType = "team-collections" | "my-collections"
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
|
type CollectionType =
|
||||||
|
| {
|
||||||
|
type: "team-collections"
|
||||||
|
selectedTeam: SelectedTeam
|
||||||
|
}
|
||||||
|
| { type: "my-collections" }
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
collectionsType: {
|
collectionsType: {
|
||||||
type: String as PropType<CollectionType>,
|
type: Object as PropType<CollectionType>,
|
||||||
default: "my-collections",
|
default: () => ({
|
||||||
|
type: "my-collections",
|
||||||
|
selectedTeam: undefined,
|
||||||
|
}),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
exportingTeamCollections: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
creatingGistCollection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
importingMyCollections: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
(e: "update-team-collections"): void
|
|
||||||
(e: "export-json-collection"): void
|
|
||||||
(e: "create-collection-gist"): void
|
|
||||||
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const hasFile = ref(false)
|
|
||||||
const hasGist = ref(false)
|
|
||||||
|
|
||||||
const importerType = ref<number | null>(null)
|
|
||||||
|
|
||||||
const stepResults = ref<StepReturnValue[]>([])
|
|
||||||
|
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
|
||||||
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
|
||||||
const inputChooseGistToImportFrom = ref<string>("")
|
|
||||||
|
|
||||||
const importerModules = computed(() =>
|
|
||||||
RESTCollectionImporters.filter(
|
|
||||||
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const importerModule = computed(() => {
|
|
||||||
if (importerType.value === null) return null
|
|
||||||
return importerModules.value[importerType.value]
|
|
||||||
})
|
|
||||||
|
|
||||||
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
|
||||||
|
|
||||||
const enableImportButton = computed(
|
|
||||||
() => !(stepResults.value.length === importerSteps.value?.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(mySelectedCollectionID, (newValue) => {
|
|
||||||
if (newValue === undefined) return
|
|
||||||
stepResults.value = []
|
|
||||||
stepResults.value.push(newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(inputChooseGistToImportFrom, (url) => {
|
|
||||||
stepResults.value = []
|
|
||||||
if (url === "") {
|
|
||||||
hasGist.value = false
|
|
||||||
} else {
|
|
||||||
hasGist.value = true
|
|
||||||
stepResults.value.push(inputChooseGistToImportFrom.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const myCollections = useReadonlyStream(restCollections$, [])
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
const importerAction = async (stepResults: StepReturnValue[]) => {
|
const showImportFailedError = () => {
|
||||||
if (!importerModule.value) return
|
toast.error(t("import.failed"))
|
||||||
|
}
|
||||||
|
|
||||||
pipe(
|
const handleImportToStore = async (
|
||||||
await importerModule.value.importer(stepResults as any)(),
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
E.match(
|
) => {
|
||||||
(err) => {
|
const importResult =
|
||||||
failedImport()
|
props.collectionsType.type === "my-collections"
|
||||||
console.error("error", err)
|
? await importToPersonalWorkspace(collections)
|
||||||
},
|
: await importToTeamsWorkspace(collections)
|
||||||
(result) => {
|
|
||||||
if (props.collectionsType === "team-collections") {
|
|
||||||
emit("import-to-teams", result)
|
|
||||||
} else {
|
|
||||||
appendRESTCollections(result)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
if (E.isRight(importResult)) {
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
toast.success(t("state.file_imported"))
|
||||||
importer: importerModule.value!.name,
|
emit("hide-modal")
|
||||||
platform: "rest",
|
} else {
|
||||||
workspaceType: "personal",
|
toast.error(t("import.failed"))
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileImported()
|
const importToPersonalWorkspace = (
|
||||||
}
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
|
) => {
|
||||||
|
appendRESTCollections(collections)
|
||||||
|
return E.right({
|
||||||
|
success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const importToTeamsWorkspace = async (
|
||||||
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
|
) => {
|
||||||
|
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
|
||||||
|
return E.left({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await toTeamsImporter(
|
||||||
|
JSON.stringify(collections),
|
||||||
|
selectedTeamID.value
|
||||||
|
)()
|
||||||
|
|
||||||
|
return E.isRight(res)
|
||||||
|
? E.right({ success: true })
|
||||||
|
: E.left({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isHoppMyCollectionExporterInProgress = ref(false)
|
||||||
|
const isHoppTeamCollectionExporterInProgress = ref(false)
|
||||||
|
const isHoppGistCollectionExporterInProgress = ref(false)
|
||||||
|
|
||||||
|
const isTeamWorkspace = computed(() => {
|
||||||
|
return props.collectionsType.type === "team-collections"
|
||||||
|
})
|
||||||
|
|
||||||
|
const HoppRESTImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_rest",
|
||||||
|
name: "import.from_json",
|
||||||
|
title: "import.from_json_description",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppRESTImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_json",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppMyCollectionImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_my_collection",
|
||||||
|
name: "import.from_my_collections",
|
||||||
|
title: "import.from_my_collections_description",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["team-workspace"],
|
||||||
|
},
|
||||||
|
component: defineStep("my_collection_import", MyCollectionImport, () => ({
|
||||||
|
async onImportFromMyCollection(content) {
|
||||||
|
handleImportToStore([content])
|
||||||
|
|
||||||
|
// our analytics consider this as an export event, so keeping compatibility with that
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "import_to_teams",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppOpenAPIImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_openapi",
|
||||||
|
name: "import.from_openapi",
|
||||||
|
title: "import.from_openapi_description",
|
||||||
|
icon: IconOpenAPI,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
supported_sources: [
|
||||||
|
{
|
||||||
|
id: "file_import",
|
||||||
|
name: "import.from_file",
|
||||||
|
icon: IconFile,
|
||||||
|
step: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json, .yaml, .yml",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppOpenAPIImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_openapi",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "url_import",
|
||||||
|
name: "import.from_url",
|
||||||
|
icon: IconLink,
|
||||||
|
step: UrlSource({
|
||||||
|
caption: "import.from_url",
|
||||||
|
onImportFromURL: async (content) => {
|
||||||
|
const res = await hoppOpenAPIImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_openapi",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppPostmanImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_postman",
|
||||||
|
name: "import.from_postman",
|
||||||
|
title: "import.from_postman_description",
|
||||||
|
icon: IconPostman,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppPostmanImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_postman",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppInsomniaImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_insomnia",
|
||||||
|
name: "import.from_insomnia",
|
||||||
|
title: "import.from_insomnia_description",
|
||||||
|
icon: IconInsomnia,
|
||||||
|
disabled: true,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppInsomniaImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_insomnia",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppGistImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_gist",
|
||||||
|
name: "import.from_gist",
|
||||||
|
title: "import.from_gist_description",
|
||||||
|
icon: IconGithub,
|
||||||
|
disabled: true,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.from_url",
|
||||||
|
onImportFromGist: async (content) => {
|
||||||
|
if (E.isLeft(content)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppRESTImporter(content.right)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_gist",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppMyCollectionsExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_my_collections",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
isLoading: isHoppMyCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!myCollections.value.length) {
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppMyCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
myCollectionsExporter(myCollections.value),
|
||||||
|
"Collections"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
if (E.isRight(message)) {
|
||||||
|
toast.success(t(message.right))
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "json",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppMyCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishImport = async () => {
|
const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
||||||
await importerAction(stepResults.value)
|
metadata: {
|
||||||
|
id: "hopp_team_collections",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "export.as_json_description",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["team-workspace"],
|
||||||
|
isLoading: isHoppTeamCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.collectionsType.type === "team-collections" &&
|
||||||
|
props.collectionsType.selectedTeam
|
||||||
|
) {
|
||||||
|
const res = await teamCollectionsExporter(
|
||||||
|
props.collectionsType.selectedTeam.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
const { exportCollectionsToJSON } = res.right
|
||||||
|
|
||||||
|
if (!JSON.parse(exportCollectionsToJSON).length) {
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = false
|
||||||
|
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDownloadCollection(
|
||||||
|
exportCollectionsToJSON,
|
||||||
|
"team-collections"
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "json",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(res.left.error.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFileChange = () => {
|
const HoppGistCollectionsExporter: ImporterOrExporter = {
|
||||||
stepResults.value = []
|
metadata: {
|
||||||
|
id: "create_secret_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
|
icon: IconGithub,
|
||||||
|
disabled: !currentUser.value
|
||||||
|
? true
|
||||||
|
: currentUser.value.provider !== "github.com",
|
||||||
|
title: t("export.create_secret_gist"),
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
isLoading: isHoppGistCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
isHoppGistCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
const inputFileToImport = inputChooseFileToImportFrom.value[0]
|
const collectionJSON = await getCollectionJSON()
|
||||||
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
if (!inputFileToImport) {
|
if (!accessToken) {
|
||||||
hasFile.value = false
|
toast.error(t("error.something_went_wrong"))
|
||||||
return
|
isHoppGistCollectionExporterInProgress.value = false
|
||||||
}
|
|
||||||
|
|
||||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
|
||||||
inputChooseFileToImportFrom.value[0].value = ""
|
|
||||||
hasFile.value = false
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
if (!content) {
|
|
||||||
hasFile.value = false
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stepResults.value.push(content)
|
if (E.isRight(collectionJSON)) {
|
||||||
hasFile.value = !!content?.length
|
collectionsGistExporter(collectionJSON.right, accessToken)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "gist",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppGistCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const importerModules = computed(() => {
|
||||||
|
const enabledImporters = [
|
||||||
|
HoppRESTImporter,
|
||||||
|
HoppMyCollectionImporter,
|
||||||
|
HoppOpenAPIImporter,
|
||||||
|
HoppPostmanImporter,
|
||||||
|
HoppInsomniaImporter,
|
||||||
|
HoppGistImporter,
|
||||||
|
]
|
||||||
|
|
||||||
|
const isTeams = props.collectionsType.type === "team-collections"
|
||||||
|
|
||||||
|
return enabledImporters.filter((importer) => {
|
||||||
|
return isTeams
|
||||||
|
? importer.metadata.applicableTo.includes("team-workspace")
|
||||||
|
: importer.metadata.applicableTo.includes("personal-workspace")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const enabledExporters = [
|
||||||
|
HoppMyCollectionsExporter,
|
||||||
|
HoppTeamCollectionsExporter,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
enabledExporters.push(HoppGistCollectionsExporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsText(inputFileToImport.files[0])
|
return enabledExporters.filter((exporter) => {
|
||||||
}
|
return exporter.metadata.applicableTo.includes(
|
||||||
|
props.collectionsType.type === "my-collections"
|
||||||
|
? "personal-workspace"
|
||||||
|
: "team-workspace"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const fileImported = () => {
|
const hasTeamWriteAccess = computed(() => {
|
||||||
toast.success(t("state.file_imported").toString())
|
const { collectionsType } = props
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
const failedImport = () => {
|
|
||||||
toast.error(t("import.failed").toString())
|
|
||||||
}
|
|
||||||
const hideModal = () => {
|
|
||||||
resetImport()
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetImport = () => {
|
const isTeamCollection = collectionsType.type === "team-collections"
|
||||||
importerType.value = null
|
|
||||||
hasFile.value = false
|
if (!isTeamCollection || !collectionsType.selectedTeam) {
|
||||||
hasGist.value = false
|
return false
|
||||||
stepResults.value = []
|
}
|
||||||
inputChooseFileToImportFrom.value = ""
|
|
||||||
inputChooseGistToImportFrom.value = ""
|
return (
|
||||||
mySelectedCollectionID.value = undefined
|
collectionsType.selectedTeam.myRole === "EDITOR" ||
|
||||||
|
collectionsType.selectedTeam.myRole === "OWNER"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedTeamID = computed(() => {
|
||||||
|
const { collectionsType } = props
|
||||||
|
|
||||||
|
return collectionsType.type === "team-collections"
|
||||||
|
? collectionsType.selectedTeam?.id
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const myCollections = useReadonlyStream(restCollections$, [])
|
||||||
|
|
||||||
|
const getCollectionJSON = async () => {
|
||||||
|
if (
|
||||||
|
props.collectionsType.type === "team-collections" &&
|
||||||
|
props.collectionsType.selectedTeam?.id
|
||||||
|
) {
|
||||||
|
const res = await getTeamCollectionJSON(
|
||||||
|
props.collectionsType.selectedTeam?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return E.isRight(res)
|
||||||
|
? E.right(res.right.exportCollectionsToJSON)
|
||||||
|
: E.left(res.left)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.collectionsType.type === "my-collections") {
|
||||||
|
return E.right(JSON.stringify(myCollections.value, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-1 flex-col">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary"
|
||||||
:style="
|
:style="
|
||||||
saveRequest
|
saveRequest
|
||||||
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-1 flex-col">
|
||||||
<HoppSmartTree :adapter="myAdapter">
|
<HoppSmartTree :adapter="myAdapter">
|
||||||
<template
|
<template
|
||||||
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
#content="{ node, toggleChildren, isOpen, highlightChildren }"
|
||||||
@@ -222,6 +222,12 @@
|
|||||||
requestIndex: pathToIndex(node.id),
|
requestIndex: pathToIndex(node.id),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@share-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('share-request', {
|
||||||
|
request: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
@drag-request="
|
@drag-request="
|
||||||
dragRequest($event, {
|
dragRequest($event, {
|
||||||
folderPath: node.data.data.parentIndex,
|
folderPath: node.data.data.parentIndex,
|
||||||
@@ -248,7 +254,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -258,10 +264,10 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<span class="text-secondaryLight text-center">
|
<span class="text-center text-secondaryLight">
|
||||||
{{ t("collection.import_or_create") }}
|
{{ t("collection.import_or_create") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 flex-col items-stretch">
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:icon="IconImport"
|
:icon="IconImport"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
@@ -460,6 +466,12 @@ const emit = defineEmits<{
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "share-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "drop-request",
|
event: "drop-request",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -526,13 +538,12 @@ const isSelected = ({
|
|||||||
props.picked.folderPath === folderPath &&
|
props.picked.folderPath === folderPath &&
|
||||||
props.picked.requestIndex === requestIndex
|
props.picked.requestIndex === requestIndex
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "my-folder" &&
|
|
||||||
props.picked.folderPath === folderPath
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "my-folder" &&
|
||||||
|
props.picked.folderPath === folderPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = useService(RESTTabService)
|
const tabs = useService(RESTTabService)
|
||||||
@@ -729,11 +740,10 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
|||||||
status: "loaded",
|
status: "loaded",
|
||||||
data: data,
|
data: data,
|
||||||
} as ChildrenResult<Folder | Requests>
|
} as ChildrenResult<Folder | Requests>
|
||||||
} else {
|
}
|
||||||
return {
|
return {
|
||||||
status: "loaded",
|
status: "loaded",
|
||||||
data: [],
|
data: [],
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
@dragend="resetDragState"
|
@dragend="resetDragState"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
:draggable="!hasNoTeamAccess"
|
:draggable="!hasNoTeamAccess"
|
||||||
@drop="handelDrop"
|
@drop="handelDrop"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@@ -23,12 +23,12 @@
|
|||||||
@contextmenu.prevent="options?.tippy.show()"
|
@contextmenu.prevent="options?.tippy.show()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center flex-1 min-w-0 cursor-pointer pointer-events-auto"
|
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
|
||||||
:class="requestLabelColor"
|
:style="{ color: getMethodLabelColorClassOf(request) }"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="IconCheckCircle"
|
:is="IconCheckCircle"
|
||||||
@@ -37,12 +37,12 @@
|
|||||||
:class="{ 'text-accent': isSelected }"
|
:class="{ 'text-accent': isSelected }"
|
||||||
/>
|
/>
|
||||||
<HoppSmartSpinner v-else-if="isRequestLoading" />
|
<HoppSmartSpinner v-else-if="isRequestLoading" />
|
||||||
<span v-else class="font-semibold truncate text-tiny">
|
<span v-else class="truncate text-tiny font-semibold">
|
||||||
{{ request.method }}
|
{{ request.method }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 pointer-events-none transition group-hover:text-secondaryDark"
|
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
{{ request.name }}
|
{{ request.name }}
|
||||||
@@ -50,15 +50,15 @@
|
|||||||
<span
|
<span
|
||||||
v-if="isActive"
|
v-if="isActive"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
|
||||||
:title="`${t('collection.request_in_use')}`"
|
:title="`${t('collection.request_in_use')}`"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -93,6 +93,7 @@
|
|||||||
@keyup.e="edit?.$el.click()"
|
@keyup.e="edit?.$el.click()"
|
||||||
@keyup.d="duplicate?.$el.click()"
|
@keyup.d="duplicate?.$el.click()"
|
||||||
@keyup.delete="deleteAction?.$el.click()"
|
@keyup.delete="deleteAction?.$el.click()"
|
||||||
|
@keyup.s="shareAction?.$el.click()"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
@@ -132,6 +133,18 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="shareAction"
|
||||||
|
:icon="IconShare2"
|
||||||
|
:label="t('action.share')"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('share-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
@@ -161,6 +174,7 @@ import IconEdit from "~icons/lucide/edit"
|
|||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { ref, PropType, watch, computed } from "vue"
|
import { ref, PropType, watch, computed } from "vue"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
@@ -239,6 +253,7 @@ const emit = defineEmits<{
|
|||||||
(event: "duplicate-request"): void
|
(event: "duplicate-request"): void
|
||||||
(event: "remove-request"): void
|
(event: "remove-request"): void
|
||||||
(event: "select-request"): void
|
(event: "select-request"): void
|
||||||
|
(event: "share-request"): void
|
||||||
(event: "drag-request", payload: DataTransfer): void
|
(event: "drag-request", payload: DataTransfer): void
|
||||||
(event: "update-request-order", payload: DataTransfer): void
|
(event: "update-request-order", payload: DataTransfer): void
|
||||||
(event: "update-last-request-order", payload: DataTransfer): void
|
(event: "update-last-request-order", payload: DataTransfer): void
|
||||||
@@ -249,6 +264,7 @@ const edit = ref<HTMLButtonElement | null>(null)
|
|||||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
const options = ref<TippyComponent | null>(null)
|
const options = ref<TippyComponent | null>(null)
|
||||||
const duplicate = ref<HTMLButtonElement | null>(null)
|
const duplicate = ref<HTMLButtonElement | null>(null)
|
||||||
|
const shareAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
const ordering = ref(false)
|
const ordering = ref(false)
|
||||||
@@ -260,10 +276,6 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
parentID: "",
|
parentID: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestLabelColor = computed(() =>
|
|
||||||
getMethodLabelColorClassOf(props.request)
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.duplicateLoading,
|
() => props.duplicateLoading,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -362,9 +374,8 @@ const updateLastItemOrder = (e: DragEvent) => {
|
|||||||
const isRequestLoading = computed(() => {
|
const isRequestLoading = computed(() => {
|
||||||
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
||||||
return props.requestMoveLoading.includes(props.requestID)
|
return props.requestMoveLoading.includes(props.requestID)
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetDragState = () => {
|
const resetDragState = () => {
|
||||||
|
|||||||
@@ -141,9 +141,8 @@ const reqName = computed(() => {
|
|||||||
return props.request.name
|
return props.request.name
|
||||||
} else if (props.mode === "rest") {
|
} else if (props.mode === "rest") {
|
||||||
return restRequestName.value
|
return restRequestName.value
|
||||||
} else {
|
|
||||||
return gqlRequestName.value
|
|
||||||
}
|
}
|
||||||
|
return gqlRequestName.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestName = ref(reqName.value)
|
const requestName = ref(reqName.value)
|
||||||
@@ -480,21 +479,20 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_coll/short_title":
|
case "team_coll/short_title":
|
||||||
return t("collection.name_length_insufficient")
|
return t("collection.name_length_insufficient")
|
||||||
case "team/invalid_coll_id":
|
case "team/invalid_coll_id":
|
||||||
return t("team.invalid_id")
|
return t("team.invalid_id")
|
||||||
case "team/not_required_role":
|
case "team/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_required_role":
|
case "team_req/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-1 flex-col">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 border-b bg-primary border-dividerLight"
|
class="sticky z-10 flex flex-1 justify-between border-b border-dividerLight bg-primary"
|
||||||
:style="
|
:style="
|
||||||
saveRequest
|
saveRequest
|
||||||
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
? 'top: calc(var(--upper-primary-sticky-fold) - var(--line-height-body))'
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
class="!rounded-none"
|
class="!rounded-none"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
:title="t('team.no_access')"
|
:title="t('team.no_access')"
|
||||||
:label="t('add.new')"
|
:label="t('action.new')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-else
|
v-else
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
:label="t('add.new')"
|
:label="t('action.new')"
|
||||||
class="!rounded-none"
|
class="!rounded-none"
|
||||||
@click="emit('display-modal-add')"
|
@click="emit('display-modal-add')"
|
||||||
/>
|
/>
|
||||||
@@ -240,6 +240,12 @@
|
|||||||
requestIndex: node.data.data.data.id,
|
requestIndex: node.data.data.data.id,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@share-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('share-request', {
|
||||||
|
request: node.data.data.data.request,
|
||||||
|
})
|
||||||
|
"
|
||||||
@drag-request="
|
@drag-request="
|
||||||
dragRequest($event, {
|
dragRequest($event, {
|
||||||
folderPath: node.data.data.parentIndex,
|
folderPath: node.data.data.parentIndex,
|
||||||
@@ -269,10 +275,10 @@
|
|||||||
@drop.stop
|
@drop.stop
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<span class="text-secondaryLight text-center">
|
<span class="text-center text-secondaryLight">
|
||||||
{{ t("collection.import_or_create") }}
|
{{ t("collection.import_or_create") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 flex-col items-stretch">
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:icon="IconImport"
|
:icon="IconImport"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
@@ -473,6 +479,12 @@ const emit = defineEmits<{
|
|||||||
folderPath?: string | undefined
|
folderPath?: string | undefined
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "share-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "drop-request",
|
event: "drop-request",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -542,13 +554,12 @@ const isSelected = ({
|
|||||||
props.picked.pickedType === "teams-request" &&
|
props.picked.pickedType === "teams-request" &&
|
||||||
props.picked.requestID === requestID
|
props.picked.requestID === requestID
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "teams-folder" &&
|
|
||||||
props.picked.folderID === folderID
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "teams-folder" &&
|
||||||
|
props.picked.folderID === folderID
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||||
@@ -714,82 +725,78 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
|||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
const data = this.data.value.map((item, index) => ({
|
const data = this.data.value.map((item, index) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
data: {
|
||||||
|
isLastItem: index === this.data.value.length - 1,
|
||||||
|
type: "collections",
|
||||||
data: {
|
data: {
|
||||||
isLastItem: index === this.data.value.length - 1,
|
parentIndex: null,
|
||||||
type: "collections",
|
data: item,
|
||||||
data: {
|
|
||||||
parentIndex: null,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
},
|
||||||
return {
|
}))
|
||||||
status: "loaded",
|
return {
|
||||||
data: cloneDeep(data),
|
status: "loaded",
|
||||||
} as ChildrenResult<TeamCollections>
|
data: cloneDeep(data),
|
||||||
}
|
} as ChildrenResult<TeamCollections>
|
||||||
} else {
|
}
|
||||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||||
|
|
||||||
!props.teamLoadingCollections.includes(parsedID) &&
|
!props.teamLoadingCollections.includes(parsedID) &&
|
||||||
emit("expand-team-collection", parsedID)
|
emit("expand-team-collection", parsedID)
|
||||||
|
|
||||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const items = this.findCollInTree(this.data.value, parsedID)
|
|
||||||
if (items) {
|
|
||||||
const data = [
|
|
||||||
...(items.children
|
|
||||||
? items.children.map((item, index) => ({
|
|
||||||
id: `${id}/${item.id}`,
|
|
||||||
data: {
|
|
||||||
isLastItem:
|
|
||||||
items.children && items.children.length > 1
|
|
||||||
? index === items.children.length - 1
|
|
||||||
: false,
|
|
||||||
type: "folders",
|
|
||||||
data: {
|
|
||||||
parentIndex: parsedID,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
...(items.requests
|
|
||||||
? items.requests.map((item, index) => ({
|
|
||||||
id: `${id}/${item.id}`,
|
|
||||||
data: {
|
|
||||||
isLastItem:
|
|
||||||
items.requests && items.requests.length > 1
|
|
||||||
? index === items.requests.length - 1
|
|
||||||
: false,
|
|
||||||
type: "requests",
|
|
||||||
data: {
|
|
||||||
parentIndex: parsedID,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
status: "loaded",
|
|
||||||
data: cloneDeep(data),
|
|
||||||
} as ChildrenResult<TeamFolder | TeamRequests>
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: "loaded",
|
|
||||||
data: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const items = this.findCollInTree(this.data.value, parsedID)
|
||||||
|
if (items) {
|
||||||
|
const data = [
|
||||||
|
...(items.children
|
||||||
|
? items.children.map((item, index) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
isLastItem:
|
||||||
|
items.children && items.children.length > 1
|
||||||
|
? index === items.children.length - 1
|
||||||
|
: false,
|
||||||
|
type: "folders",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
...(items.requests
|
||||||
|
? items.requests.map((item, index) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
isLastItem:
|
||||||
|
items.requests && items.requests.length > 1
|
||||||
|
? index === items.requests.length - 1
|
||||||
|
: false,
|
||||||
|
type: "requests",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: cloneDeep(data),
|
||||||
|
} as ChildrenResult<TeamFolder | TeamRequests>
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="dropEvent"
|
@drop.prevent="dropEvent"
|
||||||
@dragover="dragging = true"
|
@dragover="dragging = true"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
@contextmenu.prevent="options.tippy.show()"
|
@contextmenu.prevent="options.tippy.show()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex cursor-pointer items-center justify-center px-4"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
@@ -136,10 +136,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
<div v-if="showChildren || isFiltered" class="flex">
|
||||||
<div
|
<div
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
|
class="ml-[1.375rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex flex-col flex-1 truncate">
|
<div class="flex flex-1 flex-col truncate">
|
||||||
<CollectionsGraphqlFolder
|
<CollectionsGraphqlFolder
|
||||||
v-for="(folder, index) in collection.folders"
|
v-for="(folder, index) in collection.folders"
|
||||||
:key="`folder-${String(index)}`"
|
:key="`folder-${String(index)}`"
|
||||||
@@ -271,7 +271,7 @@ const collectionIcon = computed(() => {
|
|||||||
if (isSelected.value) return IconCheckCircle
|
if (isSelected.value) return IconCheckCircle
|
||||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||||
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
|
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
@dragover.prevent
|
@dragover.prevent
|
||||||
@drop.prevent="dropEvent"
|
@drop.prevent="dropEvent"
|
||||||
@dragover="dragging = true"
|
@dragover="dragging = true"
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
@contextmenu.prevent="options.tippy.show()"
|
@contextmenu.prevent="options.tippy.show()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex cursor-pointer items-center justify-center px-4"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
@@ -128,10 +128,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="showChildren || isFiltered" class="flex">
|
<div v-if="showChildren || isFiltered" class="flex">
|
||||||
<div
|
<div
|
||||||
class="bg-dividerLight cursor-nsResize flex ml-5.5 transform transition w-0.5 hover:bg-dividerDark hover:scale-x-125"
|
class="ml-[1.375rem] flex w-0.5 transform cursor-nsResize bg-dividerLight transition hover:scale-x-125 hover:bg-dividerDark"
|
||||||
@click="toggleShowChildren()"
|
@click="toggleShowChildren()"
|
||||||
></div>
|
></div>
|
||||||
<div class="flex flex-col flex-1 truncate">
|
<div class="flex flex-1 flex-col truncate">
|
||||||
<!-- Referring to this component only (this is recursive) -->
|
<!-- Referring to this component only (this is recursive) -->
|
||||||
<Folder
|
<Folder
|
||||||
v-for="(subFolder, subFolderIndex) in folder.folders"
|
v-for="(subFolder, subFolderIndex) in folder.folders"
|
||||||
@@ -253,7 +253,7 @@ const collectionIcon = computed(() => {
|
|||||||
if (isSelected.value) return IconCheckCircle
|
if (isSelected.value) return IconCheckCircle
|
||||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||||
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
|
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
|
|||||||
@@ -1,299 +1,227 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="graphql_collections.title"
|
||||||
:title="`${t('modal.collections')}`"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<span>
|
|
||||||
<tippy interactive trigger="click" theme="popover">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.more')"
|
|
||||||
:icon="IconMoreVertical"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('import.from_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
readCollectionGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
createCollectionGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('import.from_json')"
|
|
||||||
@click="openDialogChooseFileToImportFrom"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
class="input"
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
@change="importFromJSON"
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="exportJSON"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
|
||||||
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
|
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||||
|
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||||
|
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
import IconDownload from "~icons/lucide/download"
|
import IconUser from "~icons/lucide/user"
|
||||||
import IconGithub from "~icons/lucide/github"
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
import { computed, ref } from "vue"
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import {
|
import {
|
||||||
graphqlCollections$,
|
graphqlCollections$,
|
||||||
setGraphqlCollections,
|
setGraphqlCollections,
|
||||||
appendGraphqlCollections,
|
|
||||||
} from "~/newstore/collections"
|
} from "~/newstore/collections"
|
||||||
|
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
|
||||||
|
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
|
||||||
|
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
|
||||||
|
import { computed } from "vue"
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
show: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
const toast = useToast()
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template refs
|
const GqlCollectionsHoppImporter: ImporterOrExporter = {
|
||||||
const tippyActions = ref<any | null>(null)
|
metadata: {
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
id: "import.from_json",
|
||||||
|
name: "import.from_json",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.from_json_description",
|
||||||
|
onImportFromFile: async (gqlCollections) => {
|
||||||
|
const res = await hoppGqlCollectionsImporter(gqlCollections)
|
||||||
|
|
||||||
const collectionJson = computed(() => {
|
if (E.isLeft(res)) {
|
||||||
return JSON.stringify(collections.value, null, 2)
|
showImportFailedError()
|
||||||
})
|
return
|
||||||
|
|
||||||
const createCollectionGist = async () => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
toast.error(t("profile.no_permission").toString())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.post(
|
|
||||||
"https://api.github.com/gists",
|
|
||||||
{
|
|
||||||
files: {
|
|
||||||
"hoppscotch-collections.json": {
|
|
||||||
content: collectionJson.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${currentUser.value.accessToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
workspaceType: "personal",
|
||||||
|
importer: "json",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const GqlCollectionsGistImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "import.from_gist",
|
||||||
|
name: "import.from_gist",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_gist",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.gql_collections_from_gist_description",
|
||||||
|
onImportFromGist: async (gqlCollections) => {
|
||||||
|
if (E.isLeft(gqlCollections)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppGqlCollectionsImporter(gqlCollections.right)
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
workspaceType: "personal",
|
||||||
|
importer: "gist",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
|
||||||
|
|
||||||
|
const GqlCollectionsHoppExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_json",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!gqlCollections.value.length) {
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
gqlCollectionsExporter(gqlCollections.value),
|
||||||
|
"GQLCollections"
|
||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
if (E.isLeft(message)) {
|
||||||
window.open(res.data.html_url)
|
toast.error(t("export.failed"))
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileImported = () => {
|
|
||||||
toast.success(t("state.file_imported").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const failedImport = () => {
|
|
||||||
toast.error(t("import.failed").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const readCollectionGist = async () => {
|
|
||||||
const gist = prompt(t("import.gist_url").toString())
|
|
||||||
if (!gist) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { files } = (await axios.get(
|
|
||||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)) as {
|
|
||||||
files: {
|
|
||||||
[fileName: string]: {
|
|
||||||
content: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const collections = JSON.parse(Object.values(files)[0].content)
|
|
||||||
setGraphqlCollections(collections)
|
|
||||||
fileImported()
|
|
||||||
} catch (e) {
|
|
||||||
failedImport()
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDialogChooseFileToImportFrom = () => {
|
|
||||||
if (inputChooseFileToImportFrom.value)
|
|
||||||
inputChooseFileToImportFrom.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importFromJSON = () => {
|
|
||||||
if (!inputChooseFileToImportFrom.value) return
|
|
||||||
|
|
||||||
if (
|
|
||||||
!inputChooseFileToImportFrom.value.files ||
|
|
||||||
inputChooseFileToImportFrom.value.files.length === 0
|
|
||||||
) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collections = JSON.parse(content)
|
toast.success(message.right)
|
||||||
if (collections[0]) {
|
|
||||||
const [name, folders, requests] = Object.keys(collections[0])
|
|
||||||
if (name === "name" && folders === "folders" && requests === "requests") {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
failedImport()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appendGraphqlCollections(collections)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
|
||||||
importer: "json",
|
|
||||||
workspaceType: "personal",
|
|
||||||
platform: "gql",
|
|
||||||
})
|
|
||||||
|
|
||||||
fileImported()
|
|
||||||
}
|
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
|
||||||
inputChooseFileToImportFrom.value.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportJSON = async () => {
|
|
||||||
const dataToWrite = collectionJson.value
|
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(dataToWrite)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
const result = await platform.io.saveFileWithDialog({
|
|
||||||
data: dataToWrite,
|
|
||||||
contentType: "application/json",
|
|
||||||
suggestedFilename: filename,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Hoppscotch Collection JSON file",
|
|
||||||
extensions: ["json"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.type === "unknown" || result.type === "saved") {
|
|
||||||
platform?.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
exporter: "json",
|
|
||||||
platform: "gql",
|
platform: "gql",
|
||||||
|
exporter: "json",
|
||||||
})
|
})
|
||||||
|
},
|
||||||
toast.success(t("state.download_started").toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GqlCollectionsGistExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
|
title: !currentUser
|
||||||
|
? "export.require_github"
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
currentUser.provider !== "github.com"
|
||||||
|
? `export.require_github`
|
||||||
|
: "export.create_secret_gist",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: !currentUser.value
|
||||||
|
? true
|
||||||
|
: currentUser.value.provider !== "github.com",
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
if (!currentUser.value) {
|
||||||
|
toast.error(t("profile.no_permission"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
const res = await gqlCollectionsGistExporter(
|
||||||
|
JSON.stringify(gqlCollections.value),
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
toast.error(t("export.failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t("export.success"))
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
exporter: "gist",
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(res.right, "_blank")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const importerModules = [GqlCollectionsHoppImporter, GqlCollectionsGistImporter]
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const modules = [GqlCollectionsHoppExporter]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
modules.push(GqlCollectionsGistExporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules
|
||||||
|
})
|
||||||
|
|
||||||
|
const showImportFailedError = () => {
|
||||||
|
toast.error(t("import.failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportToStore = async (
|
||||||
|
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||||
|
) => {
|
||||||
|
setGraphqlCollections(gqlCollections)
|
||||||
|
toast.success(t("import.success"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
<div class="flex flex-col" :class="[{ 'bg-primaryLight': dragging }]">
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@dragover.stop
|
@dragover.stop
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
@contextmenu.prevent="options.tippy.show()"
|
@contextmenu.prevent="options.tippy.show()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center w-16 px-2 truncate cursor-pointer"
|
class="flex w-16 cursor-pointer items-center justify-center truncate px-2"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex items-center flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer items-center py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
@click="selectRequest()"
|
@click="selectRequest()"
|
||||||
>
|
>
|
||||||
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
<span class="truncate" :class="{ 'text-accent': isSelected }">
|
||||||
@@ -29,15 +29,15 @@
|
|||||||
<span
|
<span
|
||||||
v-if="isActive"
|
v-if="isActive"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="relative h-1.5 w-1.5 flex flex-shrink-0 mx-3"
|
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
|
||||||
:title="`${t('collection.request_in_use')}`"
|
:title="`${t('collection.request_in_use')}`"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="absolute inline-flex flex-shrink-0 w-full h-full bg-green-500 rounded-full opacity-75 animate-ping"
|
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="relative inline-flex flex-shrink-0 rounded-full h-1.5 w-1.5 bg-green-500"
|
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
|
||||||
></span>
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="{ 'rounded border border-divider': saveRequest }">
|
<div :class="{ 'rounded border border-divider': saveRequest }">
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto rounded-t bg-primary"
|
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto rounded-t bg-primary"
|
||||||
:style="
|
:style="
|
||||||
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
||||||
"
|
"
|
||||||
@@ -11,10 +11,10 @@
|
|||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
class="py-2 pl-4 pr-2 bg-transparent !border-0"
|
class="!border-0 bg-transparent py-2 pl-4 pr-2"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex justify-between flex-1 flex-shrink-0 border-y bg-primary border-dividerLight"
|
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
@@ -67,10 +67,10 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<span class="text-secondaryLight text-center">
|
<span class="text-center text-secondaryLight">
|
||||||
{{ t("collection.import_or_create") }}
|
{{ t("collection.import_or_create") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 flex-col items-stretch">
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:icon="IconImport"
|
:icon="IconImport"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<CollectionsGraphqlAdd
|
<CollectionsGraphqlAdd
|
||||||
@@ -137,7 +137,7 @@
|
|||||||
@hide-modal="displayModalEditRequest(false)"
|
@hide-modal="displayModalEditRequest(false)"
|
||||||
/>
|
/>
|
||||||
<CollectionsGraphqlImportExport
|
<CollectionsGraphqlImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
@dragend="draggingToRoot = false"
|
@dragend="draggingToRoot = false"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex flex-col flex-shrink-0 overflow-x-auto border-b bg-primary border-dividerLight"
|
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary border-b border-dividerLight"
|
||||||
:class="{ 'rounded-t': saveRequest }"
|
:class="{ 'rounded-t': saveRequest }"
|
||||||
:style="
|
:style="
|
||||||
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
v-model="filterTexts"
|
v-model="filterTexts"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="flex w-full p-4 py-2 bg-transparent h-8"
|
class="flex w-full bg-transparent px-4 py-2"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
:disabled="collectionsType.type === 'team-collections'"
|
:disabled="collectionsType.type === 'team-collections'"
|
||||||
/>
|
/>
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@remove-folder="removeFolder"
|
@remove-folder="removeFolder"
|
||||||
|
@share-request="shareRequest"
|
||||||
@drop-collection="dropCollection"
|
@drop-collection="dropCollection"
|
||||||
@update-request-order="updateRequestOrder"
|
@update-request-order="updateRequestOrder"
|
||||||
@update-collection-order="updateCollectionOrder"
|
@update-collection-order="updateCollectionOrder"
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@remove-folder="removeFolder"
|
@remove-folder="removeFolder"
|
||||||
|
@share-request="shareRequest"
|
||||||
@edit-request="editRequest"
|
@edit-request="editRequest"
|
||||||
@duplicate-request="duplicateRequest"
|
@duplicate-request="duplicateRequest"
|
||||||
@remove-request="removeRequest"
|
@remove-request="removeRequest"
|
||||||
@@ -85,12 +87,12 @@
|
|||||||
@display-modal-import-export="displayModalImportExport(true)"
|
@display-modal-import-export="displayModalImportExport(true)"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="hidden bg-primaryDark flex-col flex-1 items-center py-15 justify-center px-4 text-secondaryLight"
|
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
|
||||||
:class="{
|
:class="{
|
||||||
'!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
|
'!flex': draggingToRoot && currentReorderingStatus.type !== 'request',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<icon-lucide-list-end class="svg-icons !w-8 !h-8" />
|
<icon-lucide-list-end class="svg-icons !h-8 !w-8" />
|
||||||
</div>
|
</div>
|
||||||
<CollectionsAdd
|
<CollectionsAdd
|
||||||
:show="showModalAdd"
|
:show="showModalAdd"
|
||||||
@@ -138,17 +140,13 @@
|
|||||||
@hide-modal="showConfirmModal = false"
|
@hide-modal="showConfirmModal = false"
|
||||||
@resolve="resolveConfirmModal"
|
@resolve="resolveConfirmModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CollectionsImportExport
|
<CollectionsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
:collections-type="collectionsType.type"
|
:collections-type="collectionsType"
|
||||||
:exporting-team-collections="exportingTeamCollections"
|
|
||||||
:creating-gist-collection="creatingGistCollection"
|
|
||||||
:importing-my-collections="importingMyCollections"
|
|
||||||
@export-json-collection="exportJSONCollection"
|
|
||||||
@create-collection-gist="createCollectionGist"
|
|
||||||
@import-to-teams="importToTeams"
|
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TeamsAdd
|
<TeamsAdd
|
||||||
:show="showTeamModalAdd"
|
:show="showTeamModalAdd"
|
||||||
@hide-modal="displayTeamModalAdd(false)"
|
@hide-modal="displayTeamModalAdd(false)"
|
||||||
@@ -197,7 +195,6 @@ import {
|
|||||||
createChildCollection,
|
createChildCollection,
|
||||||
renameCollection,
|
renameCollection,
|
||||||
deleteCollection,
|
deleteCollection,
|
||||||
importJSONToTeam,
|
|
||||||
moveRESTTeamCollection,
|
moveRESTTeamCollection,
|
||||||
updateOrderRESTTeamCollection,
|
updateOrderRESTTeamCollection,
|
||||||
} from "~/helpers/backend/mutations/TeamCollection"
|
} from "~/helpers/backend/mutations/TeamCollection"
|
||||||
@@ -212,12 +209,9 @@ import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
|||||||
import { Collection as NodeCollection } from "./MyCollections.vue"
|
import { Collection as NodeCollection } from "./MyCollections.vue"
|
||||||
import {
|
import {
|
||||||
getCompleteCollectionTree,
|
getCompleteCollectionTree,
|
||||||
getTeamCollectionJSON,
|
|
||||||
teamCollToHoppRESTColl,
|
teamCollToHoppRESTColl,
|
||||||
} from "~/helpers/backend/helpers"
|
} from "~/helpers/backend/helpers"
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { createCollectionGists } from "~/helpers/gist"
|
|
||||||
import {
|
import {
|
||||||
getRequestsByPath,
|
getRequestsByPath,
|
||||||
resolveSaveContextOnRequestReorder,
|
resolveSaveContextOnRequestReorder,
|
||||||
@@ -229,7 +223,7 @@ import {
|
|||||||
resetTeamRequestsContext,
|
resetTeamRequestsContext,
|
||||||
} from "~/helpers/collection/collection"
|
} from "~/helpers/collection/collection"
|
||||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
@@ -303,12 +297,6 @@ const draggingToRoot = ref(false)
|
|||||||
const collectionMoveLoading = ref<string[]>([])
|
const collectionMoveLoading = ref<string[]>([])
|
||||||
const requestMoveLoading = ref<string[]>([])
|
const requestMoveLoading = ref<string[]>([])
|
||||||
|
|
||||||
// Export - Import refs
|
|
||||||
const collectionJSON = ref("")
|
|
||||||
const exportingTeamCollections = ref(false)
|
|
||||||
const creatingGistCollection = ref(false)
|
|
||||||
const importingMyCollections = ref(false)
|
|
||||||
|
|
||||||
// TeamList-Adapter
|
// TeamList-Adapter
|
||||||
const workspaceService = useService(WorkspaceService)
|
const workspaceService = useService(WorkspaceService)
|
||||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||||
@@ -412,14 +400,12 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hasTeamWriteAccess = computed(() => {
|
const hasTeamWriteAccess = computed(() => {
|
||||||
if (!collectionsType.value.selectedTeam) return false
|
if (collectionsType.value.type !== "team-collections") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
const role = collectionsType.value.selectedTeam?.myRole
|
||||||
collectionsType.value.type === "team-collections" &&
|
return role === "OWNER" || role === "EDITOR"
|
||||||
collectionsType.value.selectedTeam.myRole !== "VIEWER"
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
else return false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredCollections = computed(() => {
|
const filteredCollections = computed(() => {
|
||||||
@@ -1069,7 +1055,7 @@ const onRemoveCollection = () => {
|
|||||||
const collectionIndex = editingCollectionIndex.value
|
const collectionIndex = editingCollectionIndex.value
|
||||||
|
|
||||||
const collectionToRemove =
|
const collectionToRemove =
|
||||||
collectionIndex || collectionIndex == 0
|
collectionIndex || collectionIndex === 0
|
||||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||||
collectionIndex,
|
collectionIndex,
|
||||||
])
|
])
|
||||||
@@ -1468,9 +1454,8 @@ const checkIfCollectionIsAParentOfTheChildren = (
|
|||||||
)
|
)
|
||||||
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -1491,9 +1476,8 @@ const isMoveToSameLocation = (
|
|||||||
|
|
||||||
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1673,25 +1657,22 @@ const isSameSameParent = (
|
|||||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||||
|
|
||||||
return dragedItemParent.join("/") === destinationCollectionIndex
|
return dragedItemParent.join("/") === destinationCollectionIndex
|
||||||
} else {
|
|
||||||
if (destinationItemPath === null) return false
|
|
||||||
const destinationItemIndex = pathToIndex(destinationItemPath)
|
|
||||||
|
|
||||||
// length of 1 means the request is in the root
|
|
||||||
if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
|
|
||||||
return true
|
|
||||||
} else if (draggedItemIndex.length === destinationItemIndex.length) {
|
|
||||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
|
||||||
const destinationItemParent = destinationItemIndex.slice(0, -1)
|
|
||||||
if (isEqual(dragedItemParent, destinationItemParent)) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (destinationItemPath === null) return false
|
||||||
|
const destinationItemIndex = pathToIndex(destinationItemPath)
|
||||||
|
|
||||||
|
// length of 1 means the request is in the root
|
||||||
|
if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
|
||||||
|
return true
|
||||||
|
} else if (draggedItemIndex.length === destinationItemIndex.length) {
|
||||||
|
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||||
|
const destinationItemParent = destinationItemIndex.slice(0, -1)
|
||||||
|
if (isEqual(dragedItemParent, destinationItemParent)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1833,33 +1814,6 @@ const updateCollectionOrder = (payload: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Import - Export Collection functions
|
// Import - Export Collection functions
|
||||||
/**
|
|
||||||
* Export the whole my collection or specific team collection to JSON
|
|
||||||
*/
|
|
||||||
const getJSONCollection = async () => {
|
|
||||||
if (collectionsType.value.type === "my-collections") {
|
|
||||||
collectionJSON.value = JSON.stringify(myCollections.value, null, 2)
|
|
||||||
} else {
|
|
||||||
if (!collectionsType.value.selectedTeam) return
|
|
||||||
exportingTeamCollections.value = true
|
|
||||||
pipe(
|
|
||||||
await getTeamCollectionJSON(collectionsType.value.selectedTeam.id),
|
|
||||||
E.match(
|
|
||||||
(err) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
exportingTeamCollections.value = false
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
const { exportCollectionsToJSON } = result
|
|
||||||
collectionJSON.value = exportCollectionsToJSON
|
|
||||||
exportingTeamCollections.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return collectionJSON.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a downloadable file from a collection and prompts the user to download it.
|
* Create a downloadable file from a collection and prompts the user to download it.
|
||||||
@@ -1928,88 +1882,15 @@ const exportData = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportJSONCollection = async () => {
|
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
|
||||||
platform.analytics?.logEvent({
|
if (currentUser.value) {
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
// opens the share request modal
|
||||||
exporter: "json",
|
invokeAction("share.request", {
|
||||||
platform: "rest",
|
request,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
await getJSONCollection()
|
invokeAction("modals.login.toggle")
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(collectionJSON.value)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDownloadCollection(collectionJSON.value, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCollectionGist = async () => {
|
|
||||||
if (!currentUser.value || !currentUser.value.accessToken) {
|
|
||||||
toast.error(t("profile.no_permission").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "gist",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
creatingGistCollection.value = true
|
|
||||||
await getJSONCollection()
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
createCollectionGists(collectionJSON.value, currentUser.value.accessToken),
|
|
||||||
TE.match(
|
|
||||||
(err) => {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(err)
|
|
||||||
creatingGistCollection.value = false
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
toast.success(t("export.gist_created").toString())
|
|
||||||
creatingGistCollection.value = false
|
|
||||||
window.open(result.data.html_url)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
|
||||||
if (!hasTeamWriteAccess.value) {
|
|
||||||
toast.error(t("team.no_access").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collectionsType.value.selectedTeam) return
|
|
||||||
|
|
||||||
importingMyCollections.value = true
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "import-to-teams",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
importJSONToTeam(
|
|
||||||
JSON.stringify(collection),
|
|
||||||
collectionsType.value.selectedTeam.id
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
importingMyCollections.value = false
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
importingMyCollections.value = false
|
|
||||||
displayModalImportExport(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveConfirmModal = (title: string | null) => {
|
const resolveConfirmModal = (title: string | null) => {
|
||||||
@@ -2041,37 +1922,36 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_coll/short_title":
|
case "team_coll/short_title":
|
||||||
return t("collection.name_length_insufficient")
|
return t("collection.name_length_insufficient")
|
||||||
case "team/invalid_coll_id":
|
case "team/invalid_coll_id":
|
||||||
case "bug/team_coll/no_coll_id":
|
case "bug/team_coll/no_coll_id":
|
||||||
case "team_req/invalid_target_id":
|
case "team_req/invalid_target_id":
|
||||||
return t("team.invalid_coll_id")
|
return t("team.invalid_coll_id")
|
||||||
case "team/not_required_role":
|
case "team/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_required_role":
|
case "team_req/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_found":
|
case "team_req/not_found":
|
||||||
return t("team.no_request_found")
|
return t("team.no_request_found")
|
||||||
case "bug/team_req/no_req_id":
|
case "bug/team_req/no_req_id":
|
||||||
return t("team.no_request_found")
|
return t("team.no_request_found")
|
||||||
case "team/collection_is_parent_coll":
|
case "team/collection_is_parent_coll":
|
||||||
return t("team.parent_coll_move")
|
return t("team.parent_coll_move")
|
||||||
case "team/target_and_destination_collection_are_same":
|
case "team/target_and_destination_collection_are_same":
|
||||||
return t("team.same_target_destination")
|
return t("team.same_target_destination")
|
||||||
case "team/target_collection_is_already_root_collection":
|
case "team/target_collection_is_already_root_collection":
|
||||||
return t("collection.invalid_root_move")
|
return t("collection.invalid_root_move")
|
||||||
case "team_req/requests_not_from_same_collection":
|
case "team_req/requests_not_from_same_collection":
|
||||||
return t("request.different_collection")
|
return t("request.different_collection")
|
||||||
case "team/team_collections_have_different_parents":
|
case "team/team_collections_have_different_parents":
|
||||||
return t("collection.different_parent")
|
return t("collection.different_parent")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,11 @@
|
|||||||
v-if="!currentInterceptorSupportsCookies"
|
v-if="!currentInterceptorSupportsCookies"
|
||||||
:text="t('cookies.modal.interceptor_no_support')"
|
:text="t('cookies.modal.interceptor_no_support')"
|
||||||
>
|
>
|
||||||
<AppInterceptor class="p-2 border rounded border-dividerLight" />
|
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-col">
|
<div v-else class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
|
class="sticky -mx-4 -mt-4 flex space-x-2 border-b border-dividerLight bg-primary px-4 py-4"
|
||||||
style="top: calc(-1 * var(--line-height-body))"
|
style="top: calc(-1 * var(--line-height-body))"
|
||||||
>
|
>
|
||||||
<HoppSmartInput
|
<HoppSmartInput
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
:key="domain"
|
:key="domain"
|
||||||
class="flex flex-col"
|
class="flex flex-col"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between flex-1">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<label for="cookiesList" class="p-4">
|
<label for="cookiesList" class="p-4">
|
||||||
{{ domain }}
|
{{ domain }}
|
||||||
</label>
|
</label>
|
||||||
@@ -65,11 +65,11 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded border-divider">
|
<div class="rounded border border-divider">
|
||||||
<div class="divide-y divide-dividerLight">
|
<div class="divide-y divide-dividerLight">
|
||||||
<div
|
<div
|
||||||
v-if="entries.length === 0"
|
v-if="entries.length === 0"
|
||||||
class="flex flex-col gap-2 p-4 items-center"
|
class="flex flex-col items-center gap-2 p-4"
|
||||||
>
|
>
|
||||||
{{ t("cookies.modal.no_cookies_in_domain") }}
|
{{ t("cookies.modal.no_cookies_in_domain") }}
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
class="flex divide-x divide-dividerLight"
|
class="flex divide-x divide-dividerLight"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
:value="entry"
|
:value="entry"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="border rounded border-dividerLight">
|
<div class="rounded border border-dividerLight">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex items-center justify-between pl-4">
|
<div class="flex items-center justify-between pl-4">
|
||||||
<label class="font-semibold truncate text-secondaryLight">
|
<label class="truncate font-semibold text-secondaryLight">
|
||||||
{{ t("cookies.modal.cookie_string") }}
|
{{ t("cookies.modal.cookie_string") }}
|
||||||
</label>
|
</label>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<div class="h-46">
|
<div class="h-46">
|
||||||
<div
|
<div
|
||||||
ref="cookieEditor"
|
ref="cookieEditor"
|
||||||
class="h-full border-t rounded-b border-dividerLight"
|
class="h-full rounded-b border-t border-dividerLight"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
212
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
212
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<header
|
||||||
|
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
|
||||||
|
>
|
||||||
|
<div class="flex flex-1 items-center justify-between space-x-2">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
:label="t('app.name')"
|
||||||
|
to="https://hoppscotch.io/"
|
||||||
|
blank
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('app.open_in_hoppscotch')"
|
||||||
|
:to="sharedRequestURL"
|
||||||
|
blank
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="flex-1">
|
||||||
|
<div
|
||||||
|
class="flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||||
|
>
|
||||||
|
<div class="relative flex">
|
||||||
|
<span
|
||||||
|
class="flex justify-center items-center w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||||
|
>
|
||||||
|
{{ tab.document.request.method }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
name="method"
|
||||||
|
:value="tab.document.request.endpoint"
|
||||||
|
class="flex-1 px-4 bg-primary"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex sm:mt-0">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
id="send"
|
||||||
|
:title="`${t(
|
||||||
|
'action.send'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
|
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||||
|
class="min-w-20 flex-1"
|
||||||
|
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:title="`${t(
|
||||||
|
'request.save'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||||
|
:label="t('request.save')"
|
||||||
|
filled
|
||||||
|
:icon="IconSave"
|
||||||
|
class="flex-1 rounded rounded-r-none"
|
||||||
|
blank
|
||||||
|
:to="sharedRequestURL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HttpRequestOptions
|
||||||
|
v-model="tab.document.request"
|
||||||
|
v-model:option-tab="selectedOptionTab"
|
||||||
|
:properties="properties"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<HttpResponse :document="tab.document" :is-embed="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Ref } from "vue"
|
||||||
|
import { computed, useModel } from "vue"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { useStreamSubscriber } from "~/composables/stream"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||||
|
import { HoppTab } from "~/services/tab"
|
||||||
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
|
import IconSave from "~icons/lucide/save"
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelTab: HoppTab<HoppRESTDocument>
|
||||||
|
properties: string[]
|
||||||
|
sharedRequestID: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tab = useModel(props, "modelTab")
|
||||||
|
|
||||||
|
const selectedOptionTab = ref(props.properties[0])
|
||||||
|
|
||||||
|
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||||
|
const sharedRequestURL = computed(() => {
|
||||||
|
return `${baseURL}/r/${props.sharedRequestID}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
|
const newSendRequest = async () => {
|
||||||
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMethodInEndpoint()
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||||
|
const streamResult = await streamPromise
|
||||||
|
|
||||||
|
requestCancelFunc.value = cancel
|
||||||
|
if (E.isRight(streamResult)) {
|
||||||
|
subscribeToStream(
|
||||||
|
streamResult.right,
|
||||||
|
(responseState) => {
|
||||||
|
if (loading.value) {
|
||||||
|
// Check exists because, loading can be set to false
|
||||||
|
// when cancelled
|
||||||
|
updateRESTResponse(responseState)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
loading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// TODO: Change this any to a proper type
|
||||||
|
const result = (streamResult.right as any).value
|
||||||
|
if (
|
||||||
|
result.type === "network_fail" &&
|
||||||
|
result.error?.error === "NO_PW_EXT_HOOK"
|
||||||
|
) {
|
||||||
|
const errorResponse: HoppRESTResponse = {
|
||||||
|
type: "extension_error",
|
||||||
|
error: result.error.humanMessage.heading,
|
||||||
|
component: result.error.component,
|
||||||
|
req: result.req,
|
||||||
|
}
|
||||||
|
updateRESTResponse(errorResponse)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
toast.error(`${t("error.script_fail")}`)
|
||||||
|
let error: Error
|
||||||
|
if (typeof streamResult.left === "string") {
|
||||||
|
error = { name: "RequestFailure", message: streamResult.left }
|
||||||
|
} else {
|
||||||
|
error = streamResult.left
|
||||||
|
}
|
||||||
|
updateRESTResponse({
|
||||||
|
type: "script_fail",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||||
|
tab.value.document.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEndpoint = computed(() => {
|
||||||
|
return tab.value.document.request.endpoint
|
||||||
|
})
|
||||||
|
|
||||||
|
const ensureMethodInEndpoint = () => {
|
||||||
|
if (
|
||||||
|
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
||||||
|
!newEndpoint.value.startsWith("<<")
|
||||||
|
) {
|
||||||
|
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||||
|
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||||
|
tab.value.document.request.endpoint =
|
||||||
|
"http://" + tab.value.document.request.endpoint
|
||||||
|
} else {
|
||||||
|
tab.value.document.request.endpoint =
|
||||||
|
"https://" + tab.value.document.request.endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelRequest = () => {
|
||||||
|
loading.value = false
|
||||||
|
requestCancelFunc.value?.()
|
||||||
|
|
||||||
|
updateRESTResponse(null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
@close="hideModal"
|
@close="hideModal"
|
||||||
>
|
>
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex space-y-4 flex-1 flex-col">
|
<div class="flex flex-1 flex-col space-y-4">
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="name" class="font-semibold min-w-10">{{
|
<label for="name" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.name")
|
t("environment.name")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<input
|
||||||
@@ -17,8 +17,8 @@
|
|||||||
class="input"
|
class="input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="value" class="font-semibold min-w-10">{{
|
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<input
|
||||||
@@ -28,21 +28,21 @@
|
|||||||
:placeholder="t('environment.value')"
|
:placeholder="t('environment.value')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center space-x-8 ml-2">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="scope" class="font-semibold min-w-10">
|
<label for="scope" class="min-w-[2.5rem] font-semibold">
|
||||||
{{ t("environment.scope") }}
|
{{ t("environment.scope") }}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
class="relative flex flex-1 flex-col border border-divider rounded focus-visible:border-dividerDark"
|
class="relative flex flex-1 flex-col rounded border border-divider focus-visible:border-dividerDark"
|
||||||
>
|
>
|
||||||
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
|
<EnvironmentsSelector v-model="scope" :is-scope-selector="true" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="replaceWithVariable" class="flex space-x-2 mt-3">
|
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2">
|
||||||
<div class="min-w-18" />
|
<div class="min-w-[4rem]" />
|
||||||
<HoppSmartCheckbox
|
<HoppSmartCheckbox
|
||||||
:on="replaceWithVariable"
|
:on="replaceWithVariable"
|
||||||
title="t('environment.replace_with_variable'))"
|
:title="t('environment.replace_with_variable')"
|
||||||
@change="replaceWithVariable = !replaceWithVariable"
|
@change="replaceWithVariable = !replaceWithVariable"
|
||||||
/>
|
/>
|
||||||
<label for="replaceWithVariable">
|
<label for="replaceWithVariable">
|
||||||
@@ -205,15 +205,14 @@ const addEnvironment = async () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,154 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="environment.title"
|
||||||
:title="`${t('environment.title')}`"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions!.focus()"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.more')"
|
|
||||||
:icon="IconMoreVertical"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('import.from_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
readEnvironmentGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
createEnvironmentGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
|
||||||
<HoppSmartSpinner class="my-4" />
|
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('import.from_json')"
|
|
||||||
@click="openDialogChooseFileToImportFrom"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
class="input"
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
@change="importFromJSON"
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="exportJSON"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
import { useToast } from "~/composables/toast"
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { platform } from "~/platform"
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
import axios from "axios"
|
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import {
|
import * as E from "fp-ts/Either"
|
||||||
environments$,
|
import { appendEnvironments, environments$ } from "~/newstore/environments"
|
||||||
replaceEnvironments,
|
|
||||||
appendEnvironments,
|
|
||||||
} from "~/newstore/environments"
|
|
||||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||||
|
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
|
||||||
|
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
|
||||||
|
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconPostman from "~icons/hopp/postman"
|
||||||
|
import IconUser from "~icons/lucide/user"
|
||||||
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { environmentsExporter } from "~/helpers/import-export/export/environments"
|
||||||
|
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
|
||||||
teamEnvironments?: TeamEnvironment[]
|
teamEnvironments?: TeamEnvironment[]
|
||||||
teamId?: string | undefined
|
teamId?: string | undefined
|
||||||
environmentType: "MY_ENV" | "TEAM_ENV"
|
environmentType: "MY_ENV" | "TEAM_ENV"
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const myEnvironments = useReadonlyStream(environments$, [])
|
const myEnvironments = useReadonlyStream(environments$, [])
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template refs
|
const isTeamEnvironment = computed(() => {
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
return props.environmentType === "TEAM_ENV"
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
})
|
||||||
|
|
||||||
const environmentJson = computed(() => {
|
const environmentJson = computed(() => {
|
||||||
if (
|
if (
|
||||||
@@ -158,266 +64,249 @@ const environmentJson = computed(() => {
|
|||||||
const teamEnvironments = props.teamEnvironments.map(
|
const teamEnvironments = props.teamEnvironments.map(
|
||||||
(x) => x.environment as Environment
|
(x) => x.environment as Environment
|
||||||
)
|
)
|
||||||
return JSON.stringify(teamEnvironments, null, 2)
|
return teamEnvironments
|
||||||
} else {
|
|
||||||
return JSON.stringify(myEnvironments.value, null, 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return myEnvironments.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const createEnvironmentGist = async () => {
|
const HoppEnvironmentsImport: ImporterOrExporter = {
|
||||||
if (!currentUser.value) {
|
metadata: {
|
||||||
toast.error(t("profile.no_permission").toString())
|
id: "import.from_json",
|
||||||
|
name: "import.from_json",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.hoppscotch_environment_description",
|
||||||
|
onImportFromFile: async (environments) => {
|
||||||
|
const res = await hoppEnvImporter(environments)()
|
||||||
|
|
||||||
return
|
if (E.isLeft(res)) {
|
||||||
}
|
showImportFailedError()
|
||||||
|
return
|
||||||
try {
|
|
||||||
const res = await axios.post(
|
|
||||||
"https://api.github.com/gists",
|
|
||||||
{
|
|
||||||
files: {
|
|
||||||
"hoppscotch-environments.json": {
|
|
||||||
content: environmentJson.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${currentUser.value.accessToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostmanEnvironmentsImport: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "import.from_postman",
|
||||||
|
name: "import.from_postman",
|
||||||
|
icon: IconPostman,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.postman_environment_description",
|
||||||
|
onImportFromFile: async (environments) => {
|
||||||
|
const res = await postmanEnvImporter(environments)()
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore([res.right])
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const EnvironmentsImportFromGIST: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "import.environments_from_gist",
|
||||||
|
name: "import.environments_from_gist",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.environments_from_gist",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.environments_from_gist_description",
|
||||||
|
onImportFromGist: async (environments) => {
|
||||||
|
if (E.isLeft(environments)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppEnvImporter(environments.right)()
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppEnvironmentsExport: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_json",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!environmentJson.value.length) {
|
||||||
|
return toast.error(t("error.no_environments_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
environmentsExporter(environmentJson.value),
|
||||||
|
"Environments"
|
||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
if (E.isLeft(message)) {
|
||||||
|
toast.error(t(message.left))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t(message.right))
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||||
platform: "rest",
|
platform: "rest",
|
||||||
})
|
})
|
||||||
|
},
|
||||||
window.open(res.data.html_url)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileImported = () => {
|
const HoppEnvironmentsGistExporter: ImporterOrExporter = {
|
||||||
toast.success(t("state.file_imported").toString())
|
metadata: {
|
||||||
}
|
id: "export.as_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
const failedImport = () => {
|
title:
|
||||||
toast.error(t("import.failed").toString())
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
}
|
// @ts-ignore
|
||||||
|
currentUser?.provider === "github.com"
|
||||||
const readEnvironmentGist = async () => {
|
? "export.create_secret_gist"
|
||||||
const gist = prompt(t("import.gist_url").toString())
|
: "export.require_github",
|
||||||
if (!gist) return
|
icon: IconUser,
|
||||||
|
disabled: !currentUser.value
|
||||||
try {
|
? true
|
||||||
const { files } = (await axios.get(
|
: currentUser.value.provider !== "github.com",
|
||||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
{
|
},
|
||||||
headers: {
|
action: async () => {
|
||||||
Accept: "application/vnd.github.v3+json",
|
if (!currentUser.value) {
|
||||||
},
|
toast.error(t("profile.no_permission"))
|
||||||
}
|
|
||||||
)) as {
|
|
||||||
files: {
|
|
||||||
[fileName: string]: {
|
|
||||||
content: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const environments = JSON.parse(Object.values(files)[0].content)
|
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
|
||||||
replaceEnvironments(environments)
|
|
||||||
fileImported()
|
|
||||||
} else {
|
|
||||||
importToTeams(environments)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failedImport()
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDialogChooseFileToImportFrom = () => {
|
|
||||||
if (inputChooseFileToImportFrom.value)
|
|
||||||
inputChooseFileToImportFrom.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importToTeams = async (content: Environment[]) => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [i, env] of content.entries()) {
|
|
||||||
if (i === content.length - 1) {
|
|
||||||
await pipe(
|
|
||||||
createTeamEnvironment(
|
|
||||||
JSON.stringify(env.variables),
|
|
||||||
props.teamId as string,
|
|
||||||
env.name
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
loading.value = false
|
|
||||||
hideModal()
|
|
||||||
fileImported()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
} else {
|
|
||||||
await pipe(
|
|
||||||
createTeamEnvironment(
|
|
||||||
JSON.stringify(env.variables),
|
|
||||||
props.teamId as string,
|
|
||||||
env.name
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// wait for all the environments to be created then fire the toast
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importFromJSON = () => {
|
|
||||||
if (!inputChooseFileToImportFrom.value) return
|
|
||||||
|
|
||||||
if (
|
|
||||||
!inputChooseFileToImportFrom.value.files ||
|
|
||||||
inputChooseFileToImportFrom.value.files.length === 0
|
|
||||||
) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const environments = JSON.parse(content)
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
if (
|
if (accessToken) {
|
||||||
environments._postman_variable_scope === "environment" ||
|
const res = await environmentsGistExporter(
|
||||||
environments._postman_variable_scope === "globals"
|
JSON.stringify(environmentJson.value),
|
||||||
) {
|
accessToken
|
||||||
importFromPostman(environments)
|
)
|
||||||
} else if (environments[0]) {
|
|
||||||
const [name, variables] = Object.keys(environments[0])
|
if (E.isLeft(res)) {
|
||||||
if (name === "name" && variables === "variables") {
|
toast.error(t("export.failed"))
|
||||||
// Do nothing
|
return
|
||||||
}
|
}
|
||||||
importFromHoppscotch(environments)
|
|
||||||
} else {
|
|
||||||
failedImport()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
toast.success(t("export.success"))
|
||||||
inputChooseFileToImportFrom.value.value = ""
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(res.right, "_blank")
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const importFromHoppscotch = (environments: Environment[]) => {
|
const importerModules = [
|
||||||
|
HoppEnvironmentsImport,
|
||||||
|
EnvironmentsImportFromGIST,
|
||||||
|
PostmanEnvironmentsImport,
|
||||||
|
]
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const enabledExporters = [HoppEnvironmentsExport]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
enabledExporters.push(HoppEnvironmentsGistExporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledExporters
|
||||||
|
})
|
||||||
|
|
||||||
|
const showImportFailedError = () => {
|
||||||
|
toast.error(t("import.failed").toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportToStore = async (environments: Environment[]) => {
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
appendEnvironments(environments)
|
appendEnvironments(environments)
|
||||||
fileImported()
|
toast.success(t("state.file_imported"))
|
||||||
} else {
|
} else {
|
||||||
importToTeams(environments)
|
await importToTeams(environments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const importFromPostman = ({
|
const importToTeams = async (content: Environment[]) => {
|
||||||
name,
|
const envImportPromises: Promise<
|
||||||
values,
|
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
|
||||||
}: {
|
>[] = []
|
||||||
name: string
|
|
||||||
values: { key: string; value: string }[]
|
|
||||||
}) => {
|
|
||||||
const environment: Environment = { name, variables: [] }
|
|
||||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
|
||||||
const environments = [environment]
|
|
||||||
|
|
||||||
importFromHoppscotch(environments)
|
for (const [, env] of content.entries()) {
|
||||||
}
|
const res = createTeamEnvironment(
|
||||||
|
JSON.stringify(env.variables),
|
||||||
|
props.teamId as string,
|
||||||
|
env.name
|
||||||
|
)()
|
||||||
|
|
||||||
const exportJSON = async () => {
|
envImportPromises.push(res)
|
||||||
const dataToWrite = environmentJson.value
|
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(dataToWrite)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_environments_to_export"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
const res = await Promise.all(envImportPromises)
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
const failedImports = res.some((r) => E.isLeft(r))
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
if (failedImports) {
|
||||||
|
toast.error(t("import.failed"))
|
||||||
const result = await platform.io.saveFileWithDialog({
|
|
||||||
data: dataToWrite,
|
|
||||||
contentType: "application/json",
|
|
||||||
suggestedFilename: filename,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "JSON file",
|
|
||||||
extensions: ["json"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.type === "unknown" || result.type === "saved") {
|
|
||||||
toast.success(t("state.download_started").toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
|
||||||
if (err.type === "network_error") {
|
|
||||||
return t("error.network_error")
|
|
||||||
} else {
|
} else {
|
||||||
switch (err.error) {
|
toast.success(t("import.success"))
|
||||||
case "team_environment/not_found":
|
|
||||||
return t("team_environment.not_found")
|
|
||||||
default:
|
|
||||||
return t("error.something_went_wrong")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => envSelectorActions!.focus()"
|
:on-shown="() => envSelectorActions!.focus()"
|
||||||
>
|
>
|
||||||
<span
|
<HoppSmartSelectWrapper
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="`${t('environment.select')}`"
|
:title="`${t('environment.select')}`"
|
||||||
class="select-wrapper"
|
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
@@ -20,9 +19,9 @@
|
|||||||
: `${t('environment.select')}`
|
: `${t('environment.select')}`
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
class="flex-1 !justify-start pr-8 rounded-none"
|
class="flex-1 !justify-start rounded-none pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="envSelectorActions"
|
ref="envSelectorActions"
|
||||||
@@ -101,7 +100,7 @@
|
|||||||
<img
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
/>
|
/>
|
||||||
<span class="pb-2 text-center">
|
<span class="pb-2 text-center">
|
||||||
@@ -148,7 +147,7 @@
|
|||||||
<img
|
<img
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="inline-flex flex-col object-contain object-center w-16 h-16 mb-2"
|
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
/>
|
/>
|
||||||
<span class="pb-2 text-center">
|
<span class="pb-2 text-center">
|
||||||
@@ -160,7 +159,7 @@
|
|||||||
v-if="!teamListLoading && teamAdapterError"
|
v-if="!teamListLoading && teamAdapterError"
|
||||||
class="flex flex-col items-center py-4"
|
class="flex flex-col items-center py-4"
|
||||||
>
|
>
|
||||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||||
{{ getErrorMessage(teamAdapterError) }}
|
{{ getErrorMessage(teamAdapterError) }}
|
||||||
</div>
|
</div>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
@@ -190,7 +189,7 @@
|
|||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
|
class="sticky top-0 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark"
|
||||||
>
|
>
|
||||||
{{ t("environment.global_variables") }}
|
{{ t("environment.global_variables") }}
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
@@ -205,12 +204,16 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
|
<div class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
||||||
<div class="flex flex-1 space-x-4">
|
<div class="flex flex-1 space-x-4">
|
||||||
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.name") }}
|
{{ t("environment.name") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="w-full min-w-32 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.value") }}
|
{{ t("environment.value") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,10 +222,10 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="flex flex-1 space-x-4"
|
class="flex flex-1 space-x-4"
|
||||||
>
|
>
|
||||||
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
|
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-secondaryLight w-full min-w-32 truncate">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
{{ variable.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -231,7 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 mt-2 font-semibold truncate flex items-center justify-between text-secondaryDark bg-primary border border-divider rounded pl-4"
|
class="sticky top-0 mt-2 flex items-center justify-between truncate rounded border border-divider bg-primary pl-4 font-semibold text-secondaryDark"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-primaryLight': !selectedEnv.variables,
|
'bg-primaryLight': !selectedEnv.variables,
|
||||||
}"
|
}"
|
||||||
@@ -252,16 +255,20 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
|
v-if="selectedEnv.type === 'NO_ENV_SELECTED'"
|
||||||
class="text-secondaryLight my-2 flex flex-col flex-1 pl-4"
|
class="my-2 flex flex-1 flex-col pl-4 text-secondaryLight"
|
||||||
>
|
>
|
||||||
{{ t("environment.no_active_environment") }}
|
{{ t("environment.no_active_environment") }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="my-2 flex flex-col flex-1 space-y-2 pl-4 pr-2">
|
<div v-else class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
||||||
<div class="flex flex-1 space-x-4">
|
<div class="flex flex-1 space-x-4">
|
||||||
<span class="w-1/4 min-w-32 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.name") }}
|
{{ t("environment.name") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="w-full min-w-32 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.value") }}
|
{{ t("environment.value") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,10 +277,10 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="flex flex-1 space-x-4"
|
class="flex flex-1 space-x-4"
|
||||||
>
|
>
|
||||||
<span class="text-secondaryLight w-1/4 min-w-32 truncate">
|
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-secondaryLight w-full min-w-32 truncate">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
{{ variable.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,12 +453,11 @@ const isEnvActive = (id: string | number) => {
|
|||||||
} else {
|
} else {
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
return selectedEnv.value.index === id
|
return selectedEnv.value.index === id
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
|
||||||
selectedEnv.value.teamEnvID === id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnv.value.teamEnvID === id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,40 +502,36 @@ const selectedEnv = computed(() => {
|
|||||||
name: props.modelValue.environment.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: props.modelValue.environment.id,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return { type: "global", name: "Global" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
|
||||||
const environment =
|
|
||||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
|
||||||
return {
|
|
||||||
type: "MY_ENV",
|
|
||||||
index: selectedEnvironmentIndex.value.index,
|
|
||||||
name: environment.name,
|
|
||||||
variables: environment.variables,
|
|
||||||
}
|
|
||||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
|
||||||
const teamEnv = teamEnvironmentList.value.find(
|
|
||||||
(env) =>
|
|
||||||
env.id ===
|
|
||||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
|
||||||
selectedEnvironmentIndex.value.teamEnvID)
|
|
||||||
)
|
|
||||||
if (teamEnv) {
|
|
||||||
return {
|
|
||||||
type: "TEAM_ENV",
|
|
||||||
name: teamEnv.environment.name,
|
|
||||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
|
||||||
variables: teamEnv.environment.variables,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { type: "NO_ENV_SELECTED" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { type: "NO_ENV_SELECTED" }
|
|
||||||
}
|
}
|
||||||
|
return { type: "global", name: "Global" }
|
||||||
}
|
}
|
||||||
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
|
const environment =
|
||||||
|
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
||||||
|
return {
|
||||||
|
type: "MY_ENV",
|
||||||
|
index: selectedEnvironmentIndex.value.index,
|
||||||
|
name: environment.name,
|
||||||
|
variables: environment.variables,
|
||||||
|
}
|
||||||
|
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||||
|
const teamEnv = teamEnvironmentList.value.find(
|
||||||
|
(env) =>
|
||||||
|
env.id ===
|
||||||
|
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnvironmentIndex.value.teamEnvID)
|
||||||
|
)
|
||||||
|
if (teamEnv) {
|
||||||
|
return {
|
||||||
|
type: "TEAM_ENV",
|
||||||
|
name: teamEnv.environment.name,
|
||||||
|
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||||
|
variables: teamEnv.environment.variables,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: "NO_ENV_SELECTED" }
|
||||||
|
}
|
||||||
|
return { type: "NO_ENV_SELECTED" }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set the selected environment as initial scope value
|
// Set the selected environment as initial scope value
|
||||||
@@ -577,13 +579,12 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,9 +593,8 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
|
|||||||
const environmentVariables = computed(() => {
|
const environmentVariables = computed(() => {
|
||||||
if (selectedEnv.value.variables) {
|
if (selectedEnv.value.variables) {
|
||||||
return selectedEnv.value.variables
|
return selectedEnv.value.variables
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
const editGlobalEnv = () => {
|
const editGlobalEnv = () => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-col flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary"
|
||||||
>
|
>
|
||||||
<WorkspaceCurrent :section="t('tab.environments')" />
|
<WorkspaceCurrent :section="t('tab.environments')" />
|
||||||
<EnvironmentsMyEnvironment
|
<EnvironmentsMyEnvironment
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex items-center justify-between flex-1">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<label for="variableList" class="p-4">
|
<label for="variableList" class="p-4">
|
||||||
{{ t("environment.variable_list") }}
|
{{ t("environment.variable_list") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="evnExpandError"
|
v-if="evnExpandError"
|
||||||
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
>
|
>
|
||||||
{{ t("environment.nested_overflow") }}
|
{{ t("environment.nested_overflow") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded divide-y divide-dividerLight border-divider">
|
<div class="divide-y divide-dividerLight rounded border border-divider">
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-for="({ id, env }, index) in vars"
|
||||||
:key="`variable-${id}-${index}`"
|
:key="`variable-${id}-${index}`"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="env.key"
|
v-model="env.key"
|
||||||
v-focus
|
v-focus
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
||||||
:name="'param' + index"
|
:name="'param' + index"
|
||||||
/>
|
/>
|
||||||
@@ -198,9 +198,8 @@ const workingEnv = computed(() => {
|
|||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: props.editingEnvironmentIndex,
|
index: props.editingEnvironmentIndex,
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||||
@@ -226,12 +225,11 @@ const liveEnvs = computed(() => {
|
|||||||
return [
|
return [
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||||
]
|
]
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
|
||||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return [
|
||||||
|
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||||
|
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1,24 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
@contextmenu.prevent="options!.tippy.show()"
|
@contextmenu.prevent="options!.tippy.show()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="environmentIndex === 'Global'"
|
v-if="environmentIndex === 'Global'"
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex cursor-pointer items-center justify-center px-4"
|
||||||
@click="emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<icon-lucide-globe class="svg-icons" />
|
<icon-lucide-globe class="svg-icons" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex cursor-pointer items-center justify-center px-4"
|
||||||
@click="emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<icon-lucide-layers class="svg-icons" />
|
<icon-lucide-layers class="svg-icons" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
@click="emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
|
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<span class="text-secondaryLight text-center">
|
<span class="text-center text-secondaryLight">
|
||||||
{{ t("environment.import_or_create") }}
|
{{ t("environment.import_or_create") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 flex-col items-stretch">
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:icon="IconImport"
|
:icon="IconImport"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
environment-type="MY_ENV"
|
environment-type="MY_ENV"
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
@submit="saveEnvironment"
|
@submit="saveEnvironment"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex items-center justify-between flex-1">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<label for="variableList" class="p-4">
|
<label for="variableList" class="p-4">
|
||||||
{{ t("environment.variable_list") }}
|
{{ t("environment.variable_list") }}
|
||||||
</label>
|
</label>
|
||||||
@@ -37,11 +37,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="evnExpandError"
|
v-if="evnExpandError"
|
||||||
class="w-full px-4 py-2 mb-2 overflow-auto font-mono text-red-400 whitespace-normal rounded bg-primaryLight"
|
class="mb-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
>
|
>
|
||||||
{{ t("environment.nested_overflow") }}
|
{{ t("environment.nested_overflow") }}
|
||||||
</div>
|
</div>
|
||||||
<div class="border rounded divide-y divide-dividerLight border-divider">
|
<div class="divide-y divide-dividerLight rounded border border-divider">
|
||||||
<div
|
<div
|
||||||
v-for="({ id, env }, index) in vars"
|
v-for="({ id, env }, index) in vars"
|
||||||
:key="`variable-${id}-${index}`"
|
:key="`variable-${id}-${index}`"
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="env.key"
|
v-model="env.key"
|
||||||
v-focus
|
v-focus
|
||||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
class="flex flex-1 bg-transparent px-4 py-2"
|
||||||
:class="isViewer && 'opacity-25'"
|
:class="isViewer && 'opacity-25'"
|
||||||
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
:placeholder="`${t('count.variable', { count: index + 1 })}`"
|
||||||
:name="'param' + index"
|
:name="'param' + index"
|
||||||
@@ -205,11 +205,8 @@ const evnExpandError = computed(() => {
|
|||||||
const liveEnvs = computed(() => {
|
const liveEnvs = computed(() => {
|
||||||
if (evnExpandError.value) {
|
if (evnExpandError.value) {
|
||||||
return []
|
return []
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -338,13 +335,12 @@ const hideModal = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch group"
|
class="group flex items-stretch"
|
||||||
@contextmenu.prevent="options!.tippy.show()"
|
@contextmenu.prevent="options!.tippy.show()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="flex items-center justify-center px-4 cursor-pointer"
|
class="flex cursor-pointer items-center justify-center px-4"
|
||||||
@click="emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<icon-lucide-layers class="svg-icons" />
|
<icon-lucide-layers class="svg-icons" />
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex flex-1 min-w-0 py-2 pr-2 cursor-pointer transition group-hover:text-secondaryDark"
|
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
|
||||||
@click="emit('edit-environment')"
|
@click="emit('edit-environment')"
|
||||||
>
|
>
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
@@ -184,13 +184,12 @@ const duplicateEnvironments = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex justify-between flex-1 flex-shrink-0 overflow-x-auto border-b top-upperPrimaryStickyFold border-dividerLight bg-primary"
|
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||||
@@ -50,10 +50,10 @@
|
|||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<span class="text-secondaryLight text-center">
|
<span class="text-center text-secondaryLight">
|
||||||
{{ t("environment.import_or_create") }}
|
{{ t("environment.import_or_create") }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-4 flex-col items-stretch">
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:icon="IconImport"
|
:icon="IconImport"
|
||||||
:label="t('import.title')"
|
:label="t('import.title')"
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
v-if="!loading && adapterError"
|
v-if="!loading && adapterError"
|
||||||
class="flex flex-col items-center py-4"
|
class="flex flex-col items-center py-4"
|
||||||
>
|
>
|
||||||
<icon-lucide-help-circle class="mb-4 svg-icons" />
|
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||||
{{ getErrorMessage(adapterError) }}
|
{{ getErrorMessage(adapterError) }}
|
||||||
</div>
|
</div>
|
||||||
<EnvironmentsTeamsDetails
|
<EnvironmentsTeamsDetails
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
:team-environments="teamEnvironments"
|
:team-environments="teamEnvironments"
|
||||||
:team-id="team?.id"
|
:team-id="team?.id"
|
||||||
environment-type="TEAM_ENV"
|
environment-type="TEAM_ENV"
|
||||||
@@ -174,13 +174,12 @@ const resetSelectedData = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,9 +47,9 @@
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
|
||||||
<div class="flex flex-col items-center justify-center max-w-md">
|
<div class="flex max-w-md flex-col items-center justify-center">
|
||||||
<icon-lucide-inbox class="w-6 h-6 text-accent" />
|
<icon-lucide-inbox class="h-6 w-6 text-accent" />
|
||||||
<h3 class="my-2 text-lg text-center">
|
<h3 class="my-2 text-center text-lg">
|
||||||
{{ t("auth.we_sent_magic_link") }}
|
{{ t("auth.we_sent_magic_link") }}
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-center">
|
<p class="text-center">
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'sign-in' && tosLink && privacyPolicyLink"
|
v-if="mode === 'sign-in' && tosLink && privacyPolicyLink"
|
||||||
class="text-secondaryLight text-tiny"
|
class="text-tiny text-secondaryLight"
|
||||||
>
|
>
|
||||||
By signing in, you are agreeing to our
|
By signing in, you are agreeing to our
|
||||||
<HoppSmartAnchor
|
<HoppSmartAnchor
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="mode === 'email-sent'"
|
v-if="mode === 'email-sent'"
|
||||||
class="flex justify-between flex-1 text-secondaryLight"
|
class="flex flex-1 justify-between text-secondaryLight"
|
||||||
>
|
>
|
||||||
<HoppSmartAnchor
|
<HoppSmartAnchor
|
||||||
class="link"
|
class="link"
|
||||||
@@ -111,20 +111,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, computed, onMounted, ref } from "vue"
|
import { Ref, computed, onMounted, ref } from "vue"
|
||||||
|
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useStreamSubscriber } from "@composables/stream"
|
import { useStreamSubscriber } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { setLocalConfig } from "~/newstore/localpersistence"
|
|
||||||
|
|
||||||
|
import IconEmail from "~icons/auth/email"
|
||||||
import IconGithub from "~icons/auth/github"
|
import IconGithub from "~icons/auth/github"
|
||||||
import IconGoogle from "~icons/auth/google"
|
import IconGoogle from "~icons/auth/google"
|
||||||
import IconEmail from "~icons/auth/email"
|
|
||||||
import IconMicrosoft from "~icons/auth/microsoft"
|
import IconMicrosoft from "~icons/auth/microsoft"
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
import { LoginItemDef } from "~/platform/auth"
|
import { LoginItemDef } from "~/platform/auth"
|
||||||
|
import { PersistenceService } from "~/services/persistence"
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean
|
show: boolean
|
||||||
@@ -138,6 +139,8 @@ const { subscribeToStream } = useStreamSubscriber()
|
|||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
const persistenceService = useService(PersistenceService)
|
||||||
|
|
||||||
const form = {
|
const form = {
|
||||||
email: "",
|
email: "",
|
||||||
}
|
}
|
||||||
@@ -260,7 +263,7 @@ const signInWithEmail = async () => {
|
|||||||
.signInWithEmail(form.email)
|
.signInWithEmail(form.email)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
mode.value = "email-sent"
|
mode.value = "email-sent"
|
||||||
setLocalConfig("emailForSignIn", form.email)
|
persistenceService.setLocalConfig("emailForSignIn", form.email)
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user