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": {
|
||||
"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
|
||||
REDIRECT_URL="http://localhost:3000"
|
||||
WHITELISTED_ORIGINS = "http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||
VITE_ALLOWED_AUTH_PROVIDERS = GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
WHITELISTED_ORIGINS="http://localhost:3170,http://localhost:3000,http://localhost:3100"
|
||||
VITE_ALLOWED_AUTH_PROVIDERS=GOOGLE,GITHUB,MICROSOFT,EMAIL
|
||||
|
||||
# Google Auth Config
|
||||
GOOGLE_CLIENT_ID="************************************************"
|
||||
@@ -59,3 +59,6 @@ VITE_BACKEND_API_URL=http://localhost:3170/v1
|
||||
# Terms Of Service And Privacy Policy Links (Optional)
|
||||
VITE_APP_TOS_LINK=https://docs.hoppscotch.io/support/terms
|
||||
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
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/ui.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
# Deploy the ui site with netlify-cli
|
||||
- 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:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
caddyProcess.on("exit", (code) => {
|
||||
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
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)
|
||||
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch?connect_timeout=300
|
||||
- PORT=3170
|
||||
- PORT=8080
|
||||
volumes:
|
||||
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
|
||||
# - ./packages/hoppscotch-backend/:/usr/src/app
|
||||
@@ -26,6 +26,7 @@ services:
|
||||
hoppscotch-db:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "3180:80"
|
||||
- "3170:3170"
|
||||
|
||||
# The main hoppscotch app. This will be hosted at port 3000
|
||||
@@ -42,7 +43,8 @@ services:
|
||||
depends_on:
|
||||
- hoppscotch-backend
|
||||
ports:
|
||||
- "3000:8080"
|
||||
- "3080:80"
|
||||
- "3000:3000"
|
||||
|
||||
# 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
|
||||
@@ -58,7 +60,8 @@ services:
|
||||
depends_on:
|
||||
- hoppscotch-backend
|
||||
ports:
|
||||
- "3100:8080"
|
||||
- "3280:80"
|
||||
- "3100:3100"
|
||||
|
||||
# The service that spins up all 3 services at once in one container
|
||||
hoppscotch-aio:
|
||||
@@ -76,6 +79,7 @@ services:
|
||||
- "3000:3000"
|
||||
- "3100:3100"
|
||||
- "3170:3170"
|
||||
- "3080:80"
|
||||
|
||||
# The preset DB service, you can delete/comment the below lines if
|
||||
# you are using an external postgres instance
|
||||
|
||||
10
package.json
@@ -22,16 +22,14 @@
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@types/node": "^17.0.24",
|
||||
"@types/node": "17.0.27",
|
||||
"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": {
|
||||
"packageExtensions": {
|
||||
|
||||
@@ -17,16 +17,16 @@
|
||||
"types": "dist/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"dependencies": {
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@lezer/lr": "^1.3.10"
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@lezer/lr": "^1.3.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.0",
|
||||
"@lezer/generator": "^1.5.1",
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-dts": "^4.2.1",
|
||||
"rollup-plugin-ts": "^2.0.7",
|
||||
"typescript": "^4.6.3"
|
||||
"rollup": "^3.29.3",
|
||||
"rollup-plugin-dts": "^6.0.2",
|
||||
"rollup-plugin-ts": "^3.4.5",
|
||||
"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",
|
||||
"version": "2023.8.1",
|
||||
"version": "2023.8.4-1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -24,18 +24,17 @@
|
||||
"do-test": "pnpm run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.8.1",
|
||||
"@nestjs/apollo": "^10.1.6",
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/graphql": "^10.1.6",
|
||||
"@nestjs/jwt": "^10.0.1",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
"@nestjs/throttler": "^4.0.0",
|
||||
"@apollo/server": "^4.9.4",
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/apollo": "^12.0.9",
|
||||
"@nestjs/common": "^10.2.6",
|
||||
"@nestjs/core": "^10.2.6",
|
||||
"@nestjs/graphql": "^12.0.9",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.2.6",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"apollo-server-express": "^3.11.1",
|
||||
"apollo-server-plugin-base": "^3.7.1",
|
||||
"argon2": "^0.30.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
@@ -43,9 +42,9 @@
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.3",
|
||||
"fp-ts": "^2.13.1",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-query-complexity": "^0.12.0",
|
||||
"graphql-redis-subscriptions": "^2.5.0",
|
||||
"graphql-redis-subscriptions": "^2.6.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"io-ts": "^2.2.16",
|
||||
@@ -63,9 +62,9 @@
|
||||
"rxjs": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@nestjs/testing": "^9.2.1",
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.6",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@types/argon2": "^0.15.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 {
|
||||
id String @id
|
||||
request Json
|
||||
creatorUid String?
|
||||
createdOn DateTime @default(now())
|
||||
|
||||
id String @id @unique
|
||||
request Json
|
||||
embedProperties Json?
|
||||
creatorUid String?
|
||||
User User? @relation(fields: [creatorUid], references: [uid])
|
||||
createdOn DateTime @default(now())
|
||||
updatedOn DateTime @updatedAt @default(now())
|
||||
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
|
||||
}
|
||||
|
||||
@@ -102,6 +104,7 @@ model User {
|
||||
currentGQLSession Json?
|
||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||
invitedUsers InvitedUsers[]
|
||||
shortcodes Shortcode[]
|
||||
}
|
||||
|
||||
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()
|
||||
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 { TeamCollectionModule } from '../team-collection/team-collection.module';
|
||||
import { TeamRequestModule } from '../team-request/team-request.module';
|
||||
import { InfraResolver } from './infra.resolver';
|
||||
import { ShortcodeModule } from 'src/shortcode/shortcode.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -22,8 +24,9 @@ import { TeamRequestModule } from '../team-request/team-request.module';
|
||||
TeamEnvironmentsModule,
|
||||
TeamCollectionModule,
|
||||
TeamRequestModule,
|
||||
ShortcodeModule,
|
||||
],
|
||||
providers: [AdminResolver, AdminService],
|
||||
providers: [InfraResolver, AdminResolver, AdminService],
|
||||
exports: [AdminService],
|
||||
})
|
||||
export class AdminModule {}
|
||||
|
||||
@@ -21,15 +21,15 @@ import { InvitedUser } from './invited-user.model';
|
||||
import { GqlUser } from '../decorators/gql-user.decorator';
|
||||
import { PubSubService } from '../pubsub/pubsub.service';
|
||||
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 {
|
||||
AddUserToTeamArgs,
|
||||
ChangeUserRoleInTeamArgs,
|
||||
} from './input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
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)
|
||||
@Resolver(() => Admin)
|
||||
@@ -51,6 +51,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all admin users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async admins() {
|
||||
@@ -59,6 +60,7 @@ export class AdminResolver {
|
||||
}
|
||||
@ResolveField(() => User, {
|
||||
description: 'Returns a user info by UID',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async userInfo(
|
||||
@@ -76,6 +78,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => [User], {
|
||||
description: 'Returns a list of all the users in infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard, GqlAdminGuard)
|
||||
async allUsers(
|
||||
@@ -88,6 +91,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => [InvitedUser], {
|
||||
description: 'Returns a list of all the invited users',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async invitedUsers(@Parent() admin: Admin): Promise<InvitedUser[]> {
|
||||
const users = await this.adminService.fetchInvitedUsers();
|
||||
@@ -96,6 +100,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => [Team], {
|
||||
description: 'Returns a list of all the teams in the infra',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async allTeams(
|
||||
@Parent() admin: Admin,
|
||||
@@ -106,6 +111,7 @@ export class AdminResolver {
|
||||
}
|
||||
@ResolveField(() => Team, {
|
||||
description: 'Returns a team info by ID when requested by Admin',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamInfo(
|
||||
@Parent() admin: Admin,
|
||||
@@ -123,6 +129,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the members in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async membersCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@@ -140,6 +147,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored collections in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async collectionCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@@ -155,6 +163,7 @@ export class AdminResolver {
|
||||
}
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored requests in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async requestCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@@ -171,6 +180,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return count of all the stored environments in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async environmentCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@@ -187,6 +197,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => [TeamInvitation], {
|
||||
description: 'Return all the pending invitations in a team',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async pendingInvitationCountInTeam(
|
||||
@Parent() admin: Admin,
|
||||
@@ -205,6 +216,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Users in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async usersCount() {
|
||||
return this.adminService.getUsersCount();
|
||||
@@ -212,6 +224,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Teams in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamsCount() {
|
||||
return this.adminService.getTeamsCount();
|
||||
@@ -219,6 +232,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Collections in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamCollectionsCount() {
|
||||
return this.adminService.getTeamCollectionsCount();
|
||||
@@ -226,6 +240,7 @@ export class AdminResolver {
|
||||
|
||||
@ResolveField(() => Number, {
|
||||
description: 'Return total number of Team Requests in organization',
|
||||
deprecationReason: 'Use `infra` query instead',
|
||||
})
|
||||
async teamRequestsCount() {
|
||||
return this.adminService.getTeamRequestsCount();
|
||||
@@ -428,6 +443,23 @@ export class AdminResolver {
|
||||
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 */
|
||||
|
||||
@Subscription(() => InvitedUser, {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
INVALID_EMAIL,
|
||||
USER_ALREADY_INVITED,
|
||||
} from '../errors';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
@@ -25,6 +26,7 @@ const mockTeamRequestService = mockDeep<TeamRequestService>();
|
||||
const mockTeamInvitationService = mockDeep<TeamInvitationService>();
|
||||
const mockTeamCollectionService = mockDeep<TeamCollectionService>();
|
||||
const mockMailerService = mockDeep<MailerService>();
|
||||
const mockShortcodeService = mockDeep<ShortcodeService>();
|
||||
|
||||
const adminService = new AdminService(
|
||||
mockUserService,
|
||||
@@ -36,6 +38,7 @@ const adminService = new AdminService(
|
||||
mockPubSub as any,
|
||||
mockPrisma as any,
|
||||
mockMailerService,
|
||||
mockShortcodeService,
|
||||
);
|
||||
|
||||
const invitedUsers: InvitedUsers[] = [
|
||||
|
||||
@@ -24,6 +24,7 @@ import { TeamRequestService } from '../team-request/team-request.service';
|
||||
import { TeamEnvironmentsService } from '../team-environments/team-environments.service';
|
||||
import { TeamInvitationService } from '../team-invitation/team-invitation.service';
|
||||
import { TeamMemberRole } from '../team/team.model';
|
||||
import { ShortcodeService } from 'src/shortcode/shortcode.service';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
@@ -37,6 +38,7 @@ export class AdminService {
|
||||
private readonly pubsub: PubSubService,
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly mailerService: MailerService,
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -74,7 +76,7 @@ export class AdminService {
|
||||
|
||||
try {
|
||||
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
|
||||
template: 'code-your-own',
|
||||
template: 'user-invitation',
|
||||
variables: {
|
||||
inviteeEmail: inviteeEmail,
|
||||
magicLink: `${process.env.VITE_BASE_URL}`,
|
||||
@@ -432,4 +434,35 @@ export class AdminService {
|
||||
|
||||
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: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
credentials: true,
|
||||
},
|
||||
playground: process.env.PRODUCTION !== 'true',
|
||||
debug: process.env.PRODUCTION !== 'true',
|
||||
autoSchemaFile: true,
|
||||
installSubscriptionHandlers: true,
|
||||
subscriptions: {
|
||||
@@ -62,10 +57,12 @@ import { AppController } from './app.controller';
|
||||
}),
|
||||
driver: ApolloDriver,
|
||||
}),
|
||||
ThrottlerModule.forRoot({
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
},
|
||||
]),
|
||||
UserModule,
|
||||
AuthModule,
|
||||
AdminModule,
|
||||
|
||||
@@ -229,7 +229,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
await this.mailerService.sendEmail(email, {
|
||||
template: 'code-your-own',
|
||||
template: 'user-invitation',
|
||||
variables: {
|
||||
inviteeEmail: email,
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* (TeamEnvironmentsService)
|
||||
@@ -621,3 +609,24 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
|
||||
*/
|
||||
export const MAILER_FROM_ADDRESS_UNDEFINED =
|
||||
'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 { UserHistoryUserResolver } from './user-history/user.resolver';
|
||||
import { UserSettingsUserResolver } from './user-settings/user.resolver';
|
||||
import { InfraResolver } from './admin/infra.resolver';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const RESOLVERS = [
|
||||
InfraResolver,
|
||||
AdminResolver,
|
||||
ShortcodeResolver,
|
||||
TeamResolver,
|
||||
@@ -93,9 +95,7 @@ export async function emitGQLSchemaFile() {
|
||||
numberScalarMode: 'integer',
|
||||
});
|
||||
|
||||
const schemaString = printSchema(schema, {
|
||||
commentDescriptions: true,
|
||||
});
|
||||
const schemaString = printSchema(schema);
|
||||
|
||||
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
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
|
||||
// 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 = {
|
||||
template: 'code-your-own';
|
||||
template: 'user-invitation';
|
||||
variables: {
|
||||
inviteeEmail: string;
|
||||
magicLink: string;
|
||||
@@ -16,7 +16,7 @@ export type UserMagicLinkMailDescription = {
|
||||
};
|
||||
|
||||
export type AdminUserInvitationMailDescription = {
|
||||
template: 'code-your-own';
|
||||
template: 'user-invitation';
|
||||
variables: {
|
||||
inviteeEmail: string;
|
||||
magicLink: string;
|
||||
|
||||
@@ -27,7 +27,7 @@ export class MailerService {
|
||||
case 'team-invitation':
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
-->
|
||||
<style type="text/css" rel="stylesheet" media="all">
|
||||
/* Base ------------------------------ */
|
||||
|
||||
|
||||
@import url("https://fonts.googleapis.com/css?family=Nunito+Sans:400,700&display=swap");
|
||||
body {
|
||||
width: 100% !important;
|
||||
@@ -22,19 +22,19 @@
|
||||
margin: 0;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
color: #3869D4;
|
||||
}
|
||||
|
||||
|
||||
a img {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
td {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
||||
.preheader {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
@@ -47,13 +47,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
/* Type ------------------------------ */
|
||||
|
||||
|
||||
body,
|
||||
td,
|
||||
th {
|
||||
font-family: "Nunito Sans", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -61,7 +61,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -69,7 +69,7 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
color: #333333;
|
||||
@@ -77,12 +77,12 @@
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
td,
|
||||
th {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
@@ -91,25 +91,25 @@
|
||||
font-size: 16px;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
|
||||
p.sub {
|
||||
font-size: 13px;
|
||||
}
|
||||
/* Utilities ------------------------------ */
|
||||
|
||||
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
/* Buttons ------------------------------ */
|
||||
|
||||
|
||||
.button {
|
||||
background-color: #3869D4;
|
||||
border-top: 10px solid #3869D4;
|
||||
@@ -124,7 +124,7 @@
|
||||
-webkit-text-size-adjust: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
.button--green {
|
||||
background-color: #22BC66;
|
||||
border-top: 10px solid #22BC66;
|
||||
@@ -132,7 +132,7 @@
|
||||
border-bottom: 10px solid #22BC66;
|
||||
border-left: 18px solid #22BC66;
|
||||
}
|
||||
|
||||
|
||||
.button--red {
|
||||
background-color: #FF6136;
|
||||
border-top: 10px solid #FF6136;
|
||||
@@ -140,7 +140,7 @@
|
||||
border-bottom: 10px solid #FF6136;
|
||||
border-left: 18px solid #FF6136;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 500px) {
|
||||
.button {
|
||||
width: 100% !important;
|
||||
@@ -148,21 +148,21 @@
|
||||
}
|
||||
}
|
||||
/* Attribute list ------------------------------ */
|
||||
|
||||
|
||||
.attributes {
|
||||
margin: 0 0 21px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_content {
|
||||
background-color: #F4F4F7;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
|
||||
.attributes_item {
|
||||
padding: 0;
|
||||
}
|
||||
/* Related Items ------------------------------ */
|
||||
|
||||
|
||||
.related {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -171,31 +171,31 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item {
|
||||
padding: 10px 0;
|
||||
color: #CBCCCF;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.related_item-title {
|
||||
display: block;
|
||||
margin: .5em 0 0;
|
||||
}
|
||||
|
||||
|
||||
.related_item-thumb {
|
||||
display: block;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
.related_heading {
|
||||
border-top: 1px solid #CBCCCF;
|
||||
text-align: center;
|
||||
padding: 25px 0 10px;
|
||||
}
|
||||
/* Discount Code ------------------------------ */
|
||||
|
||||
|
||||
.discount {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -206,33 +206,33 @@
|
||||
background-color: #F4F4F7;
|
||||
border: 2px dashed #CBCCCF;
|
||||
}
|
||||
|
||||
|
||||
.discount_heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.discount_body {
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
}
|
||||
/* Social Icons ------------------------------ */
|
||||
|
||||
|
||||
.social {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social td {
|
||||
padding: 0;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
.social_icon {
|
||||
height: 20px;
|
||||
margin: 0 8px 10px 8px;
|
||||
padding: 0;
|
||||
}
|
||||
/* Data table ------------------------------ */
|
||||
|
||||
|
||||
.purchase {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -241,7 +241,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -250,50 +250,50 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.purchase_item {
|
||||
padding: 10px 0;
|
||||
color: #51545E;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading {
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_heading p {
|
||||
margin: 0;
|
||||
color: #85878E;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
|
||||
.purchase_footer {
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
|
||||
.purchase_total--label {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
background-color: #F2F4F6;
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
p {
|
||||
color: #51545E;
|
||||
}
|
||||
|
||||
|
||||
.email-wrapper {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -303,7 +303,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #F2F4F6;
|
||||
}
|
||||
|
||||
|
||||
.email-content {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -313,16 +313,16 @@
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
/* Masthead ----------------------- */
|
||||
|
||||
|
||||
.email-masthead {
|
||||
padding: 25px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_logo {
|
||||
width: 94px;
|
||||
}
|
||||
|
||||
|
||||
.email-masthead_name {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
@@ -331,7 +331,7 @@
|
||||
text-shadow: 0 1px 0 white;
|
||||
}
|
||||
/* Body ------------------------------ */
|
||||
|
||||
|
||||
.email-body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
@@ -340,7 +340,7 @@
|
||||
-premailer-cellpadding: 0;
|
||||
-premailer-cellspacing: 0;
|
||||
}
|
||||
|
||||
|
||||
.email-body_inner {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -350,7 +350,7 @@
|
||||
-premailer-cellspacing: 0;
|
||||
background-color: #FFFFFF;
|
||||
}
|
||||
|
||||
|
||||
.email-footer {
|
||||
width: 570px;
|
||||
margin: 0 auto;
|
||||
@@ -360,11 +360,11 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.email-footer p {
|
||||
color: #A8AAAF;
|
||||
}
|
||||
|
||||
|
||||
.body-action {
|
||||
width: 100%;
|
||||
margin: 30px auto;
|
||||
@@ -374,25 +374,25 @@
|
||||
-premailer-cellspacing: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.body-sub {
|
||||
margin-top: 25px;
|
||||
padding-top: 25px;
|
||||
border-top: 1px solid #EAEAEC;
|
||||
}
|
||||
|
||||
|
||||
.content-cell {
|
||||
padding: 45px;
|
||||
}
|
||||
/*Media Queries ------------------------------ */
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-body_inner,
|
||||
.email-footer {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body,
|
||||
.email-body,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GraphQLSchemaHost } from '@nestjs/graphql';
|
||||
import {
|
||||
ApolloServerPlugin,
|
||||
BaseContext,
|
||||
GraphQLRequestListener,
|
||||
} from 'apollo-server-plugin-base';
|
||||
} from '@apollo/server';
|
||||
import { Plugin } from '@nestjs/apollo';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import {
|
||||
@@ -17,7 +18,7 @@ const COMPLEXITY_LIMIT = 50;
|
||||
export class GQLComplexityPlugin implements ApolloServerPlugin {
|
||||
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
|
||||
|
||||
async requestDidStart(): Promise<GraphQLRequestListener> {
|
||||
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
|
||||
const { schema } = this.gqlSchemaHost;
|
||||
|
||||
return {
|
||||
|
||||
@@ -69,5 +69,7 @@ export type TopicDef = {
|
||||
[topic: `team_req/${string}/req_deleted`]: string;
|
||||
[topic: `team/${string}/invite_added`]: TeamInvitation;
|
||||
[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 { User } from 'src/user/user.model';
|
||||
|
||||
@ObjectType()
|
||||
export class Shortcode {
|
||||
@Field(() => ID, {
|
||||
description: 'The shortcode. 12 digit alphanumeric.',
|
||||
description: 'The 12 digit alphanumeric code',
|
||||
})
|
||||
id: string;
|
||||
|
||||
@@ -12,8 +13,57 @@ export class Shortcode {
|
||||
})
|
||||
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;
|
||||
}
|
||||
|
||||
@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 { JwtModule } from '@nestjs/jwt';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { PubSubModule } from 'src/pubsub/pubsub.module';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
@@ -7,14 +6,7 @@ import { ShortcodeResolver } from './shortcode.resolver';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
PrismaModule,
|
||||
UserModule,
|
||||
PubSubModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
}),
|
||||
],
|
||||
imports: [PrismaModule, UserModule, PubSubModule],
|
||||
providers: [ShortcodeService, ShortcodeResolver],
|
||||
exports: [ShortcodeService],
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
Args,
|
||||
Context,
|
||||
ID,
|
||||
Mutation,
|
||||
Query,
|
||||
@@ -9,28 +8,25 @@ import {
|
||||
} from '@nestjs/graphql';
|
||||
import * as E from 'fp-ts/Either';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Shortcode } from './shortcode.model';
|
||||
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { throwErr } from 'src/utils';
|
||||
import { GqlUser } from 'src/decorators/gql-user.decorator';
|
||||
import { GqlAuthGuard } from 'src/guards/gql-auth.guard';
|
||||
import { User } from 'src/user/user.model';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { AuthUser } from '../types/AuthUser';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { PaginationArgs } from 'src/types/input-types.args';
|
||||
import { GqlThrottlerGuard } from 'src/guards/gql-throttler.guard';
|
||||
import { SkipThrottle } from '@nestjs/throttler';
|
||||
import { GqlAdminGuard } from 'src/admin/guards/gql-admin.guard';
|
||||
|
||||
@UseGuards(GqlThrottlerGuard)
|
||||
@Resolver(() => Shortcode)
|
||||
export class ShortcodeResolver {
|
||||
constructor(
|
||||
private readonly shortcodeService: ShortcodeService,
|
||||
private readonly userService: UserService,
|
||||
private readonly pubsub: PubSubService,
|
||||
private jwtService: JwtService,
|
||||
) {}
|
||||
|
||||
/* Queries */
|
||||
@@ -64,20 +60,53 @@ export class ShortcodeResolver {
|
||||
@Mutation(() => Shortcode, {
|
||||
description: 'Create a shortcode for the given request.',
|
||||
})
|
||||
@UseGuards(GqlAuthGuard)
|
||||
async createShortcode(
|
||||
@GqlUser() user: AuthUser,
|
||||
@Args({
|
||||
name: 'request',
|
||||
description: 'JSON string of the request object',
|
||||
})
|
||||
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(
|
||||
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);
|
||||
@@ -93,7 +122,7 @@ export class ShortcodeResolver {
|
||||
@Args({
|
||||
name: 'code',
|
||||
type: () => ID,
|
||||
description: 'The shortcode to resolve',
|
||||
description: 'The shortcode to remove',
|
||||
})
|
||||
code: string,
|
||||
) {
|
||||
@@ -114,6 +143,16 @@ export class ShortcodeResolver {
|
||||
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, {
|
||||
description: 'Listen for shortcode deletion',
|
||||
resolve: (value) => value,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
SHORTCODE_ALREADY_EXISTS,
|
||||
SHORTCODE_INVALID_JSON,
|
||||
INVALID_EMAIL,
|
||||
SHORTCODE_INVALID_PROPERTIES_JSON,
|
||||
SHORTCODE_INVALID_REQUEST_JSON,
|
||||
SHORTCODE_NOT_FOUND,
|
||||
SHORTCODE_PROPERTIES_NOT_FOUND,
|
||||
} from 'src/errors';
|
||||
import { Shortcode } from './shortcode.model';
|
||||
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||
import { ShortcodeService } from './shortcode.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
|
||||
@@ -22,7 +25,7 @@ const mockFB = {
|
||||
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
|
||||
// @ts-ignore
|
||||
@@ -38,18 +41,34 @@ beforeEach(() => {
|
||||
});
|
||||
const createdOn = new Date();
|
||||
|
||||
const shortCodeWithOutUser = {
|
||||
id: '123',
|
||||
request: '{}',
|
||||
const user: AuthUser = {
|
||||
uid: '123344',
|
||||
email: 'dwight@dundermifflin.com',
|
||||
displayName: 'Dwight Schrute',
|
||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||
isAdmin: false,
|
||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||
createdOn: createdOn,
|
||||
creatorUid: null,
|
||||
currentGQLSession: {},
|
||||
currentRESTSession: {},
|
||||
};
|
||||
|
||||
const shortCodeWithUser = {
|
||||
const mockEmbed = {
|
||||
id: '123',
|
||||
request: '{}',
|
||||
embedProperties: '{}',
|
||||
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 = [
|
||||
@@ -58,33 +77,67 @@ const shortcodes = [
|
||||
request: {
|
||||
hello: 'there',
|
||||
},
|
||||
creatorUid: 'testuser',
|
||||
embedProperties: {
|
||||
foo: 'bar',
|
||||
},
|
||||
creatorUid: user.uid,
|
||||
createdOn: new Date(),
|
||||
updatedOn: createdOn,
|
||||
},
|
||||
{
|
||||
id: 'blablabla1',
|
||||
request: {
|
||||
hello: 'there',
|
||||
},
|
||||
creatorUid: 'testuser',
|
||||
embedProperties: {
|
||||
foo: 'bar',
|
||||
},
|
||||
creatorUid: user.uid,
|
||||
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('getShortCode', () => {
|
||||
test('should return a valid shortcode with valid shortcode ID', async () => {
|
||||
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(
|
||||
shortCodeWithOutUser,
|
||||
);
|
||||
test('should return a valid Shortcode with valid Shortcode ID', async () => {
|
||||
mockPrisma.shortcode.findFirstOrThrow.mockResolvedValueOnce(mockEmbed);
|
||||
|
||||
const result = await shortcodeService.getShortCode(
|
||||
shortCodeWithOutUser.id,
|
||||
);
|
||||
const result = await shortcodeService.getShortCode(mockEmbed.id);
|
||||
expect(result).toEqualRight(<Shortcode>{
|
||||
id: shortCodeWithOutUser.id,
|
||||
createdOn: shortCodeWithOutUser.createdOn,
|
||||
request: JSON.stringify(shortCodeWithOutUser.request),
|
||||
id: mockEmbed.id,
|
||||
createdOn: mockEmbed.createdOn,
|
||||
request: JSON.stringify(mockEmbed.request),
|
||||
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,10 +152,10 @@ describe('ShortcodeService', () => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
||||
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||
cursor: null,
|
||||
take: 10,
|
||||
});
|
||||
@@ -110,20 +163,22 @@ describe('ShortcodeService', () => {
|
||||
{
|
||||
id: shortcodes[0].id,
|
||||
request: JSON.stringify(shortcodes[0].request),
|
||||
properties: JSON.stringify(shortcodes[0].embedProperties),
|
||||
createdOn: shortcodes[0].createdOn,
|
||||
},
|
||||
{
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||
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]]);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
||||
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||
cursor: 'blablabla',
|
||||
take: 10,
|
||||
});
|
||||
@@ -131,6 +186,7 @@ describe('ShortcodeService', () => {
|
||||
{
|
||||
id: shortcodes[1].id,
|
||||
request: JSON.stringify(shortcodes[1].request),
|
||||
properties: JSON.stringify(shortcodes[1].embedProperties),
|
||||
createdOn: shortcodes[1].createdOn,
|
||||
},
|
||||
]);
|
||||
@@ -139,7 +195,7 @@ describe('ShortcodeService', () => {
|
||||
test('should return an empty array for an invalid cursor', async () => {
|
||||
mockPrisma.shortcode.findMany.mockResolvedValue([]);
|
||||
|
||||
const result = await shortcodeService.fetchUserShortCodes('testuser', {
|
||||
const result = await shortcodeService.fetchUserShortCodes(user.uid, {
|
||||
cursor: 'invalidcursor',
|
||||
take: 10,
|
||||
});
|
||||
@@ -171,77 +227,111 @@ describe('ShortcodeService', () => {
|
||||
});
|
||||
|
||||
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(
|
||||
'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 () => {
|
||||
// generateUniqueShortCodeID --> getShortCode
|
||||
test('should throw SHORTCODE_INVALID_PROPERTIES_JSON error if incoming properties data is invalid', async () => {
|
||||
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(
|
||||
'NotFoundError',
|
||||
);
|
||||
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
|
||||
mockPrisma.shortcode.create.mockResolvedValueOnce(mockEmbed);
|
||||
|
||||
const result = await shortcodeService.createShortcode('{}', 'user_uid_1');
|
||||
expect(result).toEqualRight({
|
||||
id: shortCodeWithUser.id,
|
||||
createdOn: shortCodeWithUser.createdOn,
|
||||
request: JSON.stringify(shortCodeWithUser.request),
|
||||
const result = await shortcodeService.createShortcode('{}', '{}', user);
|
||||
expect(result).toEqualRight(<Shortcode>{
|
||||
id: mockEmbed.id,
|
||||
createdOn: mockEmbed.createdOn,
|
||||
request: JSON.stringify(mockEmbed.request),
|
||||
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||
});
|
||||
});
|
||||
|
||||
test('should successfully create a new shortcode with null user uid', async () => {
|
||||
// generateUniqueShortCodeID --> getShortCode
|
||||
test('should successfully create a new ShortCode with valid user uid', async () => {
|
||||
// generateUniqueShortCodeID --> getShortcode
|
||||
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||
'NotFoundError',
|
||||
);
|
||||
mockPrisma.shortcode.create.mockResolvedValueOnce(shortCodeWithUser);
|
||||
mockPrisma.shortcode.create.mockResolvedValueOnce(mockShortcode);
|
||||
|
||||
const result = await shortcodeService.createShortcode('{}', null);
|
||||
expect(result).toEqualRight({
|
||||
id: shortCodeWithUser.id,
|
||||
createdOn: shortCodeWithUser.createdOn,
|
||||
request: JSON.stringify(shortCodeWithOutUser.request),
|
||||
const result = await shortcodeService.createShortcode('{}', null, user);
|
||||
expect(result).toEqualRight(<Shortcode>{
|
||||
id: mockShortcode.id,
|
||||
createdOn: mockShortcode.createdOn,
|
||||
request: JSON.stringify(mockShortcode.request),
|
||||
properties: mockShortcode.embedProperties,
|
||||
});
|
||||
});
|
||||
|
||||
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of shortcode', async () => {
|
||||
// generateUniqueShortCodeID --> getShortCode
|
||||
test('should send pubsub message to `shortcode/{uid}/created` on successful creation of a Shortcode', async () => {
|
||||
// generateUniqueShortCodeID --> getShortcode
|
||||
mockPrisma.shortcode.findFirstOrThrow.mockRejectedValueOnce(
|
||||
'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(
|
||||
`shortcode/${shortCodeWithUser.creatorUid}/created`,
|
||||
{
|
||||
id: shortCodeWithUser.id,
|
||||
createdOn: shortCodeWithUser.createdOn,
|
||||
request: JSON.stringify(shortCodeWithUser.request),
|
||||
`shortcode/${mockShortcode.creatorUid}/created`,
|
||||
<Shortcode>{
|
||||
id: mockShortcode.id,
|
||||
createdOn: mockShortcode.createdOn,
|
||||
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', () => {
|
||||
test('should return true on successful deletion of shortcode with valid inputs', async () => {
|
||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
|
||||
test('should return true on successful deletion of Shortcode with valid inputs', async () => {
|
||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
|
||||
|
||||
const result = await shortcodeService.revokeShortCode(
|
||||
shortCodeWithUser.id,
|
||||
shortCodeWithUser.creatorUid,
|
||||
mockEmbed.id,
|
||||
mockEmbed.creatorUid,
|
||||
);
|
||||
|
||||
expect(mockPrisma.shortcode.delete).toHaveBeenCalledWith({
|
||||
where: {
|
||||
creator_uid_shortcode_unique: {
|
||||
creatorUid: shortCodeWithUser.creatorUid,
|
||||
id: shortCodeWithUser.id,
|
||||
creatorUid: mockEmbed.creatorUid,
|
||||
id: mockEmbed.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -249,52 +339,53 @@ describe('ShortcodeService', () => {
|
||||
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');
|
||||
expect(
|
||||
shortcodeService.revokeShortCode('invalid', 'testuser'),
|
||||
).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');
|
||||
expect(
|
||||
shortcodeService.revokeShortCode('blablablabla', 'invalidUser'),
|
||||
).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');
|
||||
expect(
|
||||
shortcodeService.revokeShortCode('invalid', 'invalid'),
|
||||
).resolves.toEqualLeft(SHORTCODE_NOT_FOUND);
|
||||
});
|
||||
|
||||
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of shortcode', async () => {
|
||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(shortCodeWithUser);
|
||||
test('should send pubsub message to `shortcode/{uid}/revoked` on successful deletion of Shortcode', async () => {
|
||||
mockPrisma.shortcode.delete.mockResolvedValueOnce(mockEmbed);
|
||||
|
||||
const result = await shortcodeService.revokeShortCode(
|
||||
shortCodeWithUser.id,
|
||||
shortCodeWithUser.creatorUid,
|
||||
mockEmbed.id,
|
||||
mockEmbed.creatorUid,
|
||||
);
|
||||
|
||||
expect(mockPubSub.publish).toHaveBeenCalledWith(
|
||||
`shortcode/${shortCodeWithUser.creatorUid}/revoked`,
|
||||
`shortcode/${mockEmbed.creatorUid}/revoked`,
|
||||
{
|
||||
id: shortCodeWithUser.id,
|
||||
createdOn: shortCodeWithUser.createdOn,
|
||||
request: JSON.stringify(shortCodeWithUser.request),
|
||||
id: mockEmbed.id,
|
||||
createdOn: mockEmbed.createdOn,
|
||||
request: JSON.stringify(mockEmbed.request),
|
||||
properties: JSON.stringify(mockEmbed.embedProperties),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
const result = await shortcodeService.deleteUserShortCodes(
|
||||
shortCodeWithUser.creatorUid,
|
||||
mockEmbed.creatorUid,
|
||||
);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
@@ -303,9 +394,176 @@ describe('ShortcodeService', () => {
|
||||
mockPrisma.shortcode.deleteMany.mockResolvedValueOnce({ count: 0 });
|
||||
|
||||
const result = await shortcodeService.deleteUserShortCodes(
|
||||
shortCodeWithUser.creatorUid,
|
||||
mockEmbed.creatorUid,
|
||||
);
|
||||
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 * as T from 'fp-ts/Task';
|
||||
import * as O from 'fp-ts/Option';
|
||||
import * as TO from 'fp-ts/TaskOption';
|
||||
import * as E from 'fp-ts/Either';
|
||||
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 { Shortcode } from './shortcode.model';
|
||||
import { Shortcode, ShortcodeWithUserEmail } from './shortcode.model';
|
||||
import { Shortcode as DBShortCode } from '@prisma/client';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
@@ -46,10 +50,14 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
* @param shortcodeInfo Prisma Shortcode type
|
||||
* @returns GQL Shortcode
|
||||
*/
|
||||
private returnShortCode(shortcodeInfo: DBShortCode): Shortcode {
|
||||
private cast(shortcodeInfo: DBShortCode): Shortcode {
|
||||
return <Shortcode>{
|
||||
id: shortcodeInfo.id,
|
||||
request: JSON.stringify(shortcodeInfo.request),
|
||||
properties:
|
||||
shortcodeInfo.embedProperties != null
|
||||
? JSON.stringify(shortcodeInfo.embedProperties)
|
||||
: null,
|
||||
createdOn: shortcodeInfo.createdOn,
|
||||
};
|
||||
}
|
||||
@@ -94,7 +102,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
const shortcodeInfo = await this.prisma.shortcode.findFirstOrThrow({
|
||||
where: { id: shortcode },
|
||||
});
|
||||
return E.right(this.returnShortCode(shortcodeInfo));
|
||||
return E.right(this.cast(shortcodeInfo));
|
||||
} catch (error) {
|
||||
return E.left(SHORTCODE_NOT_FOUND);
|
||||
}
|
||||
@@ -104,14 +112,22 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
* Create a new ShortCode
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
async createShortcode(request: string, userUID: string | null) {
|
||||
const shortcodeData = stringToJson(request);
|
||||
if (E.isLeft(shortcodeData)) return E.left(SHORTCODE_INVALID_JSON);
|
||||
async createShortcode(
|
||||
request: string,
|
||||
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();
|
||||
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({
|
||||
data: {
|
||||
id: generatedShortCode.right,
|
||||
request: shortcodeData.right,
|
||||
creatorUid: O.isNone(user) ? null : user.value.uid,
|
||||
request: requestData.right,
|
||||
embedProperties: parsedProperties.right ?? undefined,
|
||||
creatorUid: userInfo.uid,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -128,11 +145,11 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
if (createdShortCode.creatorUid) {
|
||||
this.pubsub.publish(
|
||||
`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) =>
|
||||
this.returnShortCode(code),
|
||||
this.cast(code),
|
||||
);
|
||||
|
||||
return fetchedShortCodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a ShortCode
|
||||
* Delete a ShortCode created by User of uid
|
||||
*
|
||||
* @param shortcode ShortCode
|
||||
* @param uid User Uid
|
||||
@@ -182,7 +199,7 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
|
||||
this.pubsub.publish(
|
||||
`shortcode/${deletedShortCodes.creatorUid}/revoked`,
|
||||
this.returnShortCode(deletedShortCodes),
|
||||
this.cast(deletedShortCodes),
|
||||
);
|
||||
|
||||
return E.right(true);
|
||||
@@ -205,4 +222,118 @@ export class ShortcodeService implements UserDataHandler, OnModuleInit {
|
||||
|
||||
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 { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
TEAM_COLL_DEST_SAME,
|
||||
TEAM_COLL_INVALID_JSON,
|
||||
@@ -17,9 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
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 mockPubSub = mockDeep<PubSubService>();
|
||||
|
||||
@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
describe('createDuplicateEnvironment', () => {
|
||||
test('should successfully duplicate an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
@@ -322,7 +322,9 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
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(
|
||||
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 () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
|
||||
@@ -183,11 +183,10 @@ export class TeamEnvironmentsService {
|
||||
*/
|
||||
async createDuplicateEnvironment(id: string) {
|
||||
try {
|
||||
const environment = await this.prisma.teamEnvironment.findFirst({
|
||||
const environment = await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
rejectOnNotFound: true,
|
||||
});
|
||||
|
||||
const result = await this.prisma.teamEnvironment.create({
|
||||
|
||||
@@ -142,13 +142,15 @@ describe('UserHistoryService', () => {
|
||||
});
|
||||
describe('createUserHistory', () => {
|
||||
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({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -158,7 +160,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -172,13 +174,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualRight(userHistory);
|
||||
});
|
||||
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({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -188,7 +192,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -212,13 +216,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||
});
|
||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -228,7 +234,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -245,13 +251,15 @@ describe('UserHistoryService', () => {
|
||||
);
|
||||
});
|
||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -261,7 +269,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -323,13 +331,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualLeft(USER_HISTORY_NOT_FOUND);
|
||||
});
|
||||
test('Should star/unstar a request in the history and publish a updated subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -339,7 +349,7 @@ describe('UserHistoryService', () => {
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: true,
|
||||
});
|
||||
|
||||
@@ -349,7 +359,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.3.2",
|
||||
"version": "0.4.0",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
@@ -10,6 +10,9 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "pnpm exec tsup",
|
||||
"dev": "pnpm exec tsup --watch",
|
||||
@@ -38,26 +41,24 @@
|
||||
"devDependencies": {
|
||||
"@hoppscotch/data": "workspace:^",
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@swc/core": "^1.2.181",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/lodash": "^4.14.181",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@relmify/jest-fp-ts": "^2.1.1",
|
||||
"@swc/core": "^1.3.92",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/lodash": "^4.14.199",
|
||||
"@types/qs": "^6.9.8",
|
||||
"axios": "^0.21.4",
|
||||
"chalk": "^4.1.1",
|
||||
"commander": "^8.0.0",
|
||||
"chalk": "^4.1.2",
|
||||
"commander": "^11.0.0",
|
||||
"esm": "^3.2.25",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"jest": "^27.5.1",
|
||||
"fp-ts": "^2.16.1",
|
||||
"io-ts": "^2.2.20",
|
||||
"jest": "^29.7.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "^2.8.4",
|
||||
"qs": "^6.10.3",
|
||||
"ts-jest": "^27.1.4",
|
||||
"tsup": "^5.12.7",
|
||||
"typescript": "^4.6.4"
|
||||
"prettier": "^3.0.3",
|
||||
"qs": "^6.11.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tsup": "^7.2.0",
|
||||
"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}`;
|
||||
break;
|
||||
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":
|
||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||
break;
|
||||
@@ -82,4 +87,4 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
console.error(ERROR_CODE, ERROR_MSG);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,27 +1,45 @@
|
||||
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";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
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))) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||
// CLI doesnt support bulk environments export.
|
||||
// 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.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
for( const [key,value] of Object.entries(contents)) {
|
||||
if(typeof value !== "string") {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
|
||||
envPairs.push({key, value})
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ type HoppErrors = {
|
||||
REQUEST_ERROR: HoppErrorData;
|
||||
INVALID_ARGUMENT: HoppErrorData;
|
||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
INVALID_FILE_TYPE: HoppErrorData;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
@@ -9,6 +10,22 @@ export type FormDataEntry = {
|
||||
|
||||
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 = {
|
||||
global: HoppEnvPair[];
|
||||
selected: HoppEnvPair[];
|
||||
|
||||
@@ -57,7 +57,7 @@ module.exports = {
|
||||
{
|
||||
name: "localStorage",
|
||||
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
|
||||
@@ -66,7 +66,7 @@ module.exports = {
|
||||
{
|
||||
selector: "CallExpression[callee.object.property.name='localStorage']",
|
||||
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,
|
||||
printWidth: 80,
|
||||
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;
|
||||
@apply before:backface-hidden;
|
||||
@apply after:backface-hidden;
|
||||
backface-visibility: hidden;
|
||||
-moz-backface-visibility: 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:text-accentContrast;
|
||||
@apply overscroll-none;
|
||||
@@ -11,17 +29,25 @@
|
||||
@apply antialiased;
|
||||
accent-color: var(--accent-color);
|
||||
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 {
|
||||
@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 {
|
||||
@apply bg-divider bg-clip-content;
|
||||
@apply rounded-full;
|
||||
@apply border-solid border-transparent border-4;
|
||||
@apply border-4 border-solid border-transparent;
|
||||
@apply hover:bg-dividerDark;
|
||||
@apply hover:bg-clip-content;
|
||||
}
|
||||
@@ -54,7 +80,7 @@ html {
|
||||
|
||||
body {
|
||||
@apply bg-primary;
|
||||
@apply text-secondary text-body;
|
||||
@apply text-body text-secondary;
|
||||
@apply font-medium;
|
||||
@apply select-none;
|
||||
@apply overflow-x-hidden;
|
||||
@@ -124,8 +150,8 @@ a {
|
||||
|
||||
&.link {
|
||||
@apply items-center;
|
||||
@apply py-0.5 px-1;
|
||||
@apply -my-0.5 -mx-1;
|
||||
@apply px-1 py-0.5;
|
||||
@apply -mx-1 -my-0.5;
|
||||
@apply text-accent;
|
||||
@apply rounded;
|
||||
@apply hover:text-accentDark;
|
||||
@@ -137,7 +163,7 @@ a {
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply shadow-none #{!important};
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-8;
|
||||
@@ -154,7 +180,7 @@ a {
|
||||
@apply flex;
|
||||
@apply text-tiny text-primary;
|
||||
@apply font-semibold;
|
||||
@apply py-1 px-2;
|
||||
@apply px-2 py-1;
|
||||
@apply truncate;
|
||||
@apply leading-normal;
|
||||
@apply items-center;
|
||||
@@ -162,7 +188,7 @@ a {
|
||||
kbd {
|
||||
@apply hidden;
|
||||
@apply font-sans;
|
||||
@apply bg-gray-500/45;
|
||||
background-color: rgba(107, 114, 128, 0.45);
|
||||
@apply text-primaryLight;
|
||||
@apply rounded-sm;
|
||||
@apply px-1;
|
||||
@@ -170,6 +196,12 @@ a {
|
||||
@apply truncate;
|
||||
@apply sm:inline-flex;
|
||||
}
|
||||
|
||||
.env-icon {
|
||||
@apply transition;
|
||||
@apply inline-flex;
|
||||
@apply items-center;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-svg-arrow {
|
||||
@@ -195,7 +227,7 @@ a {
|
||||
@apply max-h-[45vh];
|
||||
@apply items-stretch;
|
||||
@apply overflow-y-auto;
|
||||
@apply text-secondary text-body;
|
||||
@apply text-body text-secondary;
|
||||
@apply p-2;
|
||||
@apply leading-normal;
|
||||
@apply focus:outline-none;
|
||||
@@ -234,7 +266,7 @@ hr {
|
||||
|
||||
.heading {
|
||||
@apply font-bold;
|
||||
@apply text-secondaryDark text-lg;
|
||||
@apply text-lg text-secondaryDark;
|
||||
@apply tracking-tight;
|
||||
}
|
||||
|
||||
@@ -243,7 +275,7 @@ hr {
|
||||
.textarea {
|
||||
@apply flex;
|
||||
@apply w-full;
|
||||
@apply py-2 px-4;
|
||||
@apply px-4 py-2;
|
||||
@apply bg-transparent;
|
||||
@apply rounded;
|
||||
@apply text-secondaryDark;
|
||||
@@ -284,7 +316,7 @@ button {
|
||||
@apply transform;
|
||||
@apply origin-top-left;
|
||||
@apply scale-75;
|
||||
@apply translate-x-1 -translate-y-4;
|
||||
@apply -translate-y-4 translate-x-1;
|
||||
}
|
||||
|
||||
.floating-input:focus-within ~ label {
|
||||
@@ -293,7 +325,7 @@ button {
|
||||
|
||||
.floating-input ~ .end-actions {
|
||||
@apply absolute;
|
||||
@apply right-0.2;
|
||||
@apply right-[.05rem];
|
||||
@apply inset-y-0;
|
||||
@apply flex;
|
||||
@apply items-center;
|
||||
@@ -335,23 +367,23 @@ pre.ace_editor {
|
||||
}
|
||||
|
||||
.info-response {
|
||||
@apply text-pink-500;
|
||||
color: var(--info-color);
|
||||
}
|
||||
|
||||
.success-response {
|
||||
@apply text-green-500;
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.redir-response {
|
||||
@apply text-yellow-500;
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.cl-error-response {
|
||||
@apply text-red-500;
|
||||
color: var(--cl-error-color);
|
||||
}
|
||||
|
||||
.sv-error-response {
|
||||
@apply text-red-600;
|
||||
color: var(--sv-error-color);
|
||||
}
|
||||
|
||||
.missing-data-response {
|
||||
@@ -366,7 +398,7 @@ pre.ace_editor {
|
||||
@apply px-4 py-2;
|
||||
@apply bg-tooltip;
|
||||
@apply border-secondaryDark;
|
||||
@apply text-primary text-body;
|
||||
@apply text-body text-primary;
|
||||
@apply justify-between;
|
||||
@apply shadow-lg;
|
||||
@apply font-semibold;
|
||||
@@ -394,7 +426,7 @@ pre.ace_editor {
|
||||
@apply before:opacity-10;
|
||||
@apply before:inset-0;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply before:content-[''];
|
||||
@apply hover:no-underline;
|
||||
@apply hover:before:opacity-20;
|
||||
}
|
||||
@@ -428,7 +460,7 @@ pre.ace_editor {
|
||||
@apply before:opacity-0;
|
||||
@apply before:z-20;
|
||||
@apply before:transition;
|
||||
@apply before:content-DEFAULT;
|
||||
@apply before:content-[''];
|
||||
@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 {
|
||||
@apply inline-flex;
|
||||
@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": {
|
||||
"add": "Add",
|
||||
"autoscroll": "Autoscroll",
|
||||
"cancel": "Cancel",
|
||||
"choose_file": "Choose a file",
|
||||
@@ -54,9 +55,28 @@
|
||||
"new": "Add new",
|
||||
"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": {
|
||||
"chat_with_us": "Chat with us",
|
||||
"contact_us": "Contact us",
|
||||
"cookies": "Cookies",
|
||||
"copy": "Copy",
|
||||
"copy_user_id": "Copy User Auth Token",
|
||||
"developer_option": "Developer options",
|
||||
@@ -112,18 +132,34 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generate Token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Include in URL",
|
||||
"learn": "Learn how",
|
||||
"pass_key_by": "Pass by",
|
||||
"password": "Password",
|
||||
"token": "Token",
|
||||
"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": {
|
||||
"created": "Collection created",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Edit Collection",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Please provide a name for the collection",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -209,6 +245,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Please provide a name for the environment",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -234,6 +271,7 @@
|
||||
"error": {
|
||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||
"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",
|
||||
"danger_zone": "Danger zone",
|
||||
"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",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"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_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches 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",
|
||||
"script_fail": "Could not execute pre-request script",
|
||||
"something_went_wrong": "Something went wrong",
|
||||
@@ -291,9 +332,13 @@
|
||||
"url": "URL"
|
||||
},
|
||||
"header": {
|
||||
"install_pwa": "Install app",
|
||||
"install_pwa": "Add to Home Screen",
|
||||
"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": {
|
||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||
@@ -456,6 +501,8 @@
|
||||
"enter_curl": "Enter cURL command",
|
||||
"generate_code": "Generate 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",
|
||||
"invalid_name": "Please provide a name for the request",
|
||||
"method": "Method",
|
||||
@@ -557,6 +604,7 @@
|
||||
"use_experimental_url_bar": "Use experimental URL bar with environment highlighting",
|
||||
"user": "User",
|
||||
"verified_email": "Verified email",
|
||||
"additional": "Additional Settings",
|
||||
"verify_email": "Verify email"
|
||||
},
|
||||
"shortcodes": {
|
||||
@@ -743,9 +791,11 @@
|
||||
"disconnected_from": "Disconnected from {name}",
|
||||
"docs_generated": "Documentation generated",
|
||||
"download_started": "Download started",
|
||||
"download_failed": "Download failed",
|
||||
"enabled": "Enabled",
|
||||
"file_imported": "File imported",
|
||||
"finished_in": "Finished in {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "History deleted",
|
||||
"linewrap": "Wrap lines",
|
||||
"loading": "Loading...",
|
||||
@@ -756,6 +806,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show": "Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -837,7 +888,7 @@
|
||||
"new": "New Team",
|
||||
"new_created": "New team created",
|
||||
"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_request_found": "Request not found.",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"choose_file": "選擇一個檔案",
|
||||
"clear": "清除",
|
||||
"clear_all": "全部清除",
|
||||
"clear_history": "Clear all History",
|
||||
"clear_history": "清除所有歷史記錄",
|
||||
"close": "關閉",
|
||||
"connect": "連線",
|
||||
"connecting": "正在連接",
|
||||
@@ -79,8 +79,8 @@
|
||||
"search": "搜尋",
|
||||
"share": "分享",
|
||||
"shortcuts": "快捷方式",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。",
|
||||
"social_links": "社群連結",
|
||||
"spotlight": "聚光燈",
|
||||
"status": "狀態",
|
||||
"status_description": "檢查網站狀態",
|
||||
@@ -135,15 +135,15 @@
|
||||
"renamed": "集合已重新命名",
|
||||
"request_in_use": "請求正在使用中",
|
||||
"save_as": "另存為",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "儲存到集合",
|
||||
"select": "選擇一個集合",
|
||||
"select_location": "選擇位置",
|
||||
"select_team": "選擇一個團隊",
|
||||
"team_collections": "團隊集合"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"close_unsaved_tab": "您確定要關閉此分頁嗎?",
|
||||
"close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。",
|
||||
"exit_team": "您確定要離開此團隊嗎?",
|
||||
"logout": "您確定要登出嗎?",
|
||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||
@@ -158,9 +158,9 @@
|
||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "新增至參數",
|
||||
"open_request_in_new_tab": "在新分頁開啟請求",
|
||||
"set_environment_variable": "設為變數"
|
||||
},
|
||||
"count": {
|
||||
"header": "請求標頭 {count}",
|
||||
@@ -204,31 +204,31 @@
|
||||
"create_new": "建立新環境",
|
||||
"created": "已建立環境",
|
||||
"deleted": "刪除環境",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "已複製環境",
|
||||
"edit": "編輯環境",
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"empty_variables": "無變數",
|
||||
"global": "全域",
|
||||
"global_variables": "全域變數",
|
||||
"invalid_name": "請提供有效的環境名稱",
|
||||
"list": "Environment variables",
|
||||
"list": "環境變數",
|
||||
"my_environments": "我的環境",
|
||||
"name": "Name",
|
||||
"name": "名稱",
|
||||
"nested_overflow": "巢狀環境變數不得大於 10 層",
|
||||
"new": "建立環境",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_active_environment": "無使用中的環境",
|
||||
"no_environment": "無環境",
|
||||
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"quick_peek": "快速預覽環境",
|
||||
"replace_with_variable": "以變數替代",
|
||||
"scope": "範圍",
|
||||
"select": "選擇環境",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"set": "設定環境",
|
||||
"set_as_environment": "設為環境",
|
||||
"team_environments": "團隊環境",
|
||||
"title": "環境",
|
||||
"updated": "更新環境",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"value": "數值",
|
||||
"variable": "變數",
|
||||
"variable_list": "變數列表"
|
||||
},
|
||||
"error": {
|
||||
@@ -252,7 +252,7 @@
|
||||
"no_duration": "無持續時間",
|
||||
"no_results_found": "找不到結果",
|
||||
"page_not_found": "找不到此頁面",
|
||||
"proxy_error": "Proxy error",
|
||||
"proxy_error": "Proxy 錯誤",
|
||||
"script_fail": "無法執行預請求指令碼",
|
||||
"something_went_wrong": "發生了一些錯誤",
|
||||
"test_script_fail": "無法執行測試指令碼"
|
||||
@@ -278,13 +278,13 @@
|
||||
"renamed": "資料夾已重新命名"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?",
|
||||
"connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ",
|
||||
"connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ",
|
||||
"mutations": "變體",
|
||||
"schema": "綱要",
|
||||
"subscriptions": "訂閱",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "切換連線"
|
||||
},
|
||||
"group": {
|
||||
"time": "時間",
|
||||
@@ -339,27 +339,27 @@
|
||||
"title": "匯入"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "檢查潛在錯誤",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "新增至環境",
|
||||
"not_found": "找不到環境變數 “{environment}”。"
|
||||
},
|
||||
"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": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "請檢查您的授權認證。",
|
||||
"404_error": "請檢查您的請求網址和方式類型。",
|
||||
"cors_error": "請檢查您的跨來源資源共用設定。",
|
||||
"default_error": "請檢查您的請求。",
|
||||
"network_error": "請檢查您的網路連線。"
|
||||
},
|
||||
"title": "Inspector",
|
||||
"title": "檢查工具",
|
||||
"url": {
|
||||
"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.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "未安裝擴充套件。",
|
||||
"extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。",
|
||||
"extention_enable_action": "啟用瀏覽器擴充套件",
|
||||
"extention_not_enabled": "未啟用擴充套件。"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -472,7 +472,7 @@
|
||||
"payload": "負載",
|
||||
"query": "查詢",
|
||||
"raw_body": "原始請求本體",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重新命名請求",
|
||||
"renamed": "請求已重新命名",
|
||||
"run": "執行",
|
||||
"save": "儲存",
|
||||
@@ -510,7 +510,7 @@
|
||||
"accent_color": "強調色",
|
||||
"account": "帳號",
|
||||
"account_deleted": "已刪除您的帳號",
|
||||
"account_description": "自定義您的帳號設定。",
|
||||
"account_description": "自訂您的帳號設定。",
|
||||
"account_email_description": "您的主要電子郵件地址。",
|
||||
"account_name_description": "這是您的顯示名稱。",
|
||||
"background": "背景",
|
||||
@@ -542,7 +542,7 @@
|
||||
"read_the": "閱讀",
|
||||
"reset_default": "重置為預設",
|
||||
"short_codes": "快捷碼",
|
||||
"short_codes_description": "我們為您打造的快捷碼。",
|
||||
"short_codes_description": "您建立的快捷碼。",
|
||||
"sidebar_on_left": "左側邊欄",
|
||||
"sync": "同步",
|
||||
"sync_collections": "集合",
|
||||
@@ -551,9 +551,9 @@
|
||||
"sync_history": "歷史",
|
||||
"system_mode": "系統",
|
||||
"telemetry": "遙測服務",
|
||||
"telemetry_helps_us": "遙測服務幫助我們進行個性化操作,為您提供最佳體驗。",
|
||||
"telemetry_helps_us": "遙測服務能夠幫助我們進行個人化操作,為您提供最佳體驗。",
|
||||
"theme": "主題",
|
||||
"theme_description": "自定義您的應用程式主題。",
|
||||
"theme_description": "自訂您的應用程式主題。",
|
||||
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
|
||||
"user": "使用者",
|
||||
"verified_email": "已確認電子郵件地址",
|
||||
@@ -592,26 +592,26 @@
|
||||
"title": "導航"
|
||||
},
|
||||
"others": {
|
||||
"prettify": "Prettify Editor's Content",
|
||||
"title": "Others"
|
||||
"prettify": "美化編輯器的內容",
|
||||
"title": "其他"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "複製請求連結",
|
||||
"delete_method": "選擇 DELETE 方法",
|
||||
"get_method": "選擇 GET 方法",
|
||||
"head_method": "選擇 HEAD 方法",
|
||||
"import_curl": "Import cURL",
|
||||
"import_curl": "匯入 cURL",
|
||||
"method": "方法",
|
||||
"next_method": "選擇下一個方法",
|
||||
"post_method": "選擇 POST 方法",
|
||||
"previous_method": "選擇上一個方法",
|
||||
"put_method": "選擇 PUT 方法",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重新命名請求",
|
||||
"reset_request": "重置請求",
|
||||
"save_request": "Save Request",
|
||||
"save_request": "儲存請求",
|
||||
"save_to_collections": "儲存到集合",
|
||||
"send_request": "傳送請求",
|
||||
"show_code": "Generate code snippet",
|
||||
"show_code": "產生程式碼片段",
|
||||
"title": "請求"
|
||||
},
|
||||
"response": {
|
||||
@@ -642,82 +642,82 @@
|
||||
"url": "網址"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "變更語言",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"title": "Environments"
|
||||
"delete": "刪除目前環境",
|
||||
"duplicate": "複製目前環境",
|
||||
"duplicate_global": "複製全域環境",
|
||||
"edit": "編輯目前環境",
|
||||
"edit_global": "編輯全域環境",
|
||||
"new": "建立新環境",
|
||||
"new_variable": "建立新環境變數",
|
||||
"title": "環境"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
"chat": "與客服對話",
|
||||
"help_menu": "幫助與支援",
|
||||
"open_docs": "閱讀說明文件",
|
||||
"open_github": "開啟 GitHub 儲存庫",
|
||||
"open_keybindings": "鍵盤快捷鍵",
|
||||
"social": "社交",
|
||||
"title": "一般"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "連接至伺服器",
|
||||
"disconnect": "斷開與伺服器的連線"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "邀請您的朋友使用 Hoppscotch",
|
||||
"title": "雜項"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "儲存為新請求",
|
||||
"select_method": "選擇方法",
|
||||
"switch_to": "切換至",
|
||||
"tab_authorization": "授權分頁",
|
||||
"tab_body": "本體分頁",
|
||||
"tab_headers": "標頭分頁",
|
||||
"tab_parameters": "參數分頁",
|
||||
"tab_pre_request_script": "預請求腳本分頁",
|
||||
"tab_query": "查詢分頁",
|
||||
"tab_tests": "測試分頁",
|
||||
"tab_variables": "變數分頁"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "複製回應",
|
||||
"download": "下載回應",
|
||||
"title": "回應"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"interceptor": "攔截器",
|
||||
"interface": "介面",
|
||||
"theme": "主題",
|
||||
"user": "使用者"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "變更攔截器",
|
||||
"change_language": "變更語言",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "黑色",
|
||||
"dark": "暗色",
|
||||
"light": "亮色",
|
||||
"system": "跟隨系統"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "關閉目前分頁",
|
||||
"close_others": "關閉所有其他分頁",
|
||||
"duplicate": "複製目前分頁",
|
||||
"new_tab": "開啟新分頁",
|
||||
"title": "分頁"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "刪除目前團隊",
|
||||
"edit": "編輯目前團隊",
|
||||
"invite": "邀請他人加入團隊",
|
||||
"new": "建立新團隊",
|
||||
"switch_to_personal": "切換至您的個人工作區",
|
||||
"title": "團隊"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -777,11 +777,11 @@
|
||||
"tab": {
|
||||
"authorization": "授權",
|
||||
"body": "請求本體",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"close": "關閉分頁",
|
||||
"close_others": "關閉其他分頁",
|
||||
"collections": "集合",
|
||||
"documentation": "幫助文件",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"duplicate": "複製分頁",
|
||||
"environments": "環境",
|
||||
"headers": "請求標頭",
|
||||
"history": "歷史記錄",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.8.1",
|
||||
"version": "2023.8.4-1",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
@@ -17,22 +17,22 @@
|
||||
"postinstall": "pnpm run gql-codegen",
|
||||
"do-test": "pnpm run test",
|
||||
"do-lint": "pnpm run prod-lint",
|
||||
"do-typecheck": "pnpm run lint",
|
||||
"do-typecheck": "node type-check.mjs",
|
||||
"do-lintfix": "pnpm run lintfix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.1.0",
|
||||
"@codemirror/autocomplete": "^6.9.0",
|
||||
"@codemirror/commands": "^6.2.4",
|
||||
"@codemirror/lang-javascript": "^6.1.9",
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
"@codemirror/commands": "^6.3.0",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/lang-xml": "^6.0.2",
|
||||
"@codemirror/language": "^6.9.0",
|
||||
"@codemirror/language": "6.9.0",
|
||||
"@codemirror/legacy-modes": "^6.3.3",
|
||||
"@codemirror/lint": "^6.4.0",
|
||||
"@codemirror/search": "^6.5.1",
|
||||
"@codemirror/state": "^6.2.1",
|
||||
"@codemirror/view": "^6.16.0",
|
||||
"@codemirror/lint": "^6.4.2",
|
||||
"@codemirror/search": "^6.5.4",
|
||||
"@codemirror/state": "^6.3.1",
|
||||
"@codemirror/view": "^6.22.0",
|
||||
"@fontsource-variable/inter": "^5.0.8",
|
||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
||||
@@ -41,9 +41,7 @@
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@hoppscotch/ui": "workspace:^",
|
||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||
"@lezer/highlight": "^1.1.6",
|
||||
"@sentry/tracing": "^7.64.0",
|
||||
"@sentry/vue": "^7.64.0",
|
||||
"@lezer/highlight": "1.1.4",
|
||||
"@urql/core": "^4.1.1",
|
||||
"@urql/devtools": "^2.0.3",
|
||||
"@urql/exchange-auth": "^2.1.6",
|
||||
@@ -54,6 +52,7 @@
|
||||
"acorn-walk": "^8.2.0",
|
||||
"axios": "^1.4.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cookie-es": "^1.0.0",
|
||||
"dioc": "workspace:^",
|
||||
"esprima": "^4.0.1",
|
||||
"events": "^3.3.0",
|
||||
@@ -78,6 +77,8 @@
|
||||
"process": "^0.11.10",
|
||||
"qs": "^6.11.2",
|
||||
"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-v3": "npm:socket.io-client@^3.1.3",
|
||||
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
|
||||
@@ -91,6 +92,7 @@
|
||||
"url": "^0.11.1",
|
||||
"util": "^0.12.5",
|
||||
"uuid": "^9.0.0",
|
||||
"verzod": "^0.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "^9.2.2",
|
||||
"vue-pdf-embed": "^1.1.6",
|
||||
@@ -100,7 +102,8 @@
|
||||
"wonka": "^6.3.4",
|
||||
"workbox-window": "^7.0.0",
|
||||
"xml-formatter": "^3.5.0",
|
||||
"yargs-parser": "^21.1.1"
|
||||
"yargs-parser": "^21.1.1",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
@@ -133,30 +136,34 @@
|
||||
"@vue/compiler-sfc": "^3.3.4",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"@vue/runtime-core": "^3.3.4",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"glob": "^10.3.10",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"openapi-types": "^12.1.3",
|
||||
"postcss": "^8.4.23",
|
||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
||||
"sass": "^1.66.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-fonts": "^1.0.3",
|
||||
"unplugin-icons": "^0.16.5",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"vite-plugin-checker": "^0.6.1",
|
||||
"vite-plugin-fonts": "^0.6.0",
|
||||
"vite-plugin-html-config": "^1.0.11",
|
||||
"vite-plugin-inspect": "^0.7.38",
|
||||
"vite-plugin-pages": "^0.31.0",
|
||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vite-plugin-vue-layouts": "^0.8.0",
|
||||
"vite-plugin-windicss": "^1.9.1",
|
||||
"vitest": "^0.34.2",
|
||||
"vue-tsc": "^1.8.8",
|
||||
"windicss": "^3.5.6"
|
||||
"vue-tsc": "^1.8.8"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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 />
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AppActionHandler: typeof import('./components/app/ActionHandler.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']
|
||||
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.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']
|
||||
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.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']
|
||||
EnvironmentsAdd: typeof import('./components/environments/Add.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']
|
||||
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
|
||||
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
|
||||
IconLucideAlertCircle: typeof import('~icons/lucide/alert-circle')['default']
|
||||
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
|
||||
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
|
||||
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
|
||||
@@ -156,7 +160,7 @@ declare module 'vue' {
|
||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||
IconLucideSearch: typeof import('~icons/lucide/search')['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']
|
||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.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']
|
||||
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.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']
|
||||
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.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']
|
||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.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']
|
||||
SmartToggle: typeof import('./../../hoppscotch-ui/src/components/smart/Toggle.vue')['default']
|
||||
SmartTree: typeof import('./../../hoppscotch-ui/src/components/smart/Tree.vue')['default']
|
||||
|
||||
@@ -2,53 +2,16 @@
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
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()
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const showShortcuts = ref(false)
|
||||
const showShare = 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", () => {
|
||||
showShortcuts.value = !showShortcuts.value
|
||||
})
|
||||
@@ -60,9 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
|
||||
defineActionHandler("modals.login.toggle", () => {
|
||||
showLogin.value = !showLogin.value
|
||||
})
|
||||
|
||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||
teamID.value = teamId
|
||||
confirmRemove.value = true
|
||||
})
|
||||
</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>
|
||||
<div
|
||||
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;`"
|
||||
>
|
||||
<div v-if="contextMenuOptions" class="flex flex-col">
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@close="hideModal"
|
||||
>
|
||||
<template #body>
|
||||
<p class="px-2 mb-4 text-secondaryLight">
|
||||
<p class="mb-4 px-2 text-secondaryLight">
|
||||
{{ t("app.developer_option_description") }}
|
||||
</p>
|
||||
<div class="flex flex-1">
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
<AppInterceptor />
|
||||
</template>
|
||||
</tippy>
|
||||
<HoppButtonSecondary
|
||||
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
|
||||
:label="t('app.cookies')"
|
||||
:icon="IconCookie"
|
||||
@click="showCookiesModal = true"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<tippy
|
||||
@@ -175,7 +181,7 @@
|
||||
@click="COLUMN_LAYOUT = !COLUMN_LAYOUT"
|
||||
/>
|
||||
<span
|
||||
class="transition transform"
|
||||
class="transform transition"
|
||||
:class="{
|
||||
'rotate-180': SIDEBAR_ON_LEFT,
|
||||
}"
|
||||
@@ -195,12 +201,17 @@
|
||||
:show="showDeveloperOptions"
|
||||
@hide-modal="showDeveloperOptions = false"
|
||||
/>
|
||||
<CookiesAllModal
|
||||
:show="showCookiesModal"
|
||||
@hide-modal="showCookiesModal = false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { version } from "~/../package.json"
|
||||
import IconCookie from "~icons/lucide/cookie"
|
||||
import IconSidebar from "~icons/lucide/sidebar"
|
||||
import IconZap from "~icons/lucide/zap"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const showDeveloperOptions = ref(false)
|
||||
const showCookiesModal = ref(false)
|
||||
|
||||
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
|
||||
const SIDEBAR = useSetting("SIDEBAR")
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<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
|
||||
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="{
|
||||
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
||||
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
||||
}"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
class="tracking-wide !font-bold !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark uppercase"
|
||||
:label="t('app.name')"
|
||||
to="/"
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
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')"
|
||||
to="/"
|
||||
/>
|
||||
</div>
|
||||
<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="inline-flex items-center justify-center flex-1 space-x-2">
|
||||
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||
<button
|
||||
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')"
|
||||
>
|
||||
<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") }}
|
||||
</span>
|
||||
<span class="flex space-x-1">
|
||||
@@ -30,192 +56,224 @@
|
||||
<kbd class="shortcut-key">K</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<HoppButtonSecondary
|
||||
v-if="showInstallButton"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('header.install_pwa')"
|
||||
:icon="IconDownload"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="installPWA()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${
|
||||
mdAndLarger ? t('support.title') : t('app.options')
|
||||
} <kbd>?</kbd>`"
|
||||
:icon="IconLifeBuoy"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.support.toggle')"
|
||||
/>
|
||||
</div>
|
||||
<div class="inline-flex items-center justify-end flex-1 space-x-2">
|
||||
<div
|
||||
v-if="currentUser === null"
|
||||
class="inline-flex items-center space-x-2"
|
||||
>
|
||||
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
:icon="IconUploadCloud"
|
||||
: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"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||
:title="`${
|
||||
mdAndLarger ? t('support.title') : t('app.options')
|
||||
} <kbd>?</kbd>`"
|
||||
:icon="IconLifeBuoy"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
@click="invokeAction('modals.support.toggle')"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
:label="t('header.login')"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="inline-flex items-center space-x-2">
|
||||
<TeamsMemberStack
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam.teamMembers.length > 1
|
||||
"
|
||||
:team-members="selectedTeam.teamMembers"
|
||||
show-count
|
||||
class="mx-2"
|
||||
@handle-click="handleTeamEdit()"
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.invite_tooltip')"
|
||||
:icon="IconUserPlus"
|
||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
||||
@click="handleInvite()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam?.myRole === 'OWNER'
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.edit')"
|
||||
:icon="IconSettings"
|
||||
class="py-1.75 !text-green-500 !focus-visible:text-green-600 !hover:text-green-600"
|
||||
@click="handleTeamEdit()"
|
||||
/>
|
||||
</div>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => accountActions.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('workspace.change')"
|
||||
:label="mdAndLarger ? workspaceName : ``"
|
||||
: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"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="accountActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
@click="hide()"
|
||||
>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<span class="px-2">
|
||||
<span>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
:on-shown="() => downloadActions.focus()"
|
||||
>
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="
|
||||
currentUser.displayName ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
:title="
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
:initial="currentUser.displayName || currentUser.email"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||
"
|
||||
:title="t('header.download_app')"
|
||||
:icon="IconDownload"
|
||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
ref="downloadActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.p="profile.$el.click()"
|
||||
@keyup.s="settings.$el.click()"
|
||||
@keyup.l="logout.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
@click="hide()"
|
||||
>
|
||||
<div class="flex flex-col px-2 text-tiny">
|
||||
<span class="inline-flex font-semibold truncate">
|
||||
{{
|
||||
currentUser.displayName ||
|
||||
t("profile.default_hopp_displayname")
|
||||
}}
|
||||
</span>
|
||||
<span class="inline-flex truncate text-secondaryLight">
|
||||
{{ currentUser.email }}
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
ref="profile"
|
||||
to="/profile"
|
||||
:icon="IconUser"
|
||||
:label="t('navigation.profile')"
|
||||
:shortcut="['P']"
|
||||
@click="hide()"
|
||||
:label="t('header.download_app')"
|
||||
:icon="IconDownload"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="settings"
|
||||
to="/settings"
|
||||
:icon="IconSettings"
|
||||
:label="t('navigation.settings')"
|
||||
:shortcut="['S']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<FirebaseLogout
|
||||
ref="logout"
|
||||
:shortcut="['L']"
|
||||
@confirm-logout="hide()"
|
||||
v-if="showInstallButton"
|
||||
:label="t('header.install_pwa')"
|
||||
:icon="IconPlusSquare"
|
||||
@click="installPWA()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div
|
||||
v-if="currentUser === null"
|
||||
class="inline-flex items-center space-x-2"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconUploadCloud"
|
||||
:label="t('header.save_workspace')"
|
||||
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')"
|
||||
/>
|
||||
<HoppButtonPrimary
|
||||
:label="t('header.login')"
|
||||
class="h-8"
|
||||
@click="invokeAction('modals.login.toggle')"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="inline-flex items-center space-x-2">
|
||||
<TeamsMemberStack
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam.teamMembers.length > 1
|
||||
"
|
||||
:team-members="selectedTeam.teamMembers"
|
||||
show-count
|
||||
class="mx-2"
|
||||
@handle-click="handleTeamEdit()"
|
||||
/>
|
||||
<div
|
||||
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
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.invite_tooltip')"
|
||||
:icon="IconUserPlus"
|
||||
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||
@click="handleInvite()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-if="
|
||||
workspace.type === 'team' &&
|
||||
selectedTeam &&
|
||||
selectedTeam?.myRole === 'OWNER'
|
||||
"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('team.edit')"
|
||||
:icon="IconSettings"
|
||||
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||
@click="handleTeamEdit()"
|
||||
/>
|
||||
</div>
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => accountActions.focus()"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('workspace.change')"
|
||||
:label="mdAndLarger ? workspaceName : ``"
|
||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||
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 }">
|
||||
<div
|
||||
ref="accountActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.escape="hide()"
|
||||
@click="hide()"
|
||||
>
|
||||
<WorkspaceSelector />
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
<span class="px-2">
|
||||
<tippy
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
:on-shown="() => tippyActions.focus()"
|
||||
>
|
||||
<HoppSmartPicture
|
||||
v-if="currentUser.photoURL"
|
||||
v-tippy="{
|
||||
theme: 'tooltip',
|
||||
}"
|
||||
:url="currentUser.photoURL"
|
||||
:alt="
|
||||
currentUser.displayName ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
:title="
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<HoppSmartPicture
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
currentUser.displayName ||
|
||||
currentUser.email ||
|
||||
t('profile.default_hopp_displayname')
|
||||
"
|
||||
:initial="currentUser.displayName || currentUser.email"
|
||||
indicator
|
||||
:indicator-styles="
|
||||
network.isOnline ? 'bg-emerald-500' : 'bg-red-500'
|
||||
"
|
||||
/>
|
||||
<template #content="{ hide }">
|
||||
<div
|
||||
ref="tippyActions"
|
||||
class="flex flex-col focus:outline-none"
|
||||
tabindex="0"
|
||||
@keyup.p="profile.$el.click()"
|
||||
@keyup.s="settings.$el.click()"
|
||||
@keyup.l="logout.$el.click()"
|
||||
@keyup.escape="hide()"
|
||||
>
|
||||
<div class="flex flex-col px-2 text-tiny">
|
||||
<span class="inline-flex truncate font-semibold">
|
||||
{{
|
||||
currentUser.displayName ||
|
||||
t("profile.default_hopp_displayname")
|
||||
}}
|
||||
</span>
|
||||
<span class="inline-flex truncate text-secondaryLight">
|
||||
{{ currentUser.email }}
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
<HoppSmartItem
|
||||
ref="profile"
|
||||
to="/profile"
|
||||
:icon="IconUser"
|
||||
:label="t('navigation.profile')"
|
||||
:shortcut="['P']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="settings"
|
||||
to="/settings"
|
||||
:icon="IconSettings"
|
||||
:label="t('navigation.settings')"
|
||||
:shortcut="['S']"
|
||||
@click="hide()"
|
||||
/>
|
||||
<FirebaseLogout
|
||||
ref="logout"
|
||||
:shortcut="['L']"
|
||||
@confirm-logout="hide()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</tippy>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<AppAnnouncement v-if="!network.isOnline" />
|
||||
<AppBanner v-if="bannerContent" :banner="bannerContent" />
|
||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||
<TeamsInvite
|
||||
v-if="workspace.type === 'team' && workspace.teamID"
|
||||
@@ -231,32 +289,52 @@
|
||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||
@refetch-teams="refetchTeams"
|
||||
/>
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<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 { useReadonlyStream } from "@composables/stream"
|
||||
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 { 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 toast = useToast()
|
||||
const router = useRouter()
|
||||
|
||||
/**
|
||||
* Once the PWA code is initialized, this holds a method
|
||||
@@ -271,13 +349,40 @@ const showTeamsModal = ref(false)
|
||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||
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 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(
|
||||
platform.auth.getProbableUserStream(),
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
const teamID = ref<string | null>(null)
|
||||
|
||||
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
||||
|
||||
// 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
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const profile = ref<any | null>(null)
|
||||
const settings = ref<any | null>(null)
|
||||
const logout = ref<any | null>(null)
|
||||
const accountActions = ref<any | null>(null)
|
||||
const downloadActions = ref<any | null>(null)
|
||||
|
||||
defineActionHandler("modals.team.edit", handleTeamEdit)
|
||||
|
||||
@@ -405,6 +529,12 @@ defineActionHandler(
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
|
||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||
if (selectedTeam.value?.myRole !== TeamMemberRole.Owner) return noPermission()
|
||||
teamID.value = teamId
|
||||
confirmRemove.value = true
|
||||
})
|
||||
|
||||
const noPermission = () => {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="inspectionResults && inspectionResults.length > 0">
|
||||
<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
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconAlertTriangle"
|
||||
@@ -10,12 +10,12 @@
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
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">
|
||||
<icon-lucide-activity class="mr-2 svg-icons text-accent" />
|
||||
<span class="flex flex-1 items-center">
|
||||
<icon-lucide-activity class="svg-icons mr-2 text-accent" />
|
||||
<span class="font-bold">
|
||||
{{ t("inspections.title") }}
|
||||
</span>
|
||||
@@ -31,10 +31,10 @@
|
||||
<div
|
||||
v-for="(inspector, index) in inspectionResults"
|
||||
:key="index"
|
||||
class="flex self-stretch max-w-md w-full"
|
||||
class="flex w-full max-w-md self-stretch"
|
||||
>
|
||||
<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
|
||||
v-if="inspector.text.type === 'text'"
|
||||
@@ -44,13 +44,13 @@
|
||||
<HoppSmartLink
|
||||
blank
|
||||
:to="inspector.doc.link"
|
||||
class="text-accent hover:text-accentDark transition"
|
||||
class="text-accent transition hover:text-accentDark"
|
||||
>
|
||||
{{ inspector.doc.text }}
|
||||
<icon-lucide-arrow-up-right class="svg-icons" />
|
||||
</HoppSmartLink>
|
||||
</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
|
||||
:label="inspector.action.text"
|
||||
outline
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
>
|
||||
<template #body>
|
||||
<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") }}
|
||||
</h2>
|
||||
<HoppSmartItem
|
||||
@@ -27,7 +27,7 @@
|
||||
active
|
||||
@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") }}
|
||||
</h2>
|
||||
<template
|
||||
|
||||
@@ -47,14 +47,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Splitpanes, Pane } from "splitpanes"
|
||||
import { Pane, Splitpanes } from "splitpanes"
|
||||
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
import { computed, useSlots, ref } from "vue"
|
||||
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")
|
||||
|
||||
@@ -67,6 +68,8 @@ const SIDEBAR = useSetting("SIDEBAR")
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const persistenceService = useService(PersistenceService)
|
||||
|
||||
const hasSidebar = computed(() => !!slots.sidebar)
|
||||
const hasSecondary = computed(() => !!slots.secondary)
|
||||
|
||||
@@ -96,7 +99,7 @@ if (!COLUMN_LAYOUT.value) {
|
||||
function setPaneEvent(event: PaneEvent[], type: "vertical" | "horizontal") {
|
||||
if (!props.layoutId) return
|
||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||
setLocalConfig(storageKey, JSON.stringify(event))
|
||||
persistenceService.setLocalConfig(storageKey, JSON.stringify(event))
|
||||
}
|
||||
|
||||
function populatePaneEvent() {
|
||||
@@ -119,7 +122,7 @@ function populatePaneEvent() {
|
||||
|
||||
function getPaneData(type: "vertical" | "horizontal"): PaneEvent[] | null {
|
||||
const storageKey = `${props.layoutId}-pane-config-${type}`
|
||||
const paneEvent = getLocalConfig(storageKey)
|
||||
const paneEvent = persistenceService.getLocalConfig(storageKey)
|
||||
if (!paneEvent) return null
|
||||
return JSON.parse(paneEvent)
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
class="share-link"
|
||||
tabindex="0"
|
||||
>
|
||||
<component :is="platform.icon" class="w-6 h-6" />
|
||||
<component :is="platform.icon" class="h-6 w-6" />
|
||||
<span class="mt-3">
|
||||
{{ platform.name }}
|
||||
</span>
|
||||
</a>
|
||||
<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">
|
||||
{{ t("app.copy") }}
|
||||
</span>
|
||||
@@ -119,14 +119,14 @@ const hideModal = () => {
|
||||
.share-link {
|
||||
@apply border border-dividerLight;
|
||||
@apply rounded;
|
||||
@apply flex-col flex;
|
||||
@apply flex flex-col;
|
||||
@apply p-4;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply font-semibold;
|
||||
@apply hover: (bg-primaryLight text-secondaryDark);
|
||||
@apply focus: outline-none;
|
||||
@apply focus-visible: border-divider;
|
||||
@apply hover:bg-primaryLight hover:text-secondaryDark;
|
||||
@apply focus:outline-none;
|
||||
@apply focus-visible:border-divider;
|
||||
|
||||
svg {
|
||||
@apply opacity-80;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<HoppSmartSlideOver :show="show" :title="t('app.shortcuts')" @close="close()">
|
||||
<template #content>
|
||||
<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
|
||||
v-model="filterText"
|
||||
@@ -17,7 +17,7 @@
|
||||
v-if="isEmpty(shortcutsResults)"
|
||||
: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>
|
||||
|
||||
<details
|
||||
@@ -28,16 +28,16 @@
|
||||
open
|
||||
>
|
||||
<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
|
||||
class="font-semibold truncate capitalize-first text-secondaryDark"
|
||||
class="capitalize-first truncate font-semibold text-secondaryDark"
|
||||
>
|
||||
{{ sectionTitle }}
|
||||
</span>
|
||||
</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
|
||||
v-for="(shortcut, index) in sectionResults"
|
||||
:key="`shortcut-${index}`"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="flex items-center py-1">
|
||||
<span class="flex flex-1 mr-4">
|
||||
<span class="mr-4 flex flex-1">
|
||||
{{ shortcut.label }}
|
||||
</span>
|
||||
<kbd
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<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">
|
||||
<span class="flex items-center flex-1">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ t("shortcut.request.send_request") }}
|
||||
</span>
|
||||
<span class="flex items-center flex-1">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ t("shortcut.general.show_all") }}
|
||||
</span>
|
||||
<span class="flex items-center flex-1">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ t("shortcut.general.command_menu") }}
|
||||
</span>
|
||||
<span class="flex items-center flex-1">
|
||||
<span class="flex flex-1 items-center">
|
||||
{{ t("shortcut.general.help_menu") }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<aside class="flex justify-between h-full md:flex-col">
|
||||
<nav class="flex flex-1 flex-nowrap md:flex-col md:flex-none bg-primary">
|
||||
<aside class="flex h-full justify-between md:flex-col">
|
||||
<nav class="flex flex-1 flex-nowrap bg-primary md:flex-none md:flex-col">
|
||||
<HoppSmartLink
|
||||
v-for="(navigation, index) in primaryNavigation"
|
||||
:key="`navigation-${index}`"
|
||||
@@ -73,25 +73,25 @@ const primaryNavigation = [
|
||||
.nav-link {
|
||||
@apply relative;
|
||||
@apply p-4;
|
||||
@apply flex flex-col flex-1;
|
||||
@apply flex flex-1 flex-col;
|
||||
@apply items-center;
|
||||
@apply justify-center;
|
||||
@apply hover: (bg-primaryDark text-secondaryDark);
|
||||
@apply focus-visible: text-secondaryDark;
|
||||
@apply hover:bg-primaryDark hover:text-secondaryDark;
|
||||
@apply focus-visible:text-secondaryDark;
|
||||
@apply after:absolute;
|
||||
@apply after:inset-x-0;
|
||||
@apply after:md: inset-x-auto;
|
||||
@apply after:md: inset-y-0;
|
||||
@apply after:md:inset-x-auto;
|
||||
@apply after:md:inset-y-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:md: bottom-auto;
|
||||
@apply after:md: left-0;
|
||||
@apply after:z-2;
|
||||
@apply after:md:bottom-auto;
|
||||
@apply after:md:left-0;
|
||||
@apply after:z-10;
|
||||
@apply after:h-0.5;
|
||||
@apply after:md: h-full;
|
||||
@apply after:md:h-full;
|
||||
@apply after:w-full;
|
||||
@apply after:md: w-0.5;
|
||||
@apply after:content-DEFAULT;
|
||||
@apply focus: after: bg-divider;
|
||||
@apply after:md:w-0.5;
|
||||
@apply after:content-[""];
|
||||
@apply focus:after:bg-divider;
|
||||
|
||||
.svg-icons {
|
||||
@apply opacity-75;
|
||||
@@ -105,7 +105,7 @@ const primaryNavigation = [
|
||||
&.router-link-active {
|
||||
@apply text-secondaryDark;
|
||||
@apply bg-primaryLight;
|
||||
@apply hover: text-secondaryDark;
|
||||
@apply hover:text-secondaryDark;
|
||||
@apply after:bg-accent;
|
||||
|
||||
.svg-icons {
|
||||
@@ -116,7 +116,7 @@ const primaryNavigation = [
|
||||
&.exact-active-link {
|
||||
@apply text-secondaryDark;
|
||||
@apply bg-primaryLight;
|
||||
@apply hover: text-secondaryDark;
|
||||
@apply hover:text-secondaryDark;
|
||||
@apply after:bg-accent;
|
||||
|
||||
.svg-icons {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
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 }"
|
||||
tabindex="-1"
|
||||
@click="emit('action')"
|
||||
@@ -9,7 +9,7 @@
|
||||
>
|
||||
<component
|
||||
:is="entry.icon"
|
||||
class="opacity-50 svg-icons"
|
||||
class="svg-icons opacity-50"
|
||||
:class="{ 'opacity-100': active }"
|
||||
/>
|
||||
<template
|
||||
@@ -112,9 +112,9 @@ watch(
|
||||
@apply after:left-0;
|
||||
@apply after:bottom-0;
|
||||
@apply after:bg-transparent;
|
||||
@apply after:z-2;
|
||||
@apply after:z-10;
|
||||
@apply after:w-0.5;
|
||||
@apply after:content-DEFAULT;
|
||||
@apply after:content-[''];
|
||||
|
||||
&.active {
|
||||
@apply after:bg-accentLight;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{{ historyEntry.request.url }}
|
||||
</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] }}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<span class="block" :class="{ truncate: index !== 0 }">
|
||||
{{ folder.name }}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</span>
|
||||
<icon-lucide-chevron-right class="flex flex-shrink-0" />
|
||||
<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"
|
||||
>
|
||||
{{ historyEntry.request.method }}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</template>
|
||||
<span
|
||||
v-if="request"
|
||||
class="font-semibold truncate text-tiny flex flex-shrink-0 border border-dividerDark rounded-md px-1"
|
||||
:class="getMethodLabelColorClassOf(request)"
|
||||
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
|
||||
:style="{ color: getMethodLabelColorClassOf(request) }"
|
||||
>
|
||||
{{ request.method.toUpperCase() }}
|
||||
</span>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@close="emit('hide-modal')"
|
||||
>
|
||||
<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">
|
||||
<input
|
||||
id="command"
|
||||
@@ -16,14 +16,14 @@
|
||||
autocomplete="off"
|
||||
name="command"
|
||||
: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" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
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
|
||||
v-for="([sectionID, sectionResult], sectionIndex) in scoredResults"
|
||||
@@ -31,7 +31,7 @@
|
||||
class="flex flex-col"
|
||||
>
|
||||
<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 }}
|
||||
</h5>
|
||||
@@ -49,7 +49,7 @@
|
||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-lucide-search class="pb-2 opacity-75 svg-icons" />
|
||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
||||
</template>
|
||||
<HoppButtonSecondary
|
||||
:label="t('action.clear')"
|
||||
@@ -59,7 +59,7 @@
|
||||
</HoppSmartPlaceholder>
|
||||
</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">
|
||||
<kbd class="shortcut-key">↑</kbd>
|
||||
|
||||