Compare commits
80 Commits
release/20
...
revamp/hea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dcfc684ef | ||
|
|
1cc845e17d | ||
|
|
60bfb6fe2c | ||
|
|
144d14ab5b | ||
|
|
8f1ca6e282 | ||
|
|
a93758c6b7 | ||
|
|
1829c088cc | ||
|
|
ee1425d0dd | ||
|
|
24ae090916 | ||
|
|
a3aa9b68fc | ||
|
|
50f475334e | ||
|
|
7b18526f24 | ||
|
|
23afc201a1 | ||
|
|
b1982d74a6 | ||
|
|
e93a37c711 | ||
|
|
8d7509cdea | ||
|
|
e24d0ce605 | ||
|
|
f5d2e4f11f | ||
|
|
de725337d6 | ||
|
|
9d1d369f37 | ||
|
|
2bd925d441 | ||
|
|
bb8dc6f7eb | ||
|
|
be3e5ba7e7 | ||
|
|
663134839f | ||
|
|
736f83a70c | ||
|
|
05d2175f43 | ||
|
|
4caf0053cd | ||
|
|
97bd808431 | ||
|
|
a13c2fd4c1 | ||
|
|
16044b5840 | ||
|
|
93ce86f32d | ||
|
|
4ebf850cb6 | ||
|
|
76af7d5e10 | ||
|
|
507fe69efe | ||
|
|
23e3739718 | ||
|
|
5428a73811 | ||
|
|
4a154e6569 | ||
|
|
0aa5825d8b | ||
|
|
bdb63e99d5 | ||
|
|
6daa043a1b | ||
|
|
8175ec640a | ||
|
|
b5307e4a89 | ||
|
|
19294802be | ||
|
|
cbe3e14b47 | ||
|
|
9dcbc4a126 | ||
|
|
01df1663ad | ||
|
|
a215860782 | ||
|
|
abd5288da8 | ||
|
|
a89bc473f6 | ||
|
|
59b5a50a97 | ||
|
|
57cb59027b | ||
|
|
d1c9c3583f | ||
|
|
2462492c86 | ||
|
|
7a9f0c8756 | ||
|
|
46caf9b198 | ||
|
|
f5db54484c | ||
|
|
8deb6471b9 | ||
|
|
73b3ff8e41 | ||
|
|
016a18d3b2 | ||
|
|
ba31cdabea | ||
|
|
51510566bc | ||
|
|
cabee0ecc8 | ||
|
|
2c2b39a236 | ||
|
|
78450c9316 | ||
|
|
b18fd90b64 | ||
|
|
0188a8d7db | ||
|
|
6c63a8dc28 | ||
|
|
17d6ae15a5 | ||
|
|
40f72278a9 | ||
|
|
f717704731 | ||
|
|
185c225297 | ||
|
|
2694731c36 | ||
|
|
ae89af9978 | ||
|
|
87d617012f | ||
|
|
2420b3fa42 | ||
|
|
175a991ec4 | ||
|
|
0301649aff | ||
|
|
544b045300 | ||
|
|
65884293be | ||
|
|
3cb4861bac |
@@ -5,5 +5,5 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
|
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
|
||||||
},
|
},
|
||||||
"postCreateCommand": "mv .env.example .env && pnpm i"
|
"postCreateCommand": "cp .env.example .env && pnpm i"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
3
.github/workflows/release-push-docker.yml
vendored
@@ -18,6 +18,9 @@ jobs:
|
|||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
2
.github/workflows/ui.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
|
|
||||||
# Deploy the ui site with netlify-cli
|
# Deploy the ui site with netlify-cli
|
||||||
- name: Deploy to Netlify (ui)
|
- name: Deploy to Netlify (ui)
|
||||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
run: npx netlify-cli@15.11.0 deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||||
env:
|
env:
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||||
|
|||||||
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
@@ -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
@@ -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
|
||||||
|
|||||||
10
package.json
@@ -22,16 +22,14 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"./packages/*"
|
"./packages/*"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
|
||||||
"husky": "^7.0.4",
|
|
||||||
"lint-staged": "^12.3.8"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^16.2.3",
|
"@commitlint/cli": "^16.2.3",
|
||||||
"@commitlint/config-conventional": "^16.2.1",
|
"@commitlint/config-conventional": "^16.2.1",
|
||||||
"@types/node": "^17.0.24",
|
"@types/node": "17.0.27",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"http-server": "^14.1.1"
|
"http-server": "^14.1.1",
|
||||||
|
"husky": "^7.0.4",
|
||||||
|
"lint-staged": "12.4.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"packageExtensions": {
|
"packageExtensions": {
|
||||||
|
|||||||
@@ -17,16 +17,16 @@
|
|||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"sideEffects": false,
|
"sideEffects": false,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.9.0",
|
"@codemirror/language": "6.9.0",
|
||||||
"@lezer/highlight": "^1.1.6",
|
"@lezer/highlight": "1.1.4",
|
||||||
"@lezer/lr": "^1.3.10"
|
"@lezer/lr": "^1.3.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@lezer/generator": "^1.5.0",
|
"@lezer/generator": "^1.5.1",
|
||||||
"mocha": "^9.2.2",
|
"mocha": "^9.2.2",
|
||||||
"rollup": "^2.70.2",
|
"rollup": "^3.29.3",
|
||||||
"rollup-plugin-dts": "^4.2.1",
|
"rollup-plugin-dts": "^6.0.2",
|
||||||
"rollup-plugin-ts": "^2.0.7",
|
"rollup-plugin-ts": "^3.4.5",
|
||||||
"typescript": "^4.6.3"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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.1",
|
"version": "2023.8.4-1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -24,18 +24,17 @@
|
|||||||
"do-test": "pnpm run test"
|
"do-test": "pnpm run test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs-modules/mailer": "^1.8.1",
|
"@apollo/server": "^4.9.4",
|
||||||
"@nestjs/apollo": "^10.1.6",
|
"@nestjs-modules/mailer": "^1.9.1",
|
||||||
"@nestjs/common": "^9.2.1",
|
"@nestjs/apollo": "^12.0.9",
|
||||||
"@nestjs/core": "^9.2.1",
|
"@nestjs/common": "^10.2.6",
|
||||||
"@nestjs/graphql": "^10.1.6",
|
"@nestjs/core": "^10.2.6",
|
||||||
"@nestjs/jwt": "^10.0.1",
|
"@nestjs/graphql": "^12.0.9",
|
||||||
"@nestjs/passport": "^9.0.0",
|
"@nestjs/jwt": "^10.1.1",
|
||||||
"@nestjs/platform-express": "^9.2.1",
|
"@nestjs/passport": "^10.0.2",
|
||||||
"@nestjs/throttler": "^4.0.0",
|
"@nestjs/platform-express": "^10.2.6",
|
||||||
|
"@nestjs/throttler": "^5.0.0",
|
||||||
"@prisma/client": "^4.16.2",
|
"@prisma/client": "^4.16.2",
|
||||||
"apollo-server-express": "^3.11.1",
|
|
||||||
"apollo-server-plugin-base": "^3.7.1",
|
|
||||||
"argon2": "^0.30.3",
|
"argon2": "^0.30.3",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"cookie": "^0.5.0",
|
"cookie": "^0.5.0",
|
||||||
@@ -43,9 +42,9 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"fp-ts": "^2.13.1",
|
"fp-ts": "^2.13.1",
|
||||||
"graphql": "^15.5.0",
|
"graphql": "^16.8.1",
|
||||||
"graphql-query-complexity": "^0.12.0",
|
"graphql-query-complexity": "^0.12.0",
|
||||||
"graphql-redis-subscriptions": "^2.5.0",
|
"graphql-redis-subscriptions": "^2.6.0",
|
||||||
"graphql-subscriptions": "^2.0.0",
|
"graphql-subscriptions": "^2.0.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"io-ts": "^2.2.16",
|
"io-ts": "^2.2.16",
|
||||||
@@ -63,9 +62,9 @@
|
|||||||
"rxjs": "^7.6.0"
|
"rxjs": "^7.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^9.1.5",
|
"@nestjs/cli": "^10.1.18",
|
||||||
"@nestjs/schematics": "^9.0.3",
|
"@nestjs/schematics": "^10.0.2",
|
||||||
"@nestjs/testing": "^9.2.1",
|
"@nestjs/testing": "^10.2.6",
|
||||||
"@relmify/jest-fp-ts": "^2.0.2",
|
"@relmify/jest-fp-ts": "^2.0.2",
|
||||||
"@types/argon2": "^0.15.0",
|
"@types/argon2": "^0.15.0",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -68,11 +68,13 @@ model TeamRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Shortcode {
|
model Shortcode {
|
||||||
id String @id
|
id String @id @unique
|
||||||
request Json
|
request Json
|
||||||
|
embedProperties Json?
|
||||||
creatorUid String?
|
creatorUid String?
|
||||||
|
User User? @relation(fields: [creatorUid], references: [uid])
|
||||||
createdOn DateTime @default(now())
|
createdOn DateTime @default(now())
|
||||||
|
updatedOn DateTime @updatedAt @default(now())
|
||||||
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,6 +104,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 {
|
||||||
|
|||||||
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,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +76,7 @@ export class AdminService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
|
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
|
||||||
template: 'code-your-own',
|
template: 'user-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: inviteeEmail,
|
inviteeEmail: inviteeEmail,
|
||||||
magicLink: `${process.env.VITE_BASE_URL}`,
|
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,12 +27,7 @@ import { AppController } from './app.controller';
|
|||||||
buildSchemaOptions: {
|
buildSchemaOptions: {
|
||||||
numberScalarMode: 'integer',
|
numberScalarMode: 'integer',
|
||||||
},
|
},
|
||||||
cors: {
|
|
||||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
|
||||||
credentials: true,
|
|
||||||
},
|
|
||||||
playground: process.env.PRODUCTION !== 'true',
|
playground: process.env.PRODUCTION !== 'true',
|
||||||
debug: process.env.PRODUCTION !== 'true',
|
|
||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
installSubscriptionHandlers: true,
|
installSubscriptionHandlers: true,
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
@@ -62,10 +57,12 @@ import { AppController } from './app.controller';
|
|||||||
}),
|
}),
|
||||||
driver: ApolloDriver,
|
driver: ApolloDriver,
|
||||||
}),
|
}),
|
||||||
ThrottlerModule.forRoot({
|
ThrottlerModule.forRoot([
|
||||||
|
{
|
||||||
ttl: +process.env.RATE_LIMIT_TTL,
|
ttl: +process.env.RATE_LIMIT_TTL,
|
||||||
limit: +process.env.RATE_LIMIT_MAX,
|
limit: +process.env.RATE_LIMIT_MAX,
|
||||||
}),
|
},
|
||||||
|
]),
|
||||||
UserModule,
|
UserModule,
|
||||||
AuthModule,
|
AuthModule,
|
||||||
AdminModule,
|
AdminModule,
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.mailerService.sendEmail(email, {
|
await this.mailerService.sendEmail(email, {
|
||||||
template: 'code-your-own',
|
template: 'user-invitation',
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: email,
|
inviteeEmail: email,
|
||||||
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
magicLink: `${url}/enter?token=${generatedTokens.token}`,
|
||||||
|
|||||||
@@ -318,18 +318,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)
|
||||||
@@ -621,3 +609,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,
|
||||||
@@ -93,9 +95,7 @@ export async function emitGQLSchemaFile() {
|
|||||||
numberScalarMode: 'integer',
|
numberScalarMode: 'integer',
|
||||||
});
|
});
|
||||||
|
|
||||||
const schemaString = printSchema(schema, {
|
const schemaString = printSchema(schema);
|
||||||
commentDescriptions: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);
|
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
|
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
|
||||||
protected getTracker(req: Record<string, any>): string {
|
protected async getTracker(req: Record<string, any>): Promise<string> {
|
||||||
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
|
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
|
||||||
// learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#directives
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export type MailDescription = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type UserMagicLinkMailDescription = {
|
export type UserMagicLinkMailDescription = {
|
||||||
template: 'code-your-own';
|
template: 'user-invitation';
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: string;
|
inviteeEmail: string;
|
||||||
magicLink: string;
|
magicLink: string;
|
||||||
@@ -16,7 +16,7 @@ export type UserMagicLinkMailDescription = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type AdminUserInvitationMailDescription = {
|
export type AdminUserInvitationMailDescription = {
|
||||||
template: 'code-your-own';
|
template: 'user-invitation';
|
||||||
variables: {
|
variables: {
|
||||||
inviteeEmail: string;
|
inviteeEmail: string;
|
||||||
magicLink: string;
|
magicLink: string;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export class MailerService {
|
|||||||
case 'team-invitation':
|
case 'team-invitation':
|
||||||
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
|
||||||
|
|
||||||
case 'code-your-own':
|
case 'user-invitation':
|
||||||
return 'Sign in to Hoppscotch';
|
return 'Sign in to Hoppscotch';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { GraphQLSchemaHost } from '@nestjs/graphql';
|
import { GraphQLSchemaHost } from '@nestjs/graphql';
|
||||||
import {
|
import {
|
||||||
ApolloServerPlugin,
|
ApolloServerPlugin,
|
||||||
|
BaseContext,
|
||||||
GraphQLRequestListener,
|
GraphQLRequestListener,
|
||||||
} from 'apollo-server-plugin-base';
|
} from '@apollo/server';
|
||||||
import { Plugin } from '@nestjs/apollo';
|
import { Plugin } from '@nestjs/apollo';
|
||||||
import { GraphQLError } from 'graphql';
|
import { GraphQLError } from 'graphql';
|
||||||
import {
|
import {
|
||||||
@@ -17,7 +18,7 @@ const COMPLEXITY_LIMIT = 50;
|
|||||||
export class GQLComplexityPlugin implements ApolloServerPlugin {
|
export class GQLComplexityPlugin implements ApolloServerPlugin {
|
||||||
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
|
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
|
||||||
|
|
||||||
async requestDidStart(): Promise<GraphQLRequestListener> {
|
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
|
||||||
const { schema } = this.gqlSchemaHost;
|
const { schema } = this.gqlSchemaHost;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||||
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||||
import {
|
import {
|
||||||
TEAM_COLL_DEST_SAME,
|
TEAM_COLL_DEST_SAME,
|
||||||
TEAM_COLL_INVALID_JSON,
|
TEAM_COLL_INVALID_JSON,
|
||||||
@@ -17,9 +17,6 @@ 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';
|
|
||||||
import { TeamCollectionModule } from './team-collection.module';
|
|
||||||
import * as E from 'fp-ts/Either';
|
|
||||||
|
|
||||||
const mockPrisma = mockDeep<PrismaService>();
|
const mockPrisma = mockDeep<PrismaService>();
|
||||||
const mockPubSub = mockDeep<PubSubService>();
|
const mockPubSub = mockDeep<PubSubService>();
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
|
|
||||||
describe('createDuplicateEnvironment', () => {
|
describe('createDuplicateEnvironment', () => {
|
||||||
test('should successfully duplicate an existing team environment', async () => {
|
test('should successfully duplicate an existing team environment', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
teamEnvironment,
|
teamEnvironment,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -322,7 +322,9 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValue(
|
||||||
|
'NotFoundError',
|
||||||
|
);
|
||||||
|
|
||||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||||
teamEnvironment.id,
|
teamEnvironment.id,
|
||||||
@@ -332,7 +334,7 @@ describe('TeamEnvironmentsService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
|
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
|
||||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||||
teamEnvironment,
|
teamEnvironment,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -183,11 +183,10 @@ export class TeamEnvironmentsService {
|
|||||||
*/
|
*/
|
||||||
async createDuplicateEnvironment(id: string) {
|
async createDuplicateEnvironment(id: string) {
|
||||||
try {
|
try {
|
||||||
const environment = await this.prisma.teamEnvironment.findFirst({
|
const environment = await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
rejectOnNotFound: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await this.prisma.teamEnvironment.create({
|
const result = await this.prisma.teamEnvironment.create({
|
||||||
|
|||||||
@@ -142,13 +142,15 @@ describe('UserHistoryService', () => {
|
|||||||
});
|
});
|
||||||
describe('createUserHistory', () => {
|
describe('createUserHistory', () => {
|
||||||
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -158,7 +160,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,13 +174,15 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualRight(userHistory);
|
).toEqualRight(userHistory);
|
||||||
});
|
});
|
||||||
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,7 +192,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,13 +216,15 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||||
});
|
});
|
||||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -228,7 +234,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.GQL,
|
reqType: ReqType.GQL,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -245,13 +251,15 @@ describe('UserHistoryService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -261,7 +269,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -323,13 +331,15 @@ describe('UserHistoryService', () => {
|
|||||||
).toEqualLeft(USER_HISTORY_NOT_FOUND);
|
).toEqualLeft(USER_HISTORY_NOT_FOUND);
|
||||||
});
|
});
|
||||||
test('Should star/unstar a request in the history and publish a updated subscription', async () => {
|
test('Should star/unstar a request in the history and publish a updated subscription', async () => {
|
||||||
|
const executedOn = new Date();
|
||||||
|
|
||||||
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
|
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
|
||||||
userUid: 'abc',
|
userUid: 'abc',
|
||||||
id: '1',
|
id: '1',
|
||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: false,
|
isStarred: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,7 +349,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: [{}],
|
request: [{}],
|
||||||
responseMetadata: [{}],
|
responseMetadata: [{}],
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: true,
|
isStarred: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -349,7 +359,7 @@ describe('UserHistoryService', () => {
|
|||||||
request: JSON.stringify([{}]),
|
request: JSON.stringify([{}]),
|
||||||
responseMetadata: JSON.stringify([{}]),
|
responseMetadata: JSON.stringify([{}]),
|
||||||
reqType: ReqType.REST,
|
reqType: ReqType.REST,
|
||||||
executedOn: new Date(),
|
executedOn,
|
||||||
isStarred: true,
|
isStarred: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/cli",
|
"name": "@hoppscotch/cli",
|
||||||
"version": "0.3.2",
|
"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,26 +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/axios": "^0.14.0",
|
"@types/jest": "^29.5.5",
|
||||||
"@types/chalk": "^2.2.0",
|
"@types/lodash": "^4.14.199",
|
||||||
"@types/commander": "^2.12.2",
|
"@types/qs": "^6.9.8",
|
||||||
"@types/jest": "^27.4.1",
|
|
||||||
"@types/lodash": "^4.14.181",
|
|
||||||
"@types/qs": "^6.9.7",
|
|
||||||
"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.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
|||||||
ERROR_MSG = `Unavailable command: ${error.command}`;
|
ERROR_MSG = `Unavailable command: ${error.command}`;
|
||||||
break;
|
break;
|
||||||
case "MALFORMED_ENV_FILE":
|
case "MALFORMED_ENV_FILE":
|
||||||
|
ERROR_MSG = `The environment file is not of the correct format.`;
|
||||||
|
break;
|
||||||
|
case "BULK_ENV_FILE":
|
||||||
|
ERROR_MSG = `CLI doesn't support bulk environments export.`;
|
||||||
|
break;
|
||||||
case "MALFORMED_COLLECTION":
|
case "MALFORMED_COLLECTION":
|
||||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,27 +1,45 @@
|
|||||||
import { error } from "../../types/errors";
|
import { error } from "../../types/errors";
|
||||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
import {
|
||||||
|
HoppEnvs,
|
||||||
|
HoppEnvPair,
|
||||||
|
HoppEnvKeyPairObject,
|
||||||
|
HoppEnvExportObject,
|
||||||
|
HoppBulkEnvExportObject,
|
||||||
|
} from "../../types/request";
|
||||||
import { readJsonFile } from "../../utils/mutators";
|
import { readJsonFile } from "../../utils/mutators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses env json file for given path and validates the parsed env json object.
|
* Parses env json file for given path and validates the parsed env json object.
|
||||||
* @param path Path of env.json file to be parsed.
|
* @param path Path of env.json file to be parsed.
|
||||||
* @returns For successful parsing we get HoppEnvs object.
|
* @returns For successful parsing we get HoppEnvs object.
|
||||||
*/
|
*/
|
||||||
export async function parseEnvsData(path: string) {
|
export async function parseEnvsData(path: string) {
|
||||||
const contents = await readJsonFile(path)
|
const contents = await readJsonFile(path);
|
||||||
|
const envPairs: Array<HoppEnvPair> = [];
|
||||||
|
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||||
|
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||||
|
const HoppBulkEnvExportObjectResult =
|
||||||
|
HoppBulkEnvExportObject.safeParse(contents);
|
||||||
|
|
||||||
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) {
|
// CLI doesnt support bulk environments export.
|
||||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
// Hence we check for this case and throw an error if it matches the format.
|
||||||
|
if (HoppBulkEnvExportObjectResult.success) {
|
||||||
|
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const envPairs: Array<HoppEnvPair> = []
|
// Checks if the environment file is of the correct format.
|
||||||
|
// If it doesnt match either of them, we throw an error.
|
||||||
for( const [key,value] of Object.entries(contents)) {
|
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||||
if(typeof value !== "string") {
|
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
envPairs.push({key, value})
|
if (HoppEnvKeyPairResult.success) {
|
||||||
|
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||||
|
envPairs.push({ key, value });
|
||||||
}
|
}
|
||||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
} else if (HoppEnvExportObjectResult.success) {
|
||||||
|
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
|
||||||
|
envPairs.push({ key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type HoppErrors = {
|
|||||||
REQUEST_ERROR: HoppErrorData;
|
REQUEST_ERROR: HoppErrorData;
|
||||||
INVALID_ARGUMENT: HoppErrorData;
|
INVALID_ARGUMENT: HoppErrorData;
|
||||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||||
|
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||||
INVALID_FILE_TYPE: HoppErrorData;
|
INVALID_FILE_TYPE: HoppErrorData;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import { TestReport } from "../interfaces/response";
|
import { TestReport } from "../interfaces/response";
|
||||||
import { HoppCLIError } from "./errors";
|
import { HoppCLIError } from "./errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export type FormDataEntry = {
|
export type FormDataEntry = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -9,6 +10,22 @@ export type FormDataEntry = {
|
|||||||
|
|
||||||
export type HoppEnvPair = { key: string; value: string };
|
export type HoppEnvPair = { key: string; value: string };
|
||||||
|
|
||||||
|
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||||
|
|
||||||
|
// Shape of the single environment export object that is exported from the app.
|
||||||
|
export const HoppEnvExportObject = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
variables: z.array(
|
||||||
|
z.object({
|
||||||
|
key: z.string(),
|
||||||
|
value: z.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shape of the bulk environment export object that is exported from the app.
|
||||||
|
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
||||||
|
|
||||||
export type HoppEnvs = {
|
export type HoppEnvs = {
|
||||||
global: HoppEnvPair[];
|
global: HoppEnvPair[];
|
||||||
selected: HoppEnvPair[];
|
selected: HoppEnvPair[];
|
||||||
|
|||||||
@@ -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,7 +66,7 @@ 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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ module.exports = {
|
|||||||
singleQuote: false,
|
singleQuote: false,
|
||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 2
|
tabWidth: 2,
|
||||||
|
plugins: ["prettier-plugin-tailwindcss"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -11,17 +29,25 @@
|
|||||||
@apply antialiased;
|
@apply antialiased;
|
||||||
accent-color: var(--accent-color);
|
accent-color: var(--accent-color);
|
||||||
font-variant-ligatures: common-ligatures;
|
font-variant-ligatures: common-ligatures;
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
--info-color: #ec4899;
|
||||||
|
--success-color: #10b981;
|
||||||
|
--blue-color: #3b82f6;
|
||||||
|
--warning-color: #f59e0b;
|
||||||
|
--cl-error-color: #ef4444;
|
||||||
|
--sv-error-color: #dc2626;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-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;
|
||||||
}
|
}
|
||||||
@@ -54,7 +80,7 @@ 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;
|
||||||
@@ -124,8 +150,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;
|
||||||
@@ -137,7 +163,7 @@ a {
|
|||||||
|
|
||||||
.cm-tooltip {
|
.cm-tooltip {
|
||||||
.tippy-box {
|
.tippy-box {
|
||||||
@apply shadow-none;
|
@apply shadow-none #{!important};
|
||||||
@apply fixed;
|
@apply fixed;
|
||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply -mt-8;
|
@apply -mt-8;
|
||||||
@@ -154,7 +180,7 @@ 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-normal;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@@ -162,7 +188,7 @@ a {
|
|||||||
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 +196,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,7 +227,7 @@ 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-normal;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
@@ -234,7 +266,7 @@ hr {
|
|||||||
|
|
||||||
.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 +275,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 +316,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 +325,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;
|
||||||
@@ -335,23 +367,23 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info-response {
|
.info-response {
|
||||||
@apply text-pink-500;
|
color: var(--info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-response {
|
.success-response {
|
||||||
@apply text-green-500;
|
color: var(--success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redir-response {
|
.redir-response {
|
||||||
@apply text-yellow-500;
|
color: var(--warning-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-error-response {
|
.cl-error-response {
|
||||||
@apply text-red-500;
|
color: var(--cl-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sv-error-response {
|
.sv-error-response {
|
||||||
@apply text-red-600;
|
color: var(--sv-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-data-response {
|
.missing-data-response {
|
||||||
@@ -366,7 +398,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 +426,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 +460,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,22 +533,6 @@ 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;
|
||||||
|
|||||||
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
@@ -0,0 +1,89 @@
|
|||||||
|
@mixin green-theme {
|
||||||
|
--accent-color: #10b981;
|
||||||
|
--accent-light-color: #34d399;
|
||||||
|
--accent-dark-color: #059669;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #a7f3d0;
|
||||||
|
--gradient-via-color: #34d399;
|
||||||
|
--gradient-to-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin teal-theme {
|
||||||
|
--accent-color: #14b8a6;
|
||||||
|
--accent-light-color: #2dd4bf;
|
||||||
|
--accent-dark-color: #0d9488;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #99f6e4;
|
||||||
|
--gradient-via-color: #2dd4bf;
|
||||||
|
--gradient-to-color: #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin blue-theme {
|
||||||
|
--accent-color: #3b82f6;
|
||||||
|
--accent-light-color: #60a5fa;
|
||||||
|
--accent-dark-color: #2563eb;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #bfdbfe;
|
||||||
|
--gradient-via-color: #60a5fa;
|
||||||
|
--gradient-to-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin indigo-theme {
|
||||||
|
--accent-color: #6366f1;
|
||||||
|
--accent-light-color: #818cf8;
|
||||||
|
--accent-dark-color: #4f46e5;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #c7d2fe;
|
||||||
|
--gradient-via-color: #818cf8;
|
||||||
|
--gradient-to-color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin purple-theme {
|
||||||
|
--accent-color: #8b5cf6;
|
||||||
|
--accent-light-color: #a78bfa;
|
||||||
|
--accent-dark-color: #7c3aed;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #ddd6fe;
|
||||||
|
--gradient-via-color: #a78bfa;
|
||||||
|
--gradient-to-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin yellow-theme {
|
||||||
|
--accent-color: #f59e0b;
|
||||||
|
--accent-light-color: #fbbf24;
|
||||||
|
--accent-dark-color: #d97706;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #fde68a;
|
||||||
|
--gradient-via-color: #fbbf24;
|
||||||
|
--gradient-to-color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin orange-theme {
|
||||||
|
--accent-color: #f97316;
|
||||||
|
--accent-light-color: #fb923c;
|
||||||
|
--accent-dark-color: #ea580c;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #fed7aa;
|
||||||
|
--gradient-via-color: #fb923c;
|
||||||
|
--gradient-to-color: #ea580c;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin red-theme {
|
||||||
|
--accent-color: #ef4444;
|
||||||
|
--accent-light-color: #f87171;
|
||||||
|
--accent-dark-color: #dc2626;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #fecaca;
|
||||||
|
--gradient-via-color: #f87171;
|
||||||
|
--gradient-to-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin pink-theme {
|
||||||
|
--accent-color: #ec4899;
|
||||||
|
--accent-light-color: #f472b6;
|
||||||
|
--accent-dark-color: #db2777;
|
||||||
|
--accent-contrast-color: #fff;
|
||||||
|
--gradient-from-color: #fbcfe8;
|
||||||
|
--gradient-via-color: #f472b6;
|
||||||
|
--gradient-to-color: #db2777;
|
||||||
|
}
|
||||||
81
packages/hoppscotch-common/assets/themes/base-themes.scss
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
@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: #181818;
|
||||||
|
--primary-light-color: #1c1c1e;
|
||||||
|
--primary-dark-color: #262626;
|
||||||
|
--primary-contrast-color: #171717;
|
||||||
|
|
||||||
|
--secondary-color: #a3a3a3;
|
||||||
|
--secondary-light-color: #737373;
|
||||||
|
--secondary-dark-color: #fafafa;
|
||||||
|
|
||||||
|
--divider-color: #262626;
|
||||||
|
--divider-light-color: #1f1f1f;
|
||||||
|
--divider-dark-color: #2d2d2d;
|
||||||
|
|
||||||
|
--error-color: #292524;
|
||||||
|
--tooltip-color: #f5f5f5;
|
||||||
|
--popover-color: #1b1b1b;
|
||||||
|
--editor-theme: "merbivore_soft";
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light-theme {
|
||||||
|
--primary-color: #ffffff;
|
||||||
|
--primary-light-color: #f9fafb;
|
||||||
|
--primary-dark-color: #f3f4f6;
|
||||||
|
--primary-contrast-color: #fdfdfd;
|
||||||
|
|
||||||
|
--secondary-color: #6b7280;
|
||||||
|
--secondary-light-color: #9ca3af;
|
||||||
|
--secondary-dark-color: #111827;
|
||||||
|
|
||||||
|
--divider-color: #f3f4f6;
|
||||||
|
--divider-light-color: #f3f4f6;
|
||||||
|
--divider-dark-color: #d1d5db;
|
||||||
|
|
||||||
|
--error-color: #fef3c7;
|
||||||
|
--tooltip-color: #262626;
|
||||||
|
--popover-color: #ffffff;
|
||||||
|
--editor-theme: "textmate";
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin black-theme {
|
||||||
|
--primary-color: #0f0f0f;
|
||||||
|
--primary-light-color: #171717;
|
||||||
|
--primary-dark-color: #181818;
|
||||||
|
--primary-contrast-color: #0f0f0f;
|
||||||
|
|
||||||
|
--secondary-color: #a3a3a3;
|
||||||
|
--secondary-light-color: #737373;
|
||||||
|
--secondary-dark-color: #f5f5f5;
|
||||||
|
|
||||||
|
--divider-color: #1c1c1e;
|
||||||
|
--divider-light-color: #181818;
|
||||||
|
--divider-dark-color: #323232;
|
||||||
|
|
||||||
|
--error-color: #1c1917;
|
||||||
|
--tooltip-color: #f5f5f5;
|
||||||
|
--popover-color: #0f0f0f;
|
||||||
|
--editor-theme: "twilight";
|
||||||
|
}
|
||||||
41
packages/hoppscotch-common/assets/themes/editor-themes.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
@mixin dark-editor-theme {
|
||||||
|
--editor-type-color: #a78bfa;
|
||||||
|
--editor-name-color: #60a5fa;
|
||||||
|
--editor-operator-color: #818cf8;
|
||||||
|
--editor-invalid-color: #f87171;
|
||||||
|
--editor-separator-color: #9ca3af;
|
||||||
|
--editor-meta-color: #9ca3af;
|
||||||
|
--editor-variable-color: #34d399;
|
||||||
|
--editor-link-color: #22d3ee;
|
||||||
|
--editor-process-color: #e879f9;
|
||||||
|
--editor-constant-color: #a78bfa;
|
||||||
|
--editor-keyword-color: #f472b6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin light-editor-theme {
|
||||||
|
--editor-type-color: #7c3aed;
|
||||||
|
--editor-name-color: #dc2626;
|
||||||
|
--editor-operator-color: #4f46e5;
|
||||||
|
--editor-invalid-color: #dc2626;
|
||||||
|
--editor-separator-color: #4b5563;
|
||||||
|
--editor-meta-color: #4b5563;
|
||||||
|
--editor-variable-color: #059669;
|
||||||
|
--editor-link-color: #0891b2;
|
||||||
|
--editor-process-color: #2563eb;
|
||||||
|
--editor-constant-color: #c026d3;
|
||||||
|
--editor-keyword-color: #db2777;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin black-editor-theme {
|
||||||
|
--editor-type-color: #a78bfa;
|
||||||
|
--editor-name-color: #e879f9;
|
||||||
|
--editor-operator-color: #818cf8;
|
||||||
|
--editor-invalid-color: #f87171;
|
||||||
|
--editor-separator-color: #9ca3af;
|
||||||
|
--editor-meta-color: #9ca3af;
|
||||||
|
--editor-variable-color: #34d399;
|
||||||
|
--editor-link-color: #22d3ee;
|
||||||
|
--editor-process-color: #a78bfa;
|
||||||
|
--editor-constant-color: #60a5fa;
|
||||||
|
--editor-keyword-color: #f472b6;
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"action": {
|
"action": {
|
||||||
|
"add": "Add",
|
||||||
"autoscroll": "Autoscroll",
|
"autoscroll": "Autoscroll",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"choose_file": "Choose a file",
|
"choose_file": "Choose a file",
|
||||||
@@ -54,9 +55,28 @@
|
|||||||
"new": "Add new",
|
"new": "Add new",
|
||||||
"star": "Add star"
|
"star": "Add star"
|
||||||
},
|
},
|
||||||
|
"cookies": {
|
||||||
|
"modal": {
|
||||||
|
"new_domain_name": "New domain name",
|
||||||
|
"set": "Set a cookie",
|
||||||
|
"cookie_string": "Cookie string",
|
||||||
|
"enter_cookie_string": "Enter cookie string",
|
||||||
|
"cookie_name": "Name",
|
||||||
|
"cookie_value": "Value",
|
||||||
|
"cookie_path": "Path",
|
||||||
|
"cookie_expires": "Expires",
|
||||||
|
"managed_tab": "Managed",
|
||||||
|
"raw_tab": "Raw",
|
||||||
|
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
|
||||||
|
"empty_domains": "Domain list is empty",
|
||||||
|
"empty_domain": "Domain is empty",
|
||||||
|
"no_cookies_in_domain": "No cookies set for this domain"
|
||||||
|
}
|
||||||
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"chat_with_us": "Chat with us",
|
"chat_with_us": "Chat with us",
|
||||||
"contact_us": "Contact us",
|
"contact_us": "Contact us",
|
||||||
|
"cookies": "Cookies",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copy_user_id": "Copy User Auth Token",
|
"copy_user_id": "Copy User Auth Token",
|
||||||
"developer_option": "Developer options",
|
"developer_option": "Developer options",
|
||||||
@@ -112,18 +132,34 @@
|
|||||||
},
|
},
|
||||||
"authorization": {
|
"authorization": {
|
||||||
"generate_token": "Generate Token",
|
"generate_token": "Generate Token",
|
||||||
|
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||||
"include_in_url": "Include in URL",
|
"include_in_url": "Include in URL",
|
||||||
"learn": "Learn how",
|
"learn": "Learn how",
|
||||||
"pass_key_by": "Pass by",
|
"pass_key_by": "Pass by",
|
||||||
"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",
|
||||||
"different_parent": "Cannot reorder collection with different parent",
|
"different_parent": "Cannot reorder collection with different parent",
|
||||||
"edit": "Edit Collection",
|
"edit": "Edit Collection",
|
||||||
|
"import_or_create": "Import or create a collection",
|
||||||
"invalid_name": "Please provide a name for the collection",
|
"invalid_name": "Please provide a name for the collection",
|
||||||
"invalid_root_move": "Collection already in the root",
|
"invalid_root_move": "Collection already in the root",
|
||||||
"moved": "Moved Successfully",
|
"moved": "Moved Successfully",
|
||||||
@@ -209,6 +245,7 @@
|
|||||||
"empty_variables": "No variables",
|
"empty_variables": "No variables",
|
||||||
"global": "Global",
|
"global": "Global",
|
||||||
"global_variables": "Global variables",
|
"global_variables": "Global variables",
|
||||||
|
"import_or_create": "Import or create a environment",
|
||||||
"invalid_name": "Please provide a name for the environment",
|
"invalid_name": "Please provide a name for the environment",
|
||||||
"list": "Environment variables",
|
"list": "Environment variables",
|
||||||
"my_environments": "My Environments",
|
"my_environments": "My Environments",
|
||||||
@@ -234,6 +271,7 @@
|
|||||||
"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.",
|
||||||
|
"check_how_to_add_origin": "Check how you can add an origin",
|
||||||
"curl_invalid_format": "cURL is not formatted properly",
|
"curl_invalid_format": "cURL is not formatted properly",
|
||||||
"danger_zone": "Danger zone",
|
"danger_zone": "Danger zone",
|
||||||
"delete_account": "Your account is currently an owner in these teams:",
|
"delete_account": "Your account is currently an owner in these teams:",
|
||||||
@@ -249,9 +287,12 @@
|
|||||||
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
|
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
|
||||||
"network_error": "There seems to be a network error. Please try again.",
|
"network_error": "There seems to be a network error. Please try again.",
|
||||||
"network_fail": "Could not send request",
|
"network_fail": "Could not send request",
|
||||||
|
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||||
"no_duration": "No duration",
|
"no_duration": "No duration",
|
||||||
|
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||||
"no_results_found": "No matches found",
|
"no_results_found": "No matches found",
|
||||||
"page_not_found": "This page could not be found",
|
"page_not_found": "This page could not be found",
|
||||||
|
"please_install_extension": "Please install the extension and add origin to the extension.",
|
||||||
"proxy_error": "Proxy error",
|
"proxy_error": "Proxy error",
|
||||||
"script_fail": "Could not execute pre-request script",
|
"script_fail": "Could not execute pre-request script",
|
||||||
"something_went_wrong": "Something went wrong",
|
"something_went_wrong": "Something went wrong",
|
||||||
@@ -291,9 +332,13 @@
|
|||||||
"url": "URL"
|
"url": "URL"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"install_pwa": "Install app",
|
"install_pwa": "Add to Home Screen",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
"save_workspace": "Save My Workspace"
|
"save_workspace": "Save My Workspace",
|
||||||
|
"download_app": "Download app",
|
||||||
|
"menu": "Menu",
|
||||||
|
"go_back": "Go back",
|
||||||
|
"go_forward": "Go forward"
|
||||||
},
|
},
|
||||||
"helpers": {
|
"helpers": {
|
||||||
"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.",
|
||||||
@@ -456,6 +501,8 @@
|
|||||||
"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 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",
|
||||||
@@ -557,6 +604,7 @@
|
|||||||
"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": {
|
"shortcodes": {
|
||||||
@@ -743,9 +791,11 @@
|
|||||||
"disconnected_from": "Disconnected from {name}",
|
"disconnected_from": "Disconnected from {name}",
|
||||||
"docs_generated": "Documentation generated",
|
"docs_generated": "Documentation generated",
|
||||||
"download_started": "Download started",
|
"download_started": "Download started",
|
||||||
|
"download_failed": "Download failed",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"file_imported": "File imported",
|
"file_imported": "File imported",
|
||||||
"finished_in": "Finished in {duration} ms",
|
"finished_in": "Finished in {duration} ms",
|
||||||
|
"hide": "Hide",
|
||||||
"history_deleted": "History deleted",
|
"history_deleted": "History deleted",
|
||||||
"linewrap": "Wrap lines",
|
"linewrap": "Wrap lines",
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
@@ -756,6 +806,7 @@
|
|||||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||||
"published_message": "Published message: {message} to topic: {topic}",
|
"published_message": "Published message: {message} to topic: {topic}",
|
||||||
"reconnection_error": "Failed to reconnect",
|
"reconnection_error": "Failed to reconnect",
|
||||||
|
"show": "Show",
|
||||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||||
@@ -837,7 +888,7 @@
|
|||||||
"new": "New Team",
|
"new": "New Team",
|
||||||
"new_created": "New team created",
|
"new_created": "New team created",
|
||||||
"new_name": "My New Team",
|
"new_name": "My New Team",
|
||||||
"no_access": "You do not have edit access to these collections",
|
"no_access": "You do not have edit access to this team",
|
||||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
"no_invite_found": "Invitation not found. Contact your team owner.",
|
||||||
"no_request_found": "Request not found.",
|
"no_request_found": "Request not found.",
|
||||||
"not_found": "Team not found. Contact your team owner.",
|
"not_found": "Team not found. Contact your team owner.",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"choose_file": "選擇一個檔案",
|
"choose_file": "選擇一個檔案",
|
||||||
"clear": "清除",
|
"clear": "清除",
|
||||||
"clear_all": "全部清除",
|
"clear_all": "全部清除",
|
||||||
"clear_history": "Clear all History",
|
"clear_history": "清除所有歷史記錄",
|
||||||
"close": "關閉",
|
"close": "關閉",
|
||||||
"connect": "連線",
|
"connect": "連線",
|
||||||
"connecting": "正在連接",
|
"connecting": "正在連接",
|
||||||
@@ -79,8 +79,8 @@
|
|||||||
"search": "搜尋",
|
"search": "搜尋",
|
||||||
"share": "分享",
|
"share": "分享",
|
||||||
"shortcuts": "快捷方式",
|
"shortcuts": "快捷方式",
|
||||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
"social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。",
|
||||||
"social_links": "Social links",
|
"social_links": "社群連結",
|
||||||
"spotlight": "聚光燈",
|
"spotlight": "聚光燈",
|
||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
"status_description": "檢查網站狀態",
|
"status_description": "檢查網站狀態",
|
||||||
@@ -135,15 +135,15 @@
|
|||||||
"renamed": "集合已重新命名",
|
"renamed": "集合已重新命名",
|
||||||
"request_in_use": "請求正在使用中",
|
"request_in_use": "請求正在使用中",
|
||||||
"save_as": "另存為",
|
"save_as": "另存為",
|
||||||
"save_to_collection": "Save to Collection",
|
"save_to_collection": "儲存到集合",
|
||||||
"select": "選擇一個集合",
|
"select": "選擇一個集合",
|
||||||
"select_location": "選擇位置",
|
"select_location": "選擇位置",
|
||||||
"select_team": "選擇一個團隊",
|
"select_team": "選擇一個團隊",
|
||||||
"team_collections": "團隊集合"
|
"team_collections": "團隊集合"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
"close_unsaved_tab": "您確定要關閉此分頁嗎?",
|
||||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
"close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。",
|
||||||
"exit_team": "您確定要離開此團隊嗎?",
|
"exit_team": "您確定要離開此團隊嗎?",
|
||||||
"logout": "您確定要登出嗎?",
|
"logout": "您確定要登出嗎?",
|
||||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||||
@@ -158,9 +158,9 @@
|
|||||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||||
},
|
},
|
||||||
"context_menu": {
|
"context_menu": {
|
||||||
"add_parameters": "Add to parameters",
|
"add_parameters": "新增至參數",
|
||||||
"open_request_in_new_tab": "Open request in new tab",
|
"open_request_in_new_tab": "在新分頁開啟請求",
|
||||||
"set_environment_variable": "Set as variable"
|
"set_environment_variable": "設為變數"
|
||||||
},
|
},
|
||||||
"count": {
|
"count": {
|
||||||
"header": "請求標頭 {count}",
|
"header": "請求標頭 {count}",
|
||||||
@@ -204,31 +204,31 @@
|
|||||||
"create_new": "建立新環境",
|
"create_new": "建立新環境",
|
||||||
"created": "已建立環境",
|
"created": "已建立環境",
|
||||||
"deleted": "刪除環境",
|
"deleted": "刪除環境",
|
||||||
"duplicated": "Environment duplicated",
|
"duplicated": "已複製環境",
|
||||||
"edit": "編輯環境",
|
"edit": "編輯環境",
|
||||||
"empty_variables": "No variables",
|
"empty_variables": "無變數",
|
||||||
"global": "Global",
|
"global": "全域",
|
||||||
"global_variables": "Global variables",
|
"global_variables": "全域變數",
|
||||||
"invalid_name": "請提供有效的環境名稱",
|
"invalid_name": "請提供有效的環境名稱",
|
||||||
"list": "Environment variables",
|
"list": "環境變數",
|
||||||
"my_environments": "我的環境",
|
"my_environments": "我的環境",
|
||||||
"name": "Name",
|
"name": "名稱",
|
||||||
"nested_overflow": "巢狀環境變數不得大於 10 層",
|
"nested_overflow": "巢狀環境變數不得大於 10 層",
|
||||||
"new": "建立環境",
|
"new": "建立環境",
|
||||||
"no_active_environment": "No active environment",
|
"no_active_environment": "無使用中的環境",
|
||||||
"no_environment": "無環境",
|
"no_environment": "無環境",
|
||||||
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
|
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
|
||||||
"quick_peek": "Environment Quick Peek",
|
"quick_peek": "快速預覽環境",
|
||||||
"replace_with_variable": "Replace with variable",
|
"replace_with_variable": "以變數替代",
|
||||||
"scope": "Scope",
|
"scope": "範圍",
|
||||||
"select": "選擇環境",
|
"select": "選擇環境",
|
||||||
"set": "Set environment",
|
"set": "設定環境",
|
||||||
"set_as_environment": "Set as environment",
|
"set_as_environment": "設為環境",
|
||||||
"team_environments": "團隊環境",
|
"team_environments": "團隊環境",
|
||||||
"title": "環境",
|
"title": "環境",
|
||||||
"updated": "更新環境",
|
"updated": "更新環境",
|
||||||
"value": "Value",
|
"value": "數值",
|
||||||
"variable": "Variable",
|
"variable": "變數",
|
||||||
"variable_list": "變數列表"
|
"variable_list": "變數列表"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
"no_duration": "無持續時間",
|
"no_duration": "無持續時間",
|
||||||
"no_results_found": "找不到結果",
|
"no_results_found": "找不到結果",
|
||||||
"page_not_found": "找不到此頁面",
|
"page_not_found": "找不到此頁面",
|
||||||
"proxy_error": "Proxy error",
|
"proxy_error": "Proxy 錯誤",
|
||||||
"script_fail": "無法執行預請求指令碼",
|
"script_fail": "無法執行預請求指令碼",
|
||||||
"something_went_wrong": "發生了一些錯誤",
|
"something_went_wrong": "發生了一些錯誤",
|
||||||
"test_script_fail": "無法執行測試指令碼"
|
"test_script_fail": "無法執行測試指令碼"
|
||||||
@@ -278,13 +278,13 @@
|
|||||||
"renamed": "資料夾已重新命名"
|
"renamed": "資料夾已重新命名"
|
||||||
},
|
},
|
||||||
"graphql": {
|
"graphql": {
|
||||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
"connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?",
|
||||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
"connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ",
|
||||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
"connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ",
|
||||||
"mutations": "變體",
|
"mutations": "變體",
|
||||||
"schema": "綱要",
|
"schema": "綱要",
|
||||||
"subscriptions": "訂閱",
|
"subscriptions": "訂閱",
|
||||||
"switch_connection": "Switch connection"
|
"switch_connection": "切換連線"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"time": "時間",
|
"time": "時間",
|
||||||
@@ -339,27 +339,27 @@
|
|||||||
"title": "匯入"
|
"title": "匯入"
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "檢查潛在錯誤",
|
||||||
"environment": {
|
"environment": {
|
||||||
"add_environment": "Add to Environment",
|
"add_environment": "新增至環境",
|
||||||
"not_found": "Environment variable “{environment}” not found."
|
"not_found": "找不到環境變數 “{environment}”。"
|
||||||
},
|
},
|
||||||
"header": {
|
"header": {
|
||||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
"cookie": "瀏覽器不允許 Hoppscotch 設定 Cookie 標頭。在我們推出 Hoppscotch 桌面版前,請先使用 Authorization 標頭。"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"401_error": "Please check your authentication credentials.",
|
"401_error": "請檢查您的授權認證。",
|
||||||
"404_error": "Please check your request URL and method type.",
|
"404_error": "請檢查您的請求網址和方式類型。",
|
||||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
"cors_error": "請檢查您的跨來源資源共用設定。",
|
||||||
"default_error": "Please check your request.",
|
"default_error": "請檢查您的請求。",
|
||||||
"network_error": "Please check your network connection."
|
"network_error": "請檢查您的網路連線。"
|
||||||
},
|
},
|
||||||
"title": "Inspector",
|
"title": "檢查工具",
|
||||||
"url": {
|
"url": {
|
||||||
"extension_not_installed": "Extension not installed.",
|
"extension_not_installed": "未安裝擴充套件。",
|
||||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
"extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。",
|
||||||
"extention_enable_action": "Enable Browser Extension",
|
"extention_enable_action": "啟用瀏覽器擴充套件",
|
||||||
"extention_not_enabled": "Extension not enabled."
|
"extention_not_enabled": "未啟用擴充套件。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"layout": {
|
"layout": {
|
||||||
@@ -472,7 +472,7 @@
|
|||||||
"payload": "負載",
|
"payload": "負載",
|
||||||
"query": "查詢",
|
"query": "查詢",
|
||||||
"raw_body": "原始請求本體",
|
"raw_body": "原始請求本體",
|
||||||
"rename": "Rename Request",
|
"rename": "重新命名請求",
|
||||||
"renamed": "請求已重新命名",
|
"renamed": "請求已重新命名",
|
||||||
"run": "執行",
|
"run": "執行",
|
||||||
"save": "儲存",
|
"save": "儲存",
|
||||||
@@ -510,7 +510,7 @@
|
|||||||
"accent_color": "強調色",
|
"accent_color": "強調色",
|
||||||
"account": "帳號",
|
"account": "帳號",
|
||||||
"account_deleted": "已刪除您的帳號",
|
"account_deleted": "已刪除您的帳號",
|
||||||
"account_description": "自定義您的帳號設定。",
|
"account_description": "自訂您的帳號設定。",
|
||||||
"account_email_description": "您的主要電子郵件地址。",
|
"account_email_description": "您的主要電子郵件地址。",
|
||||||
"account_name_description": "這是您的顯示名稱。",
|
"account_name_description": "這是您的顯示名稱。",
|
||||||
"background": "背景",
|
"background": "背景",
|
||||||
@@ -542,7 +542,7 @@
|
|||||||
"read_the": "閱讀",
|
"read_the": "閱讀",
|
||||||
"reset_default": "重置為預設",
|
"reset_default": "重置為預設",
|
||||||
"short_codes": "快捷碼",
|
"short_codes": "快捷碼",
|
||||||
"short_codes_description": "我們為您打造的快捷碼。",
|
"short_codes_description": "您建立的快捷碼。",
|
||||||
"sidebar_on_left": "左側邊欄",
|
"sidebar_on_left": "左側邊欄",
|
||||||
"sync": "同步",
|
"sync": "同步",
|
||||||
"sync_collections": "集合",
|
"sync_collections": "集合",
|
||||||
@@ -551,9 +551,9 @@
|
|||||||
"sync_history": "歷史",
|
"sync_history": "歷史",
|
||||||
"system_mode": "系統",
|
"system_mode": "系統",
|
||||||
"telemetry": "遙測服務",
|
"telemetry": "遙測服務",
|
||||||
"telemetry_helps_us": "遙測服務幫助我們進行個性化操作,為您提供最佳體驗。",
|
"telemetry_helps_us": "遙測服務能夠幫助我們進行個人化操作,為您提供最佳體驗。",
|
||||||
"theme": "主題",
|
"theme": "主題",
|
||||||
"theme_description": "自定義您的應用程式主題。",
|
"theme_description": "自訂您的應用程式主題。",
|
||||||
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
|
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
|
||||||
"user": "使用者",
|
"user": "使用者",
|
||||||
"verified_email": "已確認電子郵件地址",
|
"verified_email": "已確認電子郵件地址",
|
||||||
@@ -592,26 +592,26 @@
|
|||||||
"title": "導航"
|
"title": "導航"
|
||||||
},
|
},
|
||||||
"others": {
|
"others": {
|
||||||
"prettify": "Prettify Editor's Content",
|
"prettify": "美化編輯器的內容",
|
||||||
"title": "Others"
|
"title": "其他"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"copy_request_link": "複製請求連結",
|
"copy_request_link": "複製請求連結",
|
||||||
"delete_method": "選擇 DELETE 方法",
|
"delete_method": "選擇 DELETE 方法",
|
||||||
"get_method": "選擇 GET 方法",
|
"get_method": "選擇 GET 方法",
|
||||||
"head_method": "選擇 HEAD 方法",
|
"head_method": "選擇 HEAD 方法",
|
||||||
"import_curl": "Import cURL",
|
"import_curl": "匯入 cURL",
|
||||||
"method": "方法",
|
"method": "方法",
|
||||||
"next_method": "選擇下一個方法",
|
"next_method": "選擇下一個方法",
|
||||||
"post_method": "選擇 POST 方法",
|
"post_method": "選擇 POST 方法",
|
||||||
"previous_method": "選擇上一個方法",
|
"previous_method": "選擇上一個方法",
|
||||||
"put_method": "選擇 PUT 方法",
|
"put_method": "選擇 PUT 方法",
|
||||||
"rename": "Rename Request",
|
"rename": "重新命名請求",
|
||||||
"reset_request": "重置請求",
|
"reset_request": "重置請求",
|
||||||
"save_request": "Save Request",
|
"save_request": "儲存請求",
|
||||||
"save_to_collections": "儲存到集合",
|
"save_to_collections": "儲存到集合",
|
||||||
"send_request": "傳送請求",
|
"send_request": "傳送請求",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "產生程式碼片段",
|
||||||
"title": "請求"
|
"title": "請求"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -642,82 +642,82 @@
|
|||||||
"url": "網址"
|
"url": "網址"
|
||||||
},
|
},
|
||||||
"spotlight": {
|
"spotlight": {
|
||||||
"change_language": "Change Language",
|
"change_language": "變更語言",
|
||||||
"environments": {
|
"environments": {
|
||||||
"delete": "Delete current environment",
|
"delete": "刪除目前環境",
|
||||||
"duplicate": "Duplicate current environment",
|
"duplicate": "複製目前環境",
|
||||||
"duplicate_global": "Duplicate global environment",
|
"duplicate_global": "複製全域環境",
|
||||||
"edit": "Edit current environment",
|
"edit": "編輯目前環境",
|
||||||
"edit_global": "Edit global environment",
|
"edit_global": "編輯全域環境",
|
||||||
"new": "Create new environment",
|
"new": "建立新環境",
|
||||||
"new_variable": "Create a new environment variable",
|
"new_variable": "建立新環境變數",
|
||||||
"title": "Environments"
|
"title": "環境"
|
||||||
},
|
},
|
||||||
"general": {
|
"general": {
|
||||||
"chat": "Chat with support",
|
"chat": "與客服對話",
|
||||||
"help_menu": "Help and support",
|
"help_menu": "幫助與支援",
|
||||||
"open_docs": "Read Documentation",
|
"open_docs": "閱讀說明文件",
|
||||||
"open_github": "Open GitHub repository",
|
"open_github": "開啟 GitHub 儲存庫",
|
||||||
"open_keybindings": "Keyboard shortcuts",
|
"open_keybindings": "鍵盤快捷鍵",
|
||||||
"social": "Social",
|
"social": "社交",
|
||||||
"title": "General"
|
"title": "一般"
|
||||||
},
|
},
|
||||||
"graphql": {
|
"graphql": {
|
||||||
"connect": "Connect to server",
|
"connect": "連接至伺服器",
|
||||||
"disconnect": "Disconnect from server"
|
"disconnect": "斷開與伺服器的連線"
|
||||||
},
|
},
|
||||||
"miscellaneous": {
|
"miscellaneous": {
|
||||||
"invite": "Invite your friends to Hoppscotch",
|
"invite": "邀請您的朋友使用 Hoppscotch",
|
||||||
"title": "Miscellaneous"
|
"title": "雜項"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"save_as_new": "Save as new request",
|
"save_as_new": "儲存為新請求",
|
||||||
"select_method": "Select method",
|
"select_method": "選擇方法",
|
||||||
"switch_to": "Switch to",
|
"switch_to": "切換至",
|
||||||
"tab_authorization": "Authorization tab",
|
"tab_authorization": "授權分頁",
|
||||||
"tab_body": "Body tab",
|
"tab_body": "本體分頁",
|
||||||
"tab_headers": "Headers tab",
|
"tab_headers": "標頭分頁",
|
||||||
"tab_parameters": "Parameters tab",
|
"tab_parameters": "參數分頁",
|
||||||
"tab_pre_request_script": "Pre-request script tab",
|
"tab_pre_request_script": "預請求腳本分頁",
|
||||||
"tab_query": "Query tab",
|
"tab_query": "查詢分頁",
|
||||||
"tab_tests": "Tests tab",
|
"tab_tests": "測試分頁",
|
||||||
"tab_variables": "Variables tab"
|
"tab_variables": "變數分頁"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response",
|
"copy": "複製回應",
|
||||||
"download": "Download response as file",
|
"download": "下載回應",
|
||||||
"title": "Response"
|
"title": "回應"
|
||||||
},
|
},
|
||||||
"section": {
|
"section": {
|
||||||
"interceptor": "Interceptor",
|
"interceptor": "攔截器",
|
||||||
"interface": "Interface",
|
"interface": "介面",
|
||||||
"theme": "Theme",
|
"theme": "主題",
|
||||||
"user": "User"
|
"user": "使用者"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"change_interceptor": "Change Interceptor",
|
"change_interceptor": "變更攔截器",
|
||||||
"change_language": "Change Language",
|
"change_language": "變更語言",
|
||||||
"theme": {
|
"theme": {
|
||||||
"black": "Black",
|
"black": "黑色",
|
||||||
"dark": "Dark",
|
"dark": "暗色",
|
||||||
"light": "Light",
|
"light": "亮色",
|
||||||
"system": "System preference"
|
"system": "跟隨系統"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tab": {
|
"tab": {
|
||||||
"close_current": "Close current tab",
|
"close_current": "關閉目前分頁",
|
||||||
"close_others": "Close all other tabs",
|
"close_others": "關閉所有其他分頁",
|
||||||
"duplicate": "Duplicate current tab",
|
"duplicate": "複製目前分頁",
|
||||||
"new_tab": "Open a new tab",
|
"new_tab": "開啟新分頁",
|
||||||
"title": "Tabs"
|
"title": "分頁"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"delete": "Delete current team",
|
"delete": "刪除目前團隊",
|
||||||
"edit": "Edit current team",
|
"edit": "編輯目前團隊",
|
||||||
"invite": "Invite people to team",
|
"invite": "邀請他人加入團隊",
|
||||||
"new": "Create new team",
|
"new": "建立新團隊",
|
||||||
"switch_to_personal": "Switch to your personal workspace",
|
"switch_to_personal": "切換至您的個人工作區",
|
||||||
"title": "Teams"
|
"title": "團隊"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sse": {
|
"sse": {
|
||||||
@@ -777,11 +777,11 @@
|
|||||||
"tab": {
|
"tab": {
|
||||||
"authorization": "授權",
|
"authorization": "授權",
|
||||||
"body": "請求本體",
|
"body": "請求本體",
|
||||||
"close": "Close Tab",
|
"close": "關閉分頁",
|
||||||
"close_others": "Close other Tabs",
|
"close_others": "關閉其他分頁",
|
||||||
"collections": "集合",
|
"collections": "集合",
|
||||||
"documentation": "幫助文件",
|
"documentation": "幫助文件",
|
||||||
"duplicate": "Duplicate Tab",
|
"duplicate": "複製分頁",
|
||||||
"environments": "環境",
|
"environments": "環境",
|
||||||
"headers": "請求標頭",
|
"headers": "請求標頭",
|
||||||
"history": "歷史記錄",
|
"history": "歷史記錄",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@hoppscotch/common",
|
"name": "@hoppscotch/common",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "2023.8.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",
|
||||||
@@ -17,22 +17,22 @@
|
|||||||
"postinstall": "pnpm run gql-codegen",
|
"postinstall": "pnpm run gql-codegen",
|
||||||
"do-test": "pnpm run test",
|
"do-test": "pnpm run test",
|
||||||
"do-lint": "pnpm run prod-lint",
|
"do-lint": "pnpm run prod-lint",
|
||||||
"do-typecheck": "pnpm run lint",
|
"do-typecheck": "node type-check.mjs",
|
||||||
"do-lintfix": "pnpm run lintfix"
|
"do-lintfix": "pnpm run lintfix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.9.0",
|
"@codemirror/autocomplete": "^6.10.2",
|
||||||
"@codemirror/commands": "^6.2.4",
|
"@codemirror/commands": "^6.3.0",
|
||||||
"@codemirror/lang-javascript": "^6.1.9",
|
"@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.0",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.3.3",
|
||||||
"@codemirror/lint": "^6.4.0",
|
"@codemirror/lint": "^6.4.2",
|
||||||
"@codemirror/search": "^6.5.1",
|
"@codemirror/search": "^6.5.4",
|
||||||
"@codemirror/state": "^6.2.1",
|
"@codemirror/state": "^6.3.1",
|
||||||
"@codemirror/view": "^6.16.0",
|
"@codemirror/view": "^6.22.0",
|
||||||
"@fontsource-variable/inter": "^5.0.8",
|
"@fontsource-variable/inter": "^5.0.8",
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
||||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||||
@@ -41,9 +41,7 @@
|
|||||||
"@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.6",
|
"@lezer/highlight": "1.1.4",
|
||||||
"@sentry/tracing": "^7.64.0",
|
|
||||||
"@sentry/vue": "^7.64.0",
|
|
||||||
"@urql/core": "^4.1.1",
|
"@urql/core": "^4.1.1",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
@@ -54,6 +52,7 @@
|
|||||||
"acorn-walk": "^8.2.0",
|
"acorn-walk": "^8.2.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
"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",
|
||||||
@@ -78,6 +77,8 @@
|
|||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"set-cookie-parser": "^2.6.0",
|
||||||
|
"set-cookie-parser-es": "^1.0.5",
|
||||||
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
|
||||||
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
|
||||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
||||||
@@ -91,6 +92,7 @@
|
|||||||
"url": "^0.11.1",
|
"url": "^0.11.1",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
|
"verzod": "^0.2.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue-i18n": "^9.2.2",
|
||||||
"vue-pdf-embed": "^1.1.6",
|
"vue-pdf-embed": "^1.1.6",
|
||||||
@@ -100,7 +102,8 @@
|
|||||||
"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.5.0",
|
||||||
"yargs-parser": "^21.1.1"
|
"yargs-parser": "^21.1.1",
|
||||||
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
@@ -133,30 +136,34 @@
|
|||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@vue/compiler-sfc": "^3.3.4",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
"@vue/runtime-core": "^3.3.4",
|
||||||
|
"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.47.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.17.0",
|
||||||
|
"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",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||||
"sass": "^1.66.0",
|
"sass": "^1.66.0",
|
||||||
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.1.6",
|
||||||
"unplugin-fonts": "^1.0.3",
|
"unplugin-fonts": "^1.0.3",
|
||||||
"unplugin-icons": "^0.16.5",
|
"unplugin-icons": "^0.16.5",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"unplugin-vue-components": "^0.25.1",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.4.9",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"vite-plugin-checker": "^0.6.1",
|
||||||
|
"vite-plugin-fonts": "^0.6.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.38",
|
||||||
"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.16.4",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "^0.8.0",
|
||||||
"vite-plugin-windicss": "^1.9.1",
|
|
||||||
"vitest": "^0.34.2",
|
"vitest": "^0.34.2",
|
||||||
"vue-tsc": "^1.8.8",
|
"vue-tsc": "^1.8.8"
|
||||||
"windicss": "^3.5.6"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
packages/hoppscotch-common/public/badge-dark.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#000" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#fff" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>
|
||||||
|
After Width: | Height: | Size: 386 B |
1
packages/hoppscotch-common/public/badge-light.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="156" height="32" fill="none"><rect width="156" height="32" fill="#fff" rx="4"/><text xmlns="http://www.w3.org/2000/svg" x="50%" y="50%" fill="#000" dominant-baseline="central" font-family="Helvetica,sans-serif" font-size="12" font-weight="bold" text-anchor="middle" text-rendering="geometricPrecision">▶ Run in Hoppscotch</text></svg>
|
||||||
|
After Width: | Height: | Size: 386 B |
|
Before Width: | Height: | Size: 926 KiB After Width: | Height: | Size: 354 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 400 B After Width: | Height: | Size: 624 B |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 871 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 510 KiB After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 535 KiB After Width: | Height: | Size: 385 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 178 KiB |
@@ -1 +1,50 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none"><path fill="#10B981" d="M0 0h512v512H0z"/><circle cx="197.76" cy="157.84" r="10" fill="#fff" fill-opacity=".75"/><circle cx="259.76" cy="161.84" r="12" fill="#fff" fill-opacity=".75"/><circle cx="319.76" cy="177.84" r="10" fill="#fff" fill-opacity=".75"/><path d="M344.963 235.676c2.075-12.698-38.872-29.804-90.967-38.094-52.09-8.296-96.404-4.665-98.48 8.033-.257 1.035 0 1.812.263 2.853-1.298-.521-76.714 211.212-76.714 211.212H364.14s-17.621-181.414-20.211-181.414c.515-.772 1.035-1.549 1.035-2.59Z" fill="url(#a)"/><path d="M314.902 227.386c-1.298 8.033-30.839 9.845-66.343 4.402-35.247-5.7-62.982-16.843-61.684-24.618.521-2.59 3.888-4.665 9.331-5.7-18.141.777-30.062 4.145-31.096 9.845-1.555 10.628 34.726 25.139 81.373 32.657 46.647 7.512 85.782 4.665 87.594-5.7 1.041-6.226-9.33-12.961-26.431-19.439 4.923 2.847 7.513 5.957 7.256 8.553Z" fill="#A7F3D0" fill-opacity=".5"/><path d="M333.557 157.413c-3.104-32.137-27.729-59.351-60.9-64.53-33.172-5.186-64.531 12.954-77.749 42.238 21.251 1.298 44.057 3.631 67.904 7.518 25.396 3.888 49.237 9.074 70.745 14.774Z" fill="url(#b)"/><path d="M74.142 158.002c-2.59 15.808 30.319 35.247 81.894 51.055-.257-1.04-.257-1.818-.257-2.853 2.07-12.698 46.127-16.328 98.48-8.032 52.347 8.29 93.037 25.396 90.961 38.094-.257 1.04-.514 1.818-1.035 2.589 53.645.778 90.968-7.512 93.557-23.32 3.625-24.104-74.638-56.498-174.93-72.306-100.555-15.808-185.045-9.331-188.67 14.773Zm115.586-1.298c.778-4.145 4.665-7.255 8.81-6.477 4.145.777 7.256 4.665 6.478 8.81-.52 4.145-4.665 6.998-8.81 6.478-4.145-.778-7.255-4.666-6.478-8.811Zm59.866 4.145c.777-5.7 6.22-9.587 11.92-8.547 5.7.778 9.588 6.215 8.553 11.921-1.041 5.442-6.478 9.33-11.92 8.553-5.706-.778-9.594-6.221-8.553-11.927Zm62.975 15.294c.778-4.145 4.665-7.255 8.81-6.478 4.145.778 7.255 4.666 6.478 8.811-.515 4.145-4.665 7.255-8.81 6.477-4.145-.777-7.256-4.665-6.478-8.81Z" fill="url(#c)"/><defs><radialGradient id="b" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(0 32.7063 -69.3245 0 264.232 124.706)"><stop stop-color="#047857"/><stop offset="1" stop-color="#064E3B"/></radialGradient><radialGradient id="c" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(255.837 186.754) scale(1389.61)"><stop stop-color="#047857"/><stop offset=".115" stop-color="#064E3B"/></radialGradient><linearGradient id="a" x1="224.998" y1="157.606" x2="224.998" y2="403.696" gradientUnits="userSpaceOnUse"><stop stop-color="#86EFAC" stop-opacity=".75"/><stop offset=".635" stop-color="#fff" stop-opacity=".2"/><stop offset="1" stop-color="#fff" stop-opacity="0"/></linearGradient></defs></svg>
|
<svg width="824" height="824" viewBox="0 0 824 824" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="824" height="824" rx="184" fill="#08110F"/>
|
||||||
|
<rect width="824" height="824" rx="184" fill="url(#paint0_radial_0_21)" fill-opacity="0.5"/>
|
||||||
|
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint1_linear_0_21)"/>
|
||||||
|
<path d="M435.425 463.217C429.441 476.657 411.033 481.515 394.309 474.07C377.585 466.624 368.879 449.693 374.863 436.253C380.846 422.813 399.254 417.954 415.978 425.4C432.702 432.846 441.409 449.777 435.425 463.217Z" fill="url(#paint2_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||||
|
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint3_linear_0_21)"/>
|
||||||
|
<path d="M535.563 521.172C553.071 526.191 570.536 518.856 574.571 504.789C578.606 490.722 567.684 475.251 550.175 470.232C532.666 465.213 515.201 472.548 511.166 486.615C507.131 500.682 518.054 516.153 535.563 521.172Z" fill="url(#paint4_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||||
|
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint5_linear_0_21)"/>
|
||||||
|
<path d="M292.782 355.633C308.227 365.286 314.462 383.173 306.709 395.584C298.955 407.995 280.149 410.231 264.704 400.578C249.258 390.924 243.023 373.037 250.777 360.626C258.53 348.215 277.337 345.98 292.782 355.633Z" fill="url(#paint6_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint7_linear_0_21)"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M502.355 231.325C581.373 266.506 632.095 343.263 634.119 429.03C680.633 465.639 726.858 516.883 705.36 565.168C681.25 619.319 595.382 617.091 497.781 589.689C450.767 615.718 392.444 620.168 339.689 596.68C286.934 573.192 251.229 526.908 239.1 474.517C153.428 420.321 94.3151 357.999 118.425 303.847C139.923 255.562 208.935 255.626 267.265 265.697C332.356 209.81 423.338 196.144 502.355 231.325ZM159.38 322.082C147.667 348.389 210.578 423.052 382.845 499.751C555.111 576.449 652.693 573.241 664.405 546.934C674.099 525.16 634.213 483.308 588.537 450.878C553.009 425.484 504.344 397.494 440.864 369.231C423.586 361.538 416.839 341.008 424.104 324.691C431.369 308.374 447.329 297.463 480.93 295.91C496.747 295.862 498.823 291.476 499.546 287.716C500.442 281.915 492.401 276.002 484.108 272.31C418.17 242.953 337.453 255.265 281.503 314.178C226.84 301.933 169.074 300.309 159.38 322.082Z" fill="url(#paint8_radial_0_21)" style="mix-blend-mode:soft-light"/>
|
||||||
|
<defs>
|
||||||
|
<radialGradient id="paint0_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(814.524 12.36) rotate(125.613) scale(1089.59 1210.34)">
|
||||||
|
<stop stop-color="#00D196" stop-opacity="0.5"/>
|
||||||
|
<stop offset="0.996771" stop-color="#00D196" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint1_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#00D196"/>
|
||||||
|
<stop offset="1" stop-color="#00B381"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint2_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint3_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#00D196"/>
|
||||||
|
<stop offset="1" stop-color="#00B381"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint4_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint5_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#00D196"/>
|
||||||
|
<stop offset="1" stop-color="#00B381"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint6_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
<linearGradient id="paint7_linear_0_21" x1="411.893" y1="212" x2="411.893" y2="612" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#00D196"/>
|
||||||
|
<stop offset="1" stop-color="#00B381"/>
|
||||||
|
</linearGradient>
|
||||||
|
<radialGradient id="paint8_radial_0_21" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(644.721 344.481) rotate(159.984) scale(631.37 385.135)">
|
||||||
|
<stop stop-color="white"/>
|
||||||
|
<stop offset="1" stop-color="white" stop-opacity="0"/>
|
||||||
|
</radialGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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']
|
||||||
@@ -58,6 +59,8 @@ declare module 'vue' {
|
|||||||
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
|
||||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
|
||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
|
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||||
|
CookiesEditCookie: typeof import('./components/cookies/EditCookie.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']
|
||||||
@@ -140,6 +143,7 @@ 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']
|
||||||
@@ -156,7 +160,7 @@ declare module 'vue' {
|
|||||||
IconLucideRss: typeof import('~icons/lucide/rss')['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']
|
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']
|
||||||
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
|
||||||
@@ -189,7 +193,6 @@ declare module 'vue' {
|
|||||||
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
|
||||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
|
||||||
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
|
||||||
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
|
|
||||||
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
|
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
|
||||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
|
||||||
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']
|
||||||
@@ -203,6 +206,7 @@ declare module 'vue' {
|
|||||||
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']
|
||||||
|
|||||||
@@ -2,53 +2,16 @@
|
|||||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
|
||||||
:show="confirmRemove"
|
|
||||||
:title="t('confirm.remove_team')"
|
|
||||||
@hide-modal="confirmRemove = false"
|
|
||||||
@resolve="deleteTeam()"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { pipe } from "fp-ts/function"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
|
||||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { useI18n } from "~/composables/i18n"
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const showShortcuts = ref(false)
|
const showShortcuts = ref(false)
|
||||||
const showShare = ref(false)
|
const showShare = ref(false)
|
||||||
const showLogin = ref(false)
|
const showLogin = ref(false)
|
||||||
|
|
||||||
const confirmRemove = ref(false)
|
|
||||||
|
|
||||||
const teamID = ref<string | null>(null)
|
|
||||||
|
|
||||||
const deleteTeam = () => {
|
|
||||||
if (!teamID.value) return
|
|
||||||
pipe(
|
|
||||||
backendDeleteTeam(teamID.value),
|
|
||||||
TE.match(
|
|
||||||
(err) => {
|
|
||||||
// TODO: Better errors ? We know the possible errors now
|
|
||||||
toast.error(`${t("error.something_went_wrong")}`)
|
|
||||||
console.error(err)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
invokeAction("workspace.switch.personal")
|
|
||||||
toast.success(`${t("team.deleted")}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
|
||||||
}
|
|
||||||
|
|
||||||
defineActionHandler("flyouts.keybinds.toggle", () => {
|
defineActionHandler("flyouts.keybinds.toggle", () => {
|
||||||
showShortcuts.value = !showShortcuts.value
|
showShortcuts.value = !showShortcuts.value
|
||||||
})
|
})
|
||||||
@@ -60,9 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
|
|||||||
defineActionHandler("modals.login.toggle", () => {
|
defineActionHandler("modals.login.toggle", () => {
|
||||||
showLogin.value = !showLogin.value
|
showLogin.value = !showLogin.value
|
||||||
})
|
})
|
||||||
|
|
||||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
|
||||||
teamID.value = teamId
|
|
||||||
confirmRemove.value = true
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
56
packages/hoppscotch-common/src/components/app/Banner.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:role="bannerRole"
|
||||||
|
class="flex items-center px-4 py-2 text-tiny"
|
||||||
|
:class="bannerColor"
|
||||||
|
>
|
||||||
|
<component :is="bannerIcon" class="mr-2 text-white" />
|
||||||
|
|
||||||
|
<span class="text-white">
|
||||||
|
<span v-if="banner.alternateText" class="md:hidden">
|
||||||
|
{{ banner.alternateText(t) }}
|
||||||
|
</span>
|
||||||
|
<span :class="banner.alternateText ? '<md:hidden' : ''">
|
||||||
|
{{ banner.text(t) }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</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 = defineProps<{
|
||||||
|
banner: BannerContent
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const ariaRoles: Record<BannerType, string> = {
|
||||||
|
error: "alert",
|
||||||
|
warning: "status",
|
||||||
|
info: "status",
|
||||||
|
}
|
||||||
|
|
||||||
|
const bgColors: Record<BannerType, string> = {
|
||||||
|
error: "bg-red-700",
|
||||||
|
warning: "bg-yellow-700",
|
||||||
|
info: "bg-stone-800",
|
||||||
|
}
|
||||||
|
|
||||||
|
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">
|
||||||
|
|||||||
@@ -20,6 +20,12 @@
|
|||||||
<AppInterceptor />
|
<AppInterceptor />
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
|
||||||
|
:label="t('app.cookies')"
|
||||||
|
:icon="IconCookie"
|
||||||
|
@click="showCookiesModal = true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<tippy
|
<tippy
|
||||||
@@ -175,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,
|
||||||
}"
|
}"
|
||||||
@@ -195,12 +201,17 @@
|
|||||||
:show="showDeveloperOptions"
|
:show="showDeveloperOptions"
|
||||||
@hide-modal="showDeveloperOptions = false"
|
@hide-modal="showDeveloperOptions = false"
|
||||||
/>
|
/>
|
||||||
|
<CookiesAllModal
|
||||||
|
:show="showCookiesModal"
|
||||||
|
@hide-modal="showCookiesModal = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { version } from "~/../package.json"
|
import { version } from "~/../package.json"
|
||||||
|
import IconCookie from "~icons/lucide/cookie"
|
||||||
import IconSidebar from "~icons/lucide/sidebar"
|
import IconSidebar from "~icons/lucide/sidebar"
|
||||||
import IconZap from "~icons/lucide/zap"
|
import IconZap from "~icons/lucide/zap"
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
|
|||||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
const showDeveloperOptions = ref(false)
|
const showDeveloperOptions = ref(false)
|
||||||
|
const showCookiesModal = ref(false)
|
||||||
|
|
||||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||||
const SIDEBAR = useSetting("SIDEBAR")
|
const SIDEBAR = useSetting("SIDEBAR")
|
||||||
|
|||||||
@@ -1,28 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<header
|
<header
|
||||||
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
|
ref="headerRef"
|
||||||
|
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
|
||||||
|
@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,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
<div class="flex">
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.menu')"
|
||||||
|
:icon="IconMenu"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
:label="t('app.name')"
|
:label="t('app.name')"
|
||||||
to="/"
|
to="/"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center justify-center flex-1 space-x-2">
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.go_back')"
|
||||||
|
:icon="IconArrowLeft"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="router.back()"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.go_forward')"
|
||||||
|
:icon="IconArrowRight"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="router.forward()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<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 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">
|
||||||
@@ -30,14 +56,9 @@
|
|||||||
<kbd class="shortcut-key">K</kbd>
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<HoppButtonSecondary
|
</div>
|
||||||
v-if="showInstallButton"
|
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
<div class="flex">
|
||||||
:title="t('header.install_pwa')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="installPWA()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
:title="`${
|
:title="`${
|
||||||
@@ -47,8 +68,43 @@
|
|||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
@click="invokeAction('modals.support.toggle')"
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
|
<span>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => downloadActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('header.download_app')"
|
||||||
|
:icon="IconDownload"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="downloadActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
@click="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('header.download_app')"
|
||||||
|
:icon="IconDownload"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-if="showInstallButton"
|
||||||
|
:label="t('header.install_pwa')"
|
||||||
|
:icon="IconPlusSquare"
|
||||||
|
@click="installPWA()"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
<div
|
<div
|
||||||
v-if="currentUser === null"
|
v-if="currentUser === null"
|
||||||
class="inline-flex items-center space-x-2"
|
class="inline-flex items-center space-x-2"
|
||||||
@@ -56,11 +112,12 @@
|
|||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconUploadCloud"
|
:icon="IconUploadCloud"
|
||||||
:label="t('header.save_workspace')"
|
:label="t('header.save_workspace')"
|
||||||
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"
|
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"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonPrimary
|
<HoppButtonPrimary
|
||||||
:label="t('header.login')"
|
:label="t('header.login')"
|
||||||
|
class="h-8"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,13 +134,13 @@
|
|||||||
@handle-click="handleTeamEdit()"
|
@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"
|
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"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('team.invite_tooltip')"
|
:title="t('team.invite_tooltip')"
|
||||||
:icon="IconUserPlus"
|
:icon="IconUserPlus"
|
||||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
@click="handleInvite()"
|
@click="handleInvite()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
@@ -95,7 +152,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="t('team.edit')"
|
:title="t('team.edit')"
|
||||||
:icon="IconSettings"
|
:icon="IconSettings"
|
||||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
@click="handleTeamEdit()"
|
@click="handleTeamEdit()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,7 +167,7 @@
|
|||||||
:title="t('workspace.change')"
|
:title="t('workspace.change')"
|
||||||
:label="mdAndLarger ? workspaceName : ``"
|
:label="mdAndLarger ? workspaceName : ``"
|
||||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||||
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"
|
class="select-wrapper !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"
|
||||||
/>
|
/>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
@@ -148,7 +205,7 @@
|
|||||||
"
|
"
|
||||||
indicator
|
indicator
|
||||||
:indicator-styles="
|
:indicator-styles="
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartPicture
|
<HoppSmartPicture
|
||||||
@@ -162,7 +219,7 @@
|
|||||||
:initial="currentUser.displayName || currentUser.email"
|
:initial="currentUser.displayName || currentUser.email"
|
||||||
indicator
|
indicator
|
||||||
:indicator-styles="
|
:indicator-styles="
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
@@ -176,7 +233,7 @@
|
|||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col px-2 text-tiny">
|
<div class="flex flex-col px-2 text-tiny">
|
||||||
<span class="inline-flex font-semibold truncate">
|
<span class="inline-flex truncate font-semibold">
|
||||||
{{
|
{{
|
||||||
currentUser.displayName ||
|
currentUser.displayName ||
|
||||||
t("profile.default_hopp_displayname")
|
t("profile.default_hopp_displayname")
|
||||||
@@ -214,8 +271,9 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<AppAnnouncement v-if="!network.isOnline" />
|
<AppBanner v-if="bannerContent" :banner="bannerContent" />
|
||||||
<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"
|
||||||
@@ -231,32 +289,52 @@
|
|||||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||||
@refetch-teams="refetchTeams"
|
@refetch-teams="refetchTeams"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="confirmRemove"
|
||||||
|
:title="t('confirm.remove_team')"
|
||||||
|
@hide-modal="confirmRemove = false"
|
||||||
|
@resolve="deleteTeam"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, reactive, ref, watch } from "vue"
|
|
||||||
import IconUser from "~icons/lucide/user"
|
|
||||||
import IconUsers from "~icons/lucide/users"
|
|
||||||
import IconSettings from "~icons/lucide/settings"
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
|
||||||
import IconUserPlus from "~icons/lucide/user-plus"
|
|
||||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
|
||||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
|
||||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { useReadonlyStream } from "@composables/stream"
|
||||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
|
||||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
|
||||||
import { useToast } from "~/composables/toast"
|
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
|
import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
|
||||||
|
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||||
|
import { computed, reactive, ref, watch } from "vue"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql"
|
||||||
|
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import IconDownload from "~icons/lucide/download"
|
||||||
|
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||||
|
import IconSettings from "~icons/lucide/settings"
|
||||||
|
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||||
|
import IconUser from "~icons/lucide/user"
|
||||||
|
import IconUserPlus from "~icons/lucide/user-plus"
|
||||||
|
import IconUsers from "~icons/lucide/users"
|
||||||
|
import IconPlusSquare from "~icons/lucide/plus-square"
|
||||||
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
import IconArrowRight from "~icons/lucide/arrow-right"
|
||||||
|
import IconMenu from "~icons/lucide/align-left"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||||
|
import {
|
||||||
|
BannerService,
|
||||||
|
BannerContent,
|
||||||
|
BANNER_PRIORITY_HIGH,
|
||||||
|
} from "~/services/banner.service"
|
||||||
|
import { useRouter } from "vue-router"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Once the PWA code is initialized, this holds a method
|
* Once the PWA code is initialized, this holds a method
|
||||||
@@ -271,13 +349,40 @@ 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: "info",
|
||||||
|
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 currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getProbableUserStream(),
|
platform.auth.getProbableUserStream(),
|
||||||
platform.auth.getProbableUser()
|
platform.auth.getProbableUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const confirmRemove = ref(false)
|
||||||
|
const teamID = ref<string | null>(null)
|
||||||
|
|
||||||
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
||||||
|
|
||||||
// TeamList-Adapter
|
// TeamList-Adapter
|
||||||
@@ -377,12 +482,31 @@ const handleTeamEdit = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const deleteTeam = () => {
|
||||||
|
if (!teamID.value) return
|
||||||
|
pipe(
|
||||||
|
backendDeleteTeam(teamID.value),
|
||||||
|
TE.match(
|
||||||
|
(err) => {
|
||||||
|
// TODO: Better errors ? We know the possible errors now
|
||||||
|
toast.error(`${t("error.something_went_wrong")}`)
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
invokeAction("workspace.switch.personal")
|
||||||
|
toast.success(`${t("team.deleted")}`)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||||
|
}
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
const profile = ref<any | null>(null)
|
const profile = ref<any | null>(null)
|
||||||
const settings = ref<any | null>(null)
|
const settings = ref<any | null>(null)
|
||||||
const logout = ref<any | null>(null)
|
const logout = ref<any | null>(null)
|
||||||
const accountActions = ref<any | null>(null)
|
const accountActions = ref<any | null>(null)
|
||||||
|
const downloadActions = ref<any | null>(null)
|
||||||
|
|
||||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
defineActionHandler("modals.team.edit", handleTeamEdit)
|
||||||
|
|
||||||
@@ -405,6 +529,12 @@ defineActionHandler(
|
|||||||
computed(() => !currentUser.value)
|
computed(() => !currentUser.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||||
|
if (selectedTeam.value?.myRole !== TeamMemberRole.Owner) return noPermission()
|
||||||
|
teamID.value = teamId
|
||||||
|
confirmRemove.value = true
|
||||||
|
})
|
||||||
|
|
||||||
const noPermission = () => {
|
const noPermission = () => {
|
||||||
toast.error(`${t("profile.no_permission")}`)
|
toast.error(`${t("profile.no_permission")}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,7 +9,7 @@
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="entry.icon"
|
:is="entry.icon"
|
||||||
class="opacity-50 svg-icons"
|
class="svg-icons opacity-50"
|
||||||
:class="{ 'opacity-100': active }"
|
:class="{ 'opacity-100': active }"
|
||||||
/>
|
/>
|
||||||
<template
|
<template
|
||||||
@@ -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 py-5 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>
|
||||||
|
|||||||
@@ -37,7 +37,8 @@
|
|||||||
import { ref, watch } from "vue"
|
import { ref, watch } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
import { useService } from "dioc/vue"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
@@ -60,11 +61,12 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const editingName = ref("")
|
const editingName = ref("")
|
||||||
|
|
||||||
|
const tabs = useService(RESTTabService)
|
||||||
watch(
|
watch(
|
||||||
() => props.show,
|
() => props.show,
|
||||||
(show) => {
|
(show) => {
|
||||||
if (show) {
|
if (show) {
|
||||||
editingName.value = currentActiveTab.value.document.request.name
|
editingName.value = tabs.currentActiveTab.value.document.request.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||