Compare commits

...

75 Commits

Author SHA1 Message Date
Liyas Thomas
7517446f79 chore: add protocols' logo to realtime page 2023-12-12 14:21:35 +05:30
Muhammed Ajmal M
c1bc430ee6 fix: overflowing modal fix on small screens (#3643)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-12 14:21:19 +05:30
James George
9201aa7d7d fix(common): ensure banner colors are displayed correctly (#3630) 2023-12-12 13:55:18 +05:30
Liyas Thomas
87395a4553 fix: minor ui issues 2023-12-07 17:12:33 +05:30
Andrew Bastin
6063c633ee chore: pin vue workspace-wide to 3.3.9 2023-12-07 17:04:17 +05:30
Akash K
7481feb366 feat: introduce platform defs for adding additional spotlight searchers (#3631) 2023-12-07 16:28:02 +05:30
James George
bdfa14fa54 refactor(scripting-revamp): migrate js-sandbox to web worker/Node vm based implementation (#3619) 2023-12-07 16:10:42 +05:30
Andrew Bastin
0a61ec2bfe fix: useI18n type breaking 2023-12-07 15:17:41 +05:30
Nivedin
2bf0106aa2 feat: embeds (#3627)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-07 15:03:49 +05:30
Akash K
ab7c29d228 refactor: revamp the importers & exporters systems to be reused (#3425)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-06 21:24:29 +05:30
Joel Jacob Stephen
d9c75ed79e feat: introducing shared requests to admin dashboard (#3537)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-06 00:21:28 +05:30
Anwarul Islam
6fa722df7b chore: hoppscotch-ui improvements (#3497)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-12-06 00:08:44 +05:30
Balu Babu
18864bfecf feat: addition of data field into User and Team Collections (#3614)
* feat: added new columns into the TeamCollections and UserCollections models

* feat: completed addition of new data field in TeamCollection

* feat: completed addition of new data field in UserCollections

* chore: updated all tests in team-collection module

* chore: added tests for updateTeamCollection method in team-collection module

* chore: refactored all existing testcases in user-collection to reflect new changes

* chore: added new testcases for updateUserCollection method in user-collection module

* chore: made data field optional in team and user collections

* chore: fixed edgecases for data being null

* chore: resolved issue with team-request testcases

* chore: completed changes requested in PR review

* chore: changed target to prod in hoppscotch-old-backend service
2023-12-05 20:12:37 +05:30
Akash K
95754cb2b4 chore: bump deps for hoppscotch-common and hoppscotch-selfhost-web (#3575) 2023-12-05 17:33:25 +05:30
Gaurav K P
ed2a461dc5 feat(common): display status text from the API response if available (#3466)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-04 23:31:49 +05:30
Muhammed Ajmal M
8d5a456dbd fix(common): parentheses and single quotes support to curl imports (#3509) 2023-12-04 23:03:22 +05:30
Nivedin
2528bbb92f feat: shared request (#3486)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-04 22:51:18 +05:30
Liyas Thomas
259cd48dbb feat: init boring avatars (#3615) 2023-12-04 13:20:26 +05:30
Joel Jacob Stephen
b43531f200 feat: add ability to make banners dismissible + new info and warning color schemes added based on themes (#3586)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-04 00:57:37 +05:30
Muhammed Ajmal M
26da3e18a9 fix(common): prevented truncating parameters (#3502) 2023-12-04 00:49:45 +05:30
Rajdip Bhattacharya
bb4b640e58 feat: convert json to interfaces (#3566)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
2023-12-03 23:14:26 +05:30
Liyas Thomas
1cc845e17d fix: minor ui improvements (#3603) 2023-11-29 22:45:40 +05:30
James George
60bfb6fe2c refactor: move persistence logic into a dedicated service (#3493) 2023-11-29 22:40:26 +05:30
Joel Jacob Stephen
144d14ab5b fix: email validation failure in cases when email entered is correct when trying to create a team in admin dashboard (#3588)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-11-29 21:49:49 +05:30
Anwarul Islam
8f1ca6e282 feat: platform definition added for additional settings components (#3503) 2023-11-29 21:42:51 +05:30
Dante Calderon
a93758c6b7 chore: Remove whitespace in env variables 2023-11-22 19:40:32 +05:30
James George
1829c088cc feat: support for subpath based access in SH apps (#3449)
Co-authored-by: Balu Babu <balub997@gmail.com>
2023-11-22 19:35:35 +05:30
Akash K
ee1425d0dd fix: XML body disappearing with invalid XML (#3567)
fix: catch xmlformatter errors
2023-11-20 15:02:07 +05:30
Joel Jacob Stephen
24ae090916 refactor: allow banner service to hold multiple banners and display the banner with the highest score (#3556) 2023-11-17 20:31:34 +05:30
Nivedin
a3aa9b68fc refactor: interceptor error display in graphql response (#3553) 2023-11-17 17:03:53 +05:30
Joel Jacob Stephen
50f475334e fix: enlarged hoppscotch logo on dashboard login screen (#3559)
fix: resize the dashboard login icon
2023-11-16 22:51:08 +05:30
Andrew Bastin
7b18526f24 chore: merge hoppscotch/main into hoppscotch/release/2023.12.0 2023-11-16 13:51:12 +05:30
Andrew Bastin
23afc201a1 chore: bump version to release/2023.8.4 2023-11-14 21:26:16 +05:30
Andrew Bastin
b1982d74a6 fix: make schema more lenient while parsing public data structures 2023-11-14 21:24:25 +05:30
Andrew Bastin
e93a37c711 fix: add i18n entries for oauth errors 2023-11-14 17:44:37 +05:30
Nivedin
8d7509cdea fix: interceptor error from extension issue (#3548) 2023-11-14 17:17:23 +05:30
Akash K
e24d0ce605 fix: oauth 2.0 authentication type is breaking (#3531) 2023-11-14 00:12:04 +05:30
Balu Babu
f5d2e4f11f feat: introducing shortcode into admin module (#3504)
* feat: added query in infra to fetch all shortcodes

* feat: added mutation in admin to delete shortcode

* chore: added new tests for methods in shortcode module

* chore: removed .vscode file

* chore: added a new ShortcodeCreator type to output of fetchAllShortcodes query

* chore: shortcodeCreator type is now nullable

* chore: added type defs to fetchAllShortcodes method in admin module

* docs: update code comments

* chore: changed target to prod in hoppscotch-old-backend

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-11-10 14:28:02 +05:30
Andrew Bastin
de725337d6 fix: window drag taking precedence on windows 2023-11-08 20:07:13 +05:30
Andrew Bastin
9d1d369f37 fix: performance issues due to mouse on header detection 2023-11-08 18:47:40 +05:30
Andrew Bastin
2bd925d441 chore: correct version of selfhost-desktop 2023-11-08 17:18:12 +05:30
Liyas Thomas
bb8dc6f7eb chore: updated brand assets (#3500)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-11-08 15:47:35 +05:30
Anwarul Islam
be3e5ba7e7 fix: graphql query deprecation issue (#3506) 2023-11-08 14:51:39 +05:30
Andrew Bastin
663134839f feat: let platforms disable we are using cookies prompt 2023-11-07 20:49:07 +05:30
Andrew Bastin
736f83a70c fix: header inspector cookie inspection will not trigger if current interceptor supports cookies 2023-11-07 20:36:34 +05:30
Andrew Bastin
05d2175f43 fix: selfhost-desktop not running gql-codegen 2023-11-07 20:24:22 +05:30
Balu Babu
4caf0053cd feat: introduction of shared-requests (#3476)
* feat: added new property to existing shortcode model in prisma schema

* chore: created shared-requests module

* chore: created shared-request model

* chore: complete sharedRequest query

* chore: completed mutation to create a SharedRequest

* chore: completed subscription to create a SharedRequest

* chore: completed query to fetch all user created shared-requests

* chore: completed mutation to delete a SharedRequest

* chore: completed subscription to delete a SharedRequest

* chore: removed unused dependncues in share-requests module

* chore: added shared-requests into user deletion spec

* test: added all testcases for shared-request module

* test: modified all relevant tests in shortcode module

* chore: added deprecated label to all queries,mutations and subscriptions in the shortcode module

* chore: resolved all comments raised in review

* feat: added ability to update and listen to updates of shared-requests

* chore: added updatedOn field to shortcode model

* chore: fixed issue with updateSharedRequest method

* chore: fixed incorrect value getting updated

* chore: added all test-cases for updateSharedRequest method

* chore: created migration for shared-requests

* chore: moved shared-requests into shortcode module

* chore: added missing import in shortcode tests

* chore: changed properties to embedProperties in shortcode model

* feat: generated migrations file for new schema changes to Shortcodes table

* chore: changed target of old-backend service in docker-compose file

* chore: fixed issue with updatedOn field in shortcodes model

* chore: removed unused dependencies

* fix: handle invalid input for shortcode properties

* Revert "fix: handle invalid input for shortcode properties"

This reverts commit 4dcb0afb18.

* chore: changed updateShortcode method name to updateEmbedProperties

* chore: changed target of hoppscotch-old-backend service to prod

---------

Co-authored-by: Mir Arif Hasan <arif.ishan05@gmail.com>
2023-11-07 17:57:51 +05:30
Andrew Bastin
97bd808431 chore: set versions with a bump version 2023-11-07 16:04:02 +05:30
Andrew Bastin
a13c2fd4c1 chore: pin @codemirror/language to 6.9.0 2023-11-07 15:57:19 +05:30
Andrew Bastin
16044b5840 feat: desktop app
Co-authored-by: Vivek R <123vivekr@gmail.com>
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-11-07 14:20:03 +05:30
Andrew Bastin
93ce86f32d chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-11-06 18:56:01 +05:30
Andrew Bastin
4ebf850cb6 chore: bump version to 2023.8.3 2023-11-06 17:39:31 +05:30
Balu Babu
76af7d5e10 fix: mailer template issue (#3475) 2023-11-06 17:25:36 +05:30
Joel Jacob Stephen
507fe69efe feat: new banner service and added ability to bind additional services from other platforms (#3474)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-11-06 11:41:19 +05:30
Joel Jacob Stephen
23e3739718 feat: introducing a new smart table hoppscotch ui component (#3178)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-11-06 11:31:55 +05:30
Nicolas Merget
5428a73811 fix: add optional chaining for teamMembers to handle undefined team (#3484)
Co-authored-by: James George <jamesgeorge998001@gmail.com>
2023-11-06 11:25:39 +05:30
Anwarul Islam
4a154e6569 chore: fix spelling mistake on type import (#3487) 2023-11-06 11:25:03 +05:30
Liyas Thomas
0aa5825d8b fix: cleanup ui and improve consistency in input elements (#3494) 2023-11-06 10:56:15 +05:30
Andrew Bastin
bdb63e99d5 fix: pin @lezer/highlight to 1.1.4 to prevent page breaks 2023-11-03 23:30:46 +05:30
Andrew Bastin
6daa043a1b chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-11-03 10:12:54 +05:30
James George
8175ec640a chore(data): bump dependencies (#3473) 2023-11-02 23:53:52 +05:30
James George
b5307e4a89 chore(common): implement enforced pre-commit type checks for FE service files (#3472) 2023-11-02 23:37:27 +05:30
Akash K
19294802be fix: graphql page crashing and broken syntax highlighting (#3488) 2023-11-02 23:10:37 +05:30
Andrew Bastin
cbe3e14b47 refactor: versioning and migration mechanism for public data structures (#3457)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-11-02 18:54:16 +05:30
Joel Jacob Stephen
9dcbc4a126 refactor: updated dashboard gql queries and components to use the new infra type of the updated schema (#3455) 2023-11-01 23:40:19 +05:30
Gaurav K P
01df1663ad fix(common): handle false negatives in url validation (#3465) 2023-11-01 22:23:33 +05:30
Anwarul Islam
a215860782 feat: replacing windicss by tailwindcss in hoppscotch-ui (#3076)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
Co-authored-by: Joel Jacob Stephen <70131076+JoelJacobStephen@users.noreply.github.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
2023-11-01 20:55:08 +05:30
Nivedin
abd5288da8 refactor: move sentry to platform (#3451) 2023-11-01 18:17:55 +05:30
Michel Tomas
a89bc473f6 fix(self-hosted/web): add "useCredentials: true" to Vite PWA options (#3460) 2023-11-01 09:46:20 +05:30
Mir Arif Hasan
59b5a50a97 HBE-296 feat: introducing 'infra' type and splitting model properties between 'admin' and 'infra' (#3445)
* feat: infra type added in admin module

* feat: infra-resolver added in admin module

* feat: feedback resolved

* feat: deprecated tag added in some admin ResolveFields

* build: update pnpm-lock file

* feat: add field in infra type

* feat: admin extends user partially

* feat: admin extends user with omitting some fields

* chore: remove unused imports

* build: conflict resolve in pnpm lock file
2023-10-30 17:03:43 +06:00
Andrew Bastin
57cb59027b chore: bump codemirror dependencies 2023-10-19 13:37:07 +05:30
Andrew Bastin
d1c9c3583f chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0 2023-10-19 09:34:49 +05:30
James George
2462492c86 chore(cli): bump dependencies (#3441)
* chore: bump CLI dependencies

* chore: update package.json

Bump version and specify minimum Node.js version
2023-10-16 18:23:22 +05:30
Joel Jacob Stephen
7a9f0c8756 refactor: improvements to the auth implementation in admin dashboard (#3444)
* refactor: abstract axios queries to a separate helper file

* chore: delete unnecessary file

* chore: remove unnecessary console logs

* refactor: updated urls for api and authquery helpers

* refactor: updated auth implementation

* refactor: use default axios instance

* chore: improve code readability

* refactor: separate instances for rest and gql calls

* refactor: removed async await from functions that do not need them

* refactor: removed probable login and probable user from the auth system

* refactor: better error handling in login component

* chore: deleted unnecessary files and restructured some files

* feat: new errors file with typed error message formats

* refactor: removed unwanted usage of async await

* refactor: optimizing the usage and return of promises in auth flow

* refactor: convey boolean return type in a better way

* chore: apply suggestions

* refactor: handle case when mailcatcher is not active

---------

Co-authored-by: nivedin <nivedinp@gmail.com>
Co-authored-by: James George <jamesgeorge998001@gmail.com>
2023-10-16 18:14:02 +05:30
Balu Babu
46caf9b198 refactor: removed all instances of rejectOnNotFound in prisma queries (#3377)
* chore: removed rejectOnNotFound property from prisma query in team-enviroment method

* chore: fixed issues with test cases in team-environment module

* chore: changed target of hoppscotch-old-backend service back to prod
2023-10-16 14:04:03 +05:30
612 changed files with 36627 additions and 13202 deletions

View File

@@ -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

View File

@@ -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"
]
}

View 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
}

View 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} /
}
}

View File

@@ -1,11 +0,0 @@
:3000 {
try_files {path} /
root * /site/selfhost-web
file_server
}
:3100 {
try_files {path} /
root * /site/sh-admin
file_server
}

View File

@@ -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) => {

View File

@@ -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

View File

@@ -32,6 +32,9 @@
"lint-staged": "12.4.0"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

View File

@@ -17,12 +17,12 @@
"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": "^3.29.3",
"rollup-plugin-dts": "^6.0.2",

View File

@@ -0,0 +1,3 @@
:80 :3170 {
reverse_proxy localhost:8080
}

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.8.2",
"version": "2023.8.4-1",
"description": "",
"author": "",
"private": true,

View File

@@ -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;

View File

@@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "TeamCollection" ADD COLUMN "data" JSONB;
-- AlterTable
ALTER TABLE "UserCollection" ADD COLUMN "data" JSONB;

View File

@@ -43,6 +43,7 @@ model TeamInvitation {
model TeamCollection {
id String @id @default(cuid())
parentID String?
data Json?
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
children TeamCollection[] @relation("TeamCollectionChildParent")
requests TeamRequest[]
@@ -68,10 +69,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 @default(now()) @updatedAt
@@unique(fields: [id, creatorUid], name: "creator_uid_shortcode_unique")
}
@@ -102,6 +106,7 @@ model User {
currentGQLSession Json?
createdOn DateTime @default(now()) @db.Timestamp(3)
invitedUsers InvitedUsers[]
shortcodes Shortcode[]
}
model Account {
@@ -192,6 +197,7 @@ model UserCollection {
userUid String
user User @relation(fields: [userUid], references: [uid], onDelete: Cascade)
title String
data Json?
orderIndex Int
type ReqType
createdOn DateTime @default(now()) @db.Timestamp(3)

View 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);
});

View File

@@ -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',
]) {}

View File

@@ -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 {}

View File

@@ -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, {

View File

@@ -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[] = [

View File

@@ -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);
}
}

View 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;
}

View 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,
);
}
}

View File

@@ -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}`,

View File

@@ -254,6 +254,13 @@ export const TEAM_COLL_INVALID_JSON = 'team_coll/invalid_json';
*/
export const TEAM_NOT_OWNER = 'team_coll/team_not_owner' as const;
/**
* The Team Collection data is not valid
* (TeamCollectionService)
*/
export const TEAM_COLL_DATA_INVALID =
'team_coll/team_coll_data_invalid' as const;
/**
* Tried to perform an action on a request that doesn't accept their member role level
* (GqlRequestTeamMemberGuard)
@@ -318,18 +325,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)
@@ -597,6 +592,13 @@ export const USER_COLL_REORDERING_FAILED =
export const USER_COLL_SAME_NEXT_COLL =
'user_coll/user_collection_and_next_user_collection_are_same' as const;
/**
* The User Collection data is not valid
* (UserCollectionService)
*/
export const USER_COLL_DATA_INVALID =
'user_coll/user_coll_data_invalid' as const;
/**
* The User Collection does not belong to the logged-in user
* (UserCollectionService)
@@ -621,3 +623,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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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';
}
}

View File

@@ -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,

View File

@@ -21,8 +21,8 @@ import {
} from 'src/team-request/team-request.model';
import { TeamInvitation } from 'src/team-invitation/team-invitation.model';
import { InvitedUser } from '../admin/invited-user.model';
import { UserCollection } from '@prisma/client';
import {
UserCollection,
UserCollectionRemovedData,
UserCollectionReorderData,
} from 'src/user-collection/user-collections.model';
@@ -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;
};

View File

@@ -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;
}

View File

@@ -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],
})

View File

@@ -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,

View File

@@ -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);
});
});
});

View File

@@ -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;
}
}

View File

@@ -14,6 +14,13 @@ export class CreateRootTeamCollectionArgs {
@Field({ name: 'title', description: 'Title of the new collection' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -26,6 +33,13 @@ export class CreateChildTeamCollectionArgs {
@Field({ name: 'childTitle', description: 'Title of the new collection' })
childTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -33,12 +47,14 @@ export class RenameTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
newTitle: string;
}
@@ -98,3 +114,26 @@ export class ReplaceTeamCollectionArgs {
})
parentCollectionID?: string;
}
@ArgsType()
export class UpdateTeamCollectionArgs {
@Field(() => ID, {
name: 'collectionID',
description: 'ID of the collection',
})
collectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -12,12 +12,17 @@ export class TeamCollection {
})
title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ID, {
description: 'ID of the collection',
nullable: true,
})
parentID: string;
teamID: string;
}
@ObjectType()

View File

@@ -25,6 +25,7 @@ import {
MoveTeamCollectionArgs,
RenameTeamCollectionArgs,
ReplaceTeamCollectionArgs,
UpdateTeamCollectionArgs,
UpdateTeamCollectionOrderArgs,
} from './input-type.args';
import * as E from 'fp-ts/Either';
@@ -141,7 +142,14 @@ export class TeamCollectionResolver {
);
if (E.isLeft(teamCollections)) throwErr(teamCollections.left);
return teamCollections.right;
return <TeamCollection>{
id: teamCollections.right.id,
title: teamCollections.right.title,
parentID: teamCollections.right.parentID,
data: !teamCollections.right.data
? null
: JSON.stringify(teamCollections.right.data),
};
}
// Mutations
@@ -155,6 +163,7 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection(
args.teamID,
args.title,
args.data,
null,
);
@@ -230,6 +239,7 @@ export class TeamCollectionResolver {
const teamCollection = await this.teamCollectionService.createCollection(
team.right.id,
args.childTitle,
args.data,
args.collectionID,
);
@@ -239,6 +249,7 @@ export class TeamCollectionResolver {
@Mutation(() => TeamCollection, {
description: 'Rename a collection',
deprecationReason: 'Switch to updateTeamCollection mutation instead',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
@@ -303,6 +314,23 @@ export class TeamCollectionResolver {
return request.right;
}
@Mutation(() => TeamCollection, {
description: 'Update Team Collection details',
})
@UseGuards(GqlAuthGuard, GqlCollectionTeamMemberGuard)
@RequiresTeamRole(TeamMemberRole.OWNER, TeamMemberRole.EDITOR)
async updateTeamCollection(@Args() args: UpdateTeamCollectionArgs) {
const updatedTeamCollection =
await this.teamCollectionService.updateTeamCollection(
args.collectionID,
args.data,
args.newTitle,
);
if (E.isLeft(updatedTeamCollection)) throwErr(updatedTeamCollection.left);
return updatedTeamCollection.right;
}
// Subscriptions
@Subscription(() => TeamCollection, {

View File

@@ -1,6 +1,7 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mockDeep, mockReset } from 'jest-mock-extended';
import {
TEAM_COLL_DATA_INVALID,
TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON,
TEAM_COLL_IS_PARENT_COLL,
@@ -17,6 +18,7 @@ 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';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -51,35 +53,60 @@ const rootTeamCollection: DBTeamCollection = {
id: '123',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollectionsCasted: TeamCollection = {
id: rootTeamCollection.id,
title: rootTeamCollection.title,
parentID: rootTeamCollection.parentID,
data: JSON.stringify(rootTeamCollection.data),
};
const rootTeamCollection_2: DBTeamCollection = {
id: 'erv',
orderIndex: 2,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const rootTeamCollection_2Casted: TeamCollection = {
id: 'erv',
parentID: null,
data: JSON.stringify(rootTeamCollection_2.data),
title: 'Root Collection 1',
};
const childTeamCollection: DBTeamCollection = {
id: 'rfe',
orderIndex: 1,
parentID: rootTeamCollection.id,
data: {},
title: 'Child Collection 1',
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
};
const childTeamCollectionCasted: TeamCollection = {
id: 'rfe',
parentID: rootTeamCollection.id,
data: JSON.stringify(childTeamCollection.data),
title: 'Child Collection 1',
};
const childTeamCollection_2: DBTeamCollection = {
id: 'bgdz',
orderIndex: 1,
data: {},
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
teamID: team.id,
@@ -87,11 +114,20 @@ const childTeamCollection_2: DBTeamCollection = {
updatedOn: currentTime,
};
const childTeamCollection_2Casted: TeamCollection = {
id: 'bgdz',
data: JSON.stringify(childTeamCollection_2.data),
parentID: rootTeamCollection_2.id,
title: 'Child Collection 1',
};
const rootTeamCollectionList: DBTeamCollection[] = [
{
id: 'fdv',
orderIndex: 1,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -102,6 +138,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -111,6 +149,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -119,6 +159,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
id: 'bre3',
orderIndex: 4,
parentID: null,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -129,6 +171,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 5,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -139,6 +183,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -148,6 +194,8 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
@@ -156,6 +204,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 8,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -165,6 +214,7 @@ const rootTeamCollectionList: DBTeamCollection[] = [
orderIndex: 9,
parentID: null,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -175,17 +225,83 @@ const rootTeamCollectionList: DBTeamCollection[] = [
parentID: null,
title: 'Root Collection 1',
teamID: team.id,
data: {},
createdOn: currentTime,
updatedOn: currentTime,
},
];
const rootTeamCollectionListCasted: TeamCollection[] = [
{
id: 'fdv',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fbbg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'fgbfg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: 'bre3',
parentID: null,
data: JSON.stringify(rootTeamCollection.data),
title: 'Root Collection 1',
},
{
id: 'hghgf',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '123',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '54tyh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '234re',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '34rtg',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
{
id: '45tgh',
parentID: null,
title: 'Root Collection 1',
data: JSON.stringify(rootTeamCollection.data),
},
];
const childTeamCollectionList: DBTeamCollection[] = [
{
id: '123',
orderIndex: 1,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -195,6 +311,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 2,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -204,6 +322,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
orderIndex: 3,
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: {},
teamID: team.id,
createdOn: currentTime,
updatedOn: currentTime,
@@ -212,6 +332,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '567',
orderIndex: 4,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -221,6 +343,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '123',
orderIndex: 5,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -230,6 +354,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '678',
orderIndex: 6,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -239,6 +365,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '789',
orderIndex: 7,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -248,6 +376,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '890',
orderIndex: 8,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -257,6 +387,7 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '012',
orderIndex: 9,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -266,6 +397,8 @@ const childTeamCollectionList: DBTeamCollection[] = [
id: '0bhu',
orderIndex: 10,
parentID: rootTeamCollection.id,
data: {},
title: 'Root Collection 1',
teamID: team.id,
createdOn: currentTime,
@@ -273,6 +406,75 @@ const childTeamCollectionList: DBTeamCollection[] = [
},
];
const childTeamCollectionListCasted: TeamCollection[] = [
{
id: '123',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '345',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '456',
parentID: rootTeamCollection.id,
title: 'Root Collection 1',
data: JSON.stringify({}),
},
{
id: '567',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '123',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '678',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '789',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '890',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '012',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
{
id: '0bhu',
parentID: rootTeamCollection.id,
data: JSON.stringify({}),
title: 'Root Collection 1',
},
];
beforeEach(() => {
mockReset(mockPrisma);
mockPubSub.publish.mockClear();
@@ -311,7 +513,7 @@ describe('getParentOfCollection', () => {
const result = await teamCollectionService.getParentOfCollection(
childTeamCollection.id,
);
expect(result).toEqual(rootTeamCollection);
expect(result).toEqual(rootTeamCollectionsCasted);
});
test('should return null successfully for a root collection with valid collectionID', async () => {
@@ -347,7 +549,7 @@ describe('getChildrenOfCollection', () => {
null,
10,
);
expect(result).toEqual(childTeamCollectionList);
expect(result).toEqual(childTeamCollectionListCasted);
});
test('should return a list of 3 child collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -363,9 +565,9 @@ describe('getChildrenOfCollection', () => {
10,
);
expect(result).toEqual([
{ ...childTeamCollectionList[7] },
{ ...childTeamCollectionList[8] },
{ ...childTeamCollectionList[9] },
{ ...childTeamCollectionListCasted[7] },
{ ...childTeamCollectionListCasted[8] },
{ ...childTeamCollectionListCasted[9] },
]);
});
@@ -392,7 +594,7 @@ describe('getTeamRootCollections', () => {
null,
10,
);
expect(result).toEqual(rootTeamCollectionList);
expect(result).toEqual(rootTeamCollectionListCasted);
});
test('should return a list of 3 root collections successfully with cursor being equal to the 7th item in the list', async () => {
@@ -408,9 +610,9 @@ describe('getTeamRootCollections', () => {
10,
);
expect(result).toEqual([
{ ...rootTeamCollectionList[7] },
{ ...rootTeamCollectionList[8] },
{ ...rootTeamCollectionList[9] },
{ ...rootTeamCollectionListCasted[7] },
{ ...rootTeamCollectionListCasted[8] },
{ ...rootTeamCollectionListCasted[9] },
]);
});
@@ -464,6 +666,7 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'ab',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
@@ -478,11 +681,27 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_NOT_OWNER);
});
test('should throw TEAM_COLL_DATA_INVALID when parent TeamCollection does not belong to the team', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
rootTeamCollection,
);
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcd',
'{',
rootTeamCollection.id,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should successfully create a new root TeamCollection with valid inputs', async () => {
// isOwnerCheck
mockPrisma.teamCollection.findFirstOrThrow.mockResolvedValueOnce(
@@ -496,9 +715,10 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualRight(rootTeamCollection);
expect(result).toEqualRight(rootTeamCollectionsCasted);
});
test('should successfully create a new child TeamCollection with valid inputs', async () => {
@@ -514,9 +734,10 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(result).toEqualRight(childTeamCollection);
expect(result).toEqualRight(childTeamCollectionCasted);
});
test('should send pubsub message to "team_coll/<teamID>/coll_added" if child TeamCollection is created successfully', async () => {
@@ -532,11 +753,13 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
childTeamCollection.teamID,
childTeamCollection.title,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_added`,
childTeamCollection,
childTeamCollectionCasted,
);
});
@@ -553,11 +776,13 @@ describe('createCollection', () => {
const result = await teamCollectionService.createCollection(
rootTeamCollection.teamID,
'abcdefg',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.id,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollection,
rootTeamCollectionsCasted,
);
});
});
@@ -587,7 +812,7 @@ describe('renameCollection', () => {
'NewTitle',
);
expect(result).toEqualRight({
...rootTeamCollection,
...rootTeamCollectionsCasted,
title: 'NewTitle',
});
});
@@ -625,7 +850,7 @@ describe('renameCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
{
...rootTeamCollection,
...rootTeamCollectionsCasted,
title: 'NewTitle',
},
);
@@ -832,9 +1057,8 @@ describe('moveCollection', () => {
null,
);
expect(result).toEqualRight({
...childTeamCollection,
...childTeamCollectionCasted,
parentID: null,
orderIndex: 2,
});
});
@@ -890,9 +1114,8 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollection,
...childTeamCollectionCasted,
parentID: null,
orderIndex: 2,
},
);
});
@@ -931,9 +1154,8 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
});
});
@@ -973,9 +1195,8 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection_2.teamID}/coll_moved`,
{
...rootTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
...rootTeamCollectionsCasted,
parentID: childTeamCollection_2Casted.id,
},
);
});
@@ -1014,9 +1235,8 @@ describe('moveCollection', () => {
childTeamCollection_2.id,
);
expect(result).toEqualRight({
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
});
});
@@ -1056,9 +1276,8 @@ describe('moveCollection', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollection.teamID}/coll_moved`,
{
...childTeamCollection,
parentID: childTeamCollection_2.id,
orderIndex: 1,
...childTeamCollectionCasted,
parentID: childTeamCollection_2Casted.id,
},
);
});
@@ -1154,7 +1373,7 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[4].teamID}/coll_order_updated`,
{
collection: rootTeamCollectionList[4],
collection: rootTeamCollectionListCasted[4],
nextCollection: null,
},
);
@@ -1235,8 +1454,8 @@ describe('updateCollectionOrder', () => {
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${childTeamCollectionList[2].teamID}/coll_order_updated`,
{
collection: childTeamCollectionList[4],
nextCollection: childTeamCollectionList[2],
collection: childTeamCollectionListCasted[4],
nextCollection: childTeamCollectionListCasted[2],
},
);
});
@@ -1302,7 +1521,7 @@ describe('importCollectionsFromJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollection,
rootTeamCollectionsCasted,
);
});
});
@@ -1421,7 +1640,7 @@ describe('replaceCollectionsWithJSON', () => {
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_added`,
rootTeamCollection,
rootTeamCollectionsCasted,
);
});
});
@@ -1458,4 +1677,64 @@ describe('totalCollectionsInTeam', () => {
});
});
describe('updateTeamCollection', () => {
test('should throw TEAM_COLL_SHORT_TITLE if title is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
'de',
);
expect(result).toEqualLeft(TEAM_COLL_SHORT_TITLE);
});
test('should throw TEAM_COLL_DATA_INVALID is collection data is invalid', async () => {
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
'{',
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_DATA_INVALID);
});
test('should throw TEAM_COLL_NOT_FOUND is collectionID is invalid', async () => {
mockPrisma.teamCollection.update.mockRejectedValueOnce('RecordNotFound');
const result = await teamCollectionService.updateTeamCollection(
'invalid_id',
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(result).toEqualLeft(TEAM_COLL_NOT_FOUND);
});
test('should successfully update a collection', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify({ foo: 'bar' }),
'new_title',
);
expect(result).toEqualRight({
data: JSON.stringify({ foo: 'bar' }),
title: 'new_title',
...rootTeamCollectionsCasted,
});
});
test('should send pubsub message to "team_coll/<teamID>/coll_updated" if TeamCollection is updated successfully', async () => {
mockPrisma.teamCollection.update.mockResolvedValueOnce(rootTeamCollection);
const result = await teamCollectionService.updateTeamCollection(
rootTeamCollection.id,
JSON.stringify(rootTeamCollection.data),
rootTeamCollection.title,
);
expect(mockPubSub.publish).toHaveBeenCalledWith(
`team_coll/${rootTeamCollection.teamID}/coll_updated`,
rootTeamCollectionsCasted,
);
});
});
//ToDo: write test cases for exportCollectionsToJSON

View File

@@ -13,6 +13,7 @@ import {
TEAM_COLL_IS_PARENT_COLL,
TEAM_COL_SAME_NEXT_COLL,
TEAM_COL_REORDERING_FAILED,
TEAM_COLL_DATA_INVALID,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils';
@@ -69,6 +70,7 @@ export class TeamCollectionService {
this.generatePrismaQueryObjForFBCollFolder(f, teamID, index + 1),
),
},
data: folder.data ?? undefined,
};
}
@@ -118,6 +120,7 @@ export class TeamCollectionService {
name: collection.right.title,
folders: childrenCollectionObjects,
requests: requests.map((x) => x.request),
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -198,8 +201,11 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
teamCollections.forEach((collection) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collection),
),
);
return E.right(true);
@@ -268,8 +274,11 @@ export class TeamCollectionService {
),
);
teamCollections.forEach((x) =>
this.pubsub.publish(`team_coll/${destTeamID}/coll_added`, x),
teamCollections.forEach((collections) =>
this.pubsub.publish(
`team_coll/${destTeamID}/coll_added`,
this.cast(collections),
),
);
return E.right(true);
@@ -277,11 +286,17 @@ export class TeamCollectionService {
/**
* Typecast a database TeamCollection to a TeamCollection model
*
* @param teamCollection database TeamCollection
* @returns TeamCollection model
*/
private cast(teamCollection: DBTeamCollection): TeamCollection {
return <TeamCollection>{ ...teamCollection };
return <TeamCollection>{
id: teamCollection.id,
title: teamCollection.title,
parentID: teamCollection.parentID,
data: !teamCollection.data ? null : JSON.stringify(teamCollection.data),
};
}
/**
@@ -324,7 +339,7 @@ export class TeamCollectionService {
});
if (!teamCollection) return null;
return teamCollection.parent;
return !teamCollection.parent ? null : this.cast(teamCollection.parent);
}
/**
@@ -335,12 +350,12 @@ export class TeamCollectionService {
* @param take Number of items we want returned
* @returns A list of child collections
*/
getChildrenOfCollection(
async getChildrenOfCollection(
collectionID: string,
cursor: string | null,
take: number,
) {
return this.prisma.teamCollection.findMany({
const res = await this.prisma.teamCollection.findMany({
where: {
parentID: collectionID,
},
@@ -351,6 +366,12 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return childCollections;
}
/**
@@ -366,7 +387,7 @@ export class TeamCollectionService {
cursor: string | null,
take: number,
) {
return this.prisma.teamCollection.findMany({
const res = await this.prisma.teamCollection.findMany({
where: {
teamID,
parentID: null,
@@ -378,6 +399,12 @@ export class TeamCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const teamCollections = res.map((teamCollection) =>
this.cast(teamCollection),
);
return teamCollections;
}
/**
@@ -470,6 +497,7 @@ export class TeamCollectionService {
async createCollection(
teamID: string,
title: string,
data: string | null = null,
parentTeamCollectionID: string | null,
) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
@@ -481,6 +509,13 @@ export class TeamCollectionService {
if (O.isNone(isOwner)) return E.left(TEAM_NOT_OWNER);
}
if (data === '') return E.left(TEAM_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
data = jsonReq.right;
}
const isParent = parentTeamCollectionID
? {
connect: {
@@ -498,18 +533,23 @@ export class TeamCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentTeamCollectionID
? (await this.getRootCollectionsCount(teamID)) + 1
: (await this.getChildCollectionsCount(parentTeamCollectionID)) + 1,
},
});
this.pubsub.publish(`team_coll/${teamID}/coll_added`, teamCollection);
this.pubsub.publish(
`team_coll/${teamID}/coll_added`,
this.cast(teamCollection),
);
return E.right(this.cast(teamCollection));
}
/**
* @deprecated Use updateTeamCollection method instead
* Update the title of a TeamCollection
*
* @param collectionID The Collection ID
@@ -532,10 +572,10 @@ export class TeamCollectionService {
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
updatedTeamCollection,
this.cast(updatedTeamCollection),
);
return E.right(updatedTeamCollection);
return E.right(this.cast(updatedTeamCollection));
} catch (error) {
return E.left(TEAM_COLL_NOT_FOUND);
}
@@ -694,8 +734,8 @@ export class TeamCollectionService {
* @returns An Option of boolean, is parent or not
*/
private async isParent(
collection: TeamCollection,
destCollection: TeamCollection,
collection: DBTeamCollection,
destCollection: DBTeamCollection,
): Promise<O.Option<boolean>> {
//* Recursively check if collection is a parent by going up the tree of child-parent collections until we reach a root collection i.e parentID === null
//* Valid condition, isParent returns false
@@ -971,4 +1011,49 @@ export class TeamCollectionService {
const teamCollectionsCount = this.prisma.teamCollection.count();
return teamCollectionsCount;
}
/**
* Update Team Collection details
*
* @param collectionID Collection ID
* @param collectionData new header data in a JSONified string form
* @param newTitle New title of the collection
* @returns Updated TeamCollection
*/
async updateTeamCollection(
collectionID: string,
collectionData: string = null,
newTitle: string = null,
) {
try {
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(TEAM_COLL_SHORT_TITLE);
}
if (collectionData === '') return E.left(TEAM_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(TEAM_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
const updatedTeamCollection = await this.prisma.teamCollection.update({
where: { id: collectionID },
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`team_coll/${updatedTeamCollection.teamID}/coll_updated`,
this.cast(updatedTeamCollection),
);
return E.right(this.cast(updatedTeamCollection));
} catch (e) {
return E.left(TEAM_COLL_NOT_FOUND);
}
}
}

View File

@@ -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,
);

View File

@@ -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({

View File

@@ -42,6 +42,7 @@ const teamCollection: DbTeamCollection = {
id: 'team-coll-1',
parentID: null,
teamID: team.id,
data: {},
title: 'Team Collection 1',
orderIndex: 1,
createdOn: new Date(),

View File

@@ -1,6 +1,8 @@
// This interface defines how data will be received from the app when we are importing Hoppscotch collections
export interface CollectionFolder {
id?: string;
folders: CollectionFolder[];
requests: any[];
name: string;
data?: string;
}

View File

@@ -6,6 +6,13 @@ import { PaginationArgs } from 'src/types/input-types.args';
export class CreateRootUserCollectionArgs {
@Field({ name: 'title', description: 'Title of the new user collection' })
title: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
export class CreateChildUserCollectionArgs {
@@ -17,6 +24,13 @@ export class CreateChildUserCollectionArgs {
description: 'ID of the parent to the new user collection',
})
parentUserCollectionID: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}
@ArgsType()
@@ -95,3 +109,26 @@ export class ImportUserCollectionsFromJSONArgs {
})
parentCollectionID?: string;
}
@ArgsType()
export class UpdateUserCollectionsArgs {
@Field(() => ID, {
name: 'userCollectionID',
description: 'ID of the user collection',
})
userCollectionID: string;
@Field({
name: 'newTitle',
description: 'The updated title of the user collection',
nullable: true,
})
newTitle: string;
@Field({
name: 'data',
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
}

View File

@@ -30,6 +30,7 @@ import {
MoveUserCollectionArgs,
RenameUserCollectionsArgs,
UpdateUserCollectionArgs,
UpdateUserCollectionsArgs,
} from './input-type.args';
import { ReqType } from 'src/types/RequestTypes';
import * as E from 'fp-ts/Either';
@@ -142,7 +143,13 @@ export class UserCollectionResolver {
);
if (E.isLeft(userCollection)) throwErr(userCollection.left);
return userCollection.right;
return <UserCollection>{
...userCollection.right,
userID: userCollection.right.userUid,
data: !userCollection.right.data
? null
: JSON.stringify(userCollection.right.data),
};
}
@Query(() => UserCollectionExportJSONData, {
@@ -191,6 +198,7 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.REST,
);
@@ -212,6 +220,7 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
null,
ReqType.GQL,
);
@@ -232,6 +241,7 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.GQL,
);
@@ -252,6 +262,7 @@ export class UserCollectionResolver {
await this.userCollectionService.createUserCollection(
user,
args.title,
args.data,
args.parentUserCollectionID,
ReqType.REST,
);
@@ -359,6 +370,26 @@ export class UserCollectionResolver {
return importedCollection.right;
}
@Mutation(() => UserCollection, {
description: 'Update a UserCollection',
})
@UseGuards(GqlAuthGuard)
async updateUserCollection(
@GqlUser() user: AuthUser,
@Args() args: UpdateUserCollectionsArgs,
) {
const updatedUserCollection =
await this.userCollectionService.updateUserCollection(
args.newTitle,
args.data,
args.userCollectionID,
user.uid,
);
if (E.isLeft(updatedUserCollection)) throwErr(updatedUserCollection.left);
return updatedUserCollection.right;
}
// Subscriptions
@Subscription(() => UserCollection, {
description: 'Listen for User Collection Creation',

View File

@@ -12,6 +12,7 @@ import {
USER_NOT_FOUND,
USER_NOT_OWNER,
USER_COLL_INVALID_JSON,
USER_COLL_DATA_INVALID,
} from 'src/errors';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -43,8 +44,12 @@ export class UserCollectionService {
*/
private cast(collection: UserCollection) {
return <UserCollectionModel>{
...collection,
id: collection.id,
title: collection.title,
type: collection.type,
parentID: collection.parentID,
userID: collection.userUid,
data: !collection.data ? null : JSON.stringify(collection.data),
};
}
@@ -146,7 +151,7 @@ export class UserCollectionService {
},
});
return parent;
return !parent ? null : this.cast(parent);
}
/**
@@ -164,7 +169,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
const res = await this.prisma.userCollection.findMany({
where: {
parentID: collectionID,
type: type,
@@ -176,6 +181,12 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
}
/**
@@ -211,12 +222,20 @@ export class UserCollectionService {
async createUserCollection(
user: AuthUser,
title: string,
data: string | null = null,
parentUserCollectionID: string | null,
type: ReqType,
) {
const isTitleValid = isValidLength(title, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
if (data === '') return E.left(USER_COLL_DATA_INVALID);
if (data) {
const jsonReq = stringToJson(data);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
data = jsonReq.right;
}
// If creating a child collection
if (parentUserCollectionID !== null) {
const parentCollection = await this.getUserCollection(
@@ -251,15 +270,19 @@ export class UserCollectionService {
},
},
parent: isParent,
data: data ?? undefined,
orderIndex: !parentUserCollectionID
? (await this.getRootCollectionsCount(user.uid)) + 1
: (await this.getChildCollectionsCount(parentUserCollectionID)) + 1,
},
});
await this.pubsub.publish(`user_coll/${user.uid}/created`, userCollection);
await this.pubsub.publish(
`user_coll/${user.uid}/created`,
this.cast(userCollection),
);
return E.right(userCollection);
return E.right(this.cast(userCollection));
}
/**
@@ -276,7 +299,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
const res = await this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: null,
@@ -289,6 +312,12 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const userCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return userCollections;
}
/**
@@ -307,7 +336,7 @@ export class UserCollectionService {
take: number,
type: ReqType,
) {
return this.prisma.userCollection.findMany({
const res = await this.prisma.userCollection.findMany({
where: {
userUid: user.uid,
parentID: userCollectionID,
@@ -317,9 +346,16 @@ export class UserCollectionService {
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
});
const childCollections = res.map((childCollection) =>
this.cast(childCollection),
);
return childCollections;
}
/**
* @deprecated Use updateUserCollection method instead
* Update the title of a UserCollection
*
* @param newTitle The new title of collection
@@ -351,10 +387,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
updatedUserCollection,
this.cast(updatedUserCollection),
);
return E.right(updatedUserCollection);
return E.right(this.cast(updatedUserCollection));
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
@@ -591,10 +627,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
updatedCollection.right,
this.cast(updatedCollection.right),
);
return E.right(updatedCollection.right);
return E.right(this.cast(updatedCollection.right));
}
// destCollectionID != null i.e move into another collection
@@ -642,10 +678,10 @@ export class UserCollectionService {
this.pubsub.publish(
`user_coll/${collection.right.userUid}/moved`,
updatedCollection.right,
this.cast(updatedCollection.right),
);
return E.right(updatedCollection.right);
return E.right(this.cast(updatedCollection.right));
}
/**
@@ -846,6 +882,7 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
data: JSON.stringify(collection.right.data),
};
return E.right(result);
@@ -918,6 +955,7 @@ export class UserCollectionService {
...(x.request as Record<string, unknown>), // type casting x.request of type Prisma.JSONValue to an object to enable spread
};
}),
data: JSON.stringify(parentCollection.right.data),
}),
collectionType: parentCollection.right.type,
});
@@ -971,6 +1009,7 @@ export class UserCollectionService {
this.generatePrismaQueryObj(f, userID, index + 1, reqType),
),
},
data: folder.data ?? undefined,
};
}
@@ -1040,10 +1079,63 @@ export class UserCollectionService {
),
);
userCollections.forEach((x) =>
this.pubsub.publish(`user_coll/${userID}/created`, x),
userCollections.forEach((collection) =>
this.pubsub.publish(`user_coll/${userID}/created`, this.cast(collection)),
);
return E.right(true);
}
/**
* Update a UserCollection
*
* @param newTitle The new title of collection
* @param userCollectionID The Collection Id
* @param userID The User UID
* @returns An Either of the updated UserCollection
*/
async updateUserCollection(
newTitle: string = null,
collectionData: string | null = null,
userCollectionID: string,
userID: string,
) {
if (collectionData === '') return E.left(USER_COLL_DATA_INVALID);
if (collectionData) {
const jsonReq = stringToJson(collectionData);
if (E.isLeft(jsonReq)) return E.left(USER_COLL_DATA_INVALID);
collectionData = jsonReq.right;
}
if (newTitle != null) {
const isTitleValid = isValidLength(newTitle, this.TITLE_LENGTH);
if (!isTitleValid) return E.left(USER_COLL_SHORT_TITLE);
}
// Check to see is the collection belongs to the user
const isOwner = await this.isOwnerCheck(userCollectionID, userID);
if (O.isNone(isOwner)) return E.left(USER_NOT_OWNER);
try {
const updatedUserCollection = await this.prisma.userCollection.update({
where: {
id: userCollectionID,
},
data: {
data: collectionData ?? undefined,
title: newTitle ?? undefined,
},
});
this.pubsub.publish(
`user_coll/${updatedUserCollection.userUid}/updated`,
this.cast(updatedUserCollection),
);
return E.right(this.cast(updatedUserCollection));
} catch (error) {
return E.left(USER_COLL_NOT_FOUND);
}
}
}

View File

@@ -13,6 +13,12 @@ export class UserCollection {
})
title: string;
@Field({
description: 'JSON string representing the collection data',
nullable: true,
})
data: string;
@Field(() => ReqType, {
description: 'Type of the user collection',
})

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
"**/src/__tests__/**/*.*.ts",
"**/src/__tests__/commands/**/*.*.ts",
],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.3.3",
"version": "0.4.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"main": "dist/index.js",
@@ -10,14 +10,18 @@
"publishConfig": {
"access": "public"
},
"engines": {
"node": ">=18"
},
"scripts": {
"build": "pnpm exec tsup",
"dev": "pnpm exec tsup --watch",
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"test": "pnpm run build && jest && rm -rf dist"
"do-test": "pnpm test"
},
"keywords": [
"cli",
@@ -38,24 +42,24 @@
"devDependencies": {
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181",
"@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",
"zod": "^3.22.2"
"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"
}
}

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error } = await execAsync(cmd);
const { error, stdout } = await execAsync(cmd);
expect(error).toBeNull();
});
@@ -129,7 +129,6 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,7 +1,6 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -12,7 +12,7 @@
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"preRequestScript": "",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,23 +6,24 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
import { flow, pipe } from "fp-ts/function";
import * as TE from "fp-ts/TaskEither";
import * as E from "fp-ts/Either";
import * as RA from "fp-ts/ReadonlyArray";
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { isHoppCLIError } from "./checks";
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
import { toFormData } from "./mutators";
import { getEffectiveFinalMetaData } from "./getters";
import { PreRequestMetrics } from "../types/response";
import { isHoppCLIError } from "./checks";
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
import { getEffectiveFinalMetaData } from "./getters";
import { toFormData } from "./mutators";
/**
* Runs pre-request-script runner over given request which extracts set ENVs and

View File

@@ -1,17 +1,19 @@
import { HoppRESTRequest } from "@hoppscotch/data";
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as TE from "fp-ts/TaskEither";
import * as RA from "fp-ts/ReadonlyArray";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { hrtime } from "process";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -36,7 +38,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
execTestScript(testScript, envs, response)
runTestScript(testScript, envs, response)
)
)
),

View File

@@ -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,8 +66,10 @@ 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",
},
],
eqeqeq: 1,
"no-else-return": 1,
},
}

View File

@@ -4,5 +4,5 @@ module.exports = {
singleQuote: false,
printWidth: 80,
useTabs: false,
tabWidth: 2
tabWidth: 2,
}

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1017 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 633 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>

After

Width:  |  Height:  |  Size: 610 B

View File

@@ -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;
@@ -15,13 +33,13 @@
::-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;
}
@@ -39,7 +57,7 @@ input::placeholder,
textarea::placeholder,
.cm-placeholder {
@apply text-secondary;
@apply opacity-50;
@apply opacity-50 #{!important};
}
input,
@@ -54,11 +72,11 @@ 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;
@apply leading-body;
@apply leading-body #{!important};
animation: fade 300ms forwards;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
@@ -124,8 +142,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;
@@ -140,7 +158,7 @@ a {
@apply shadow-none #{!important};
@apply fixed;
@apply inline-flex;
@apply -mt-7.5;
@apply -mt-8;
}
}
@@ -154,15 +172,15 @@ 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 leading-body;
@apply items-center;
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 +188,12 @@ a {
@apply truncate;
@apply sm:inline-flex;
}
.env-icon {
@apply transition;
@apply inline-flex;
@apply items-center;
}
}
.tippy-svg-arrow {
@@ -195,9 +219,9 @@ 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 leading-body;
@apply focus:outline-none;
scroll-behavior: smooth;
@@ -229,12 +253,12 @@ a {
hr {
@apply border-b border-dividerLight;
@apply my-2;
@apply my-2 #{!important};
}
.heading {
@apply font-bold;
@apply text-secondaryDark text-lg;
@apply text-lg text-secondaryDark;
@apply tracking-tight;
}
@@ -243,7 +267,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 +308,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 +317,7 @@ button {
.floating-input ~ .end-actions {
@apply absolute;
@apply right-0.2;
@apply right-[.05rem];
@apply inset-y-0;
@apply flex;
@apply items-center;
@@ -318,44 +342,28 @@ pre.ace_editor {
}
}
.select-wrapper {
@apply flex flex-1;
@apply relative;
@apply after:absolute;
@apply after:flex;
@apply after:inset-y-0;
@apply after:items-center;
@apply after:justify-center;
@apply after:pointer-events-none;
@apply after:font-icon;
@apply after:text-current;
@apply after:right-3;
@apply after:content-["\e5cf"];
@apply after:text-lg;
}
.info-response {
@apply text-pink-500;
color: var(--status-info-color);
}
.success-response {
@apply text-green-500;
color: var(--status-success-color);
}
.redir-response {
@apply text-yellow-500;
.redirect-response {
color: var(--status-redirect-color);
}
.cl-error-response {
@apply text-red-500;
.critical-error-response {
color: var(--status-critical-error-color);
}
.sv-error-response {
@apply text-red-600;
.server-error-response {
color: var(--status-server-error-color);
}
.missing-data-response {
@apply text-secondaryLight;
color: var(--status-missing-data-color);
}
.toasted-container {
@@ -366,7 +374,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 +402,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 +436,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,32 +509,16 @@ pre.ace_editor {
}
}
.cm-panel.cm-search [name="close"] {
@apply flex;
@apply items-center;
@apply justify-center;
@apply min-h-5;
@apply min-w-5;
@apply bg-primaryDark #{!important};
@apply sticky #{!important};
@apply right-0 #{!important};
@apply ml-auto #{!important};
@apply my-auto #{!important};
@apply rounded #{!important};
@apply outline #{!important};
@apply outline-divider #{!important};
}
.shortcut-key {
@apply inline-flex;
@apply font-sans;
@apply text-tiny;
@apply bg-divider;
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply px-1;
@apply min-w-5;
@apply min-h-5;
@apply min-w-[1.25rem];
@apply min-h-[1.25rem];
@apply items-center;
@apply justify-center;
@apply border border-dividerDark;

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -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;
}

View File

@@ -0,0 +1,89 @@
@mixin green-theme {
--accent-color: theme("colors.emerald.500");
--accent-light-color: theme("colors.emerald.400");
--accent-dark-color: theme("colors.emerald.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.emerald.400");
--gradient-via-color: theme("colors.emerald.500");
--gradient-to-color: theme("colors.emerald.600");
}
@mixin teal-theme {
--accent-color: theme("colors.teal.500");
--accent-light-color: theme("colors.teal.400");
--accent-dark-color: theme("colors.teal.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.teal.400");
--gradient-via-color: theme("colors.teal.500");
--gradient-to-color: theme("colors.teal.600");
}
@mixin blue-theme {
--accent-color: theme("colors.blue.500");
--accent-light-color: theme("colors.blue.400");
--accent-dark-color: theme("colors.blue.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.blue.400");
--gradient-via-color: theme("colors.blue.500");
--gradient-to-color: theme("colors.blue.600");
}
@mixin indigo-theme {
--accent-color: theme("colors.indigo.500");
--accent-light-color: theme("colors.indigo.400");
--accent-dark-color: theme("colors.indigo.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.indigo.400");
--gradient-via-color: theme("colors.indigo.500");
--gradient-to-color: theme("colors.indigo.600");
}
@mixin purple-theme {
--accent-color: theme("colors.purple.500");
--accent-light-color: theme("colors.purple.400");
--accent-dark-color: theme("colors.purple.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.purple.400");
--gradient-via-color: theme("colors.purple.500");
--gradient-to-color: theme("colors.purple.600");
}
@mixin yellow-theme {
--accent-color: theme("colors.amber.500");
--accent-light-color: theme("colors.amber.400");
--accent-dark-color: theme("colors.amber.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.amber.400");
--gradient-via-color: theme("colors.amber.500");
--gradient-to-color: theme("colors.amber.600");
}
@mixin orange-theme {
--accent-color: theme("colors.orange.500");
--accent-light-color: theme("colors.orange.400");
--accent-dark-color: theme("colors.orange.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.orange.400");
--gradient-via-color: theme("colors.orange.500");
--gradient-to-color: theme("colors.orange.600");
}
@mixin red-theme {
--accent-color: theme("colors.red.500");
--accent-light-color: theme("colors.red.400");
--accent-dark-color: theme("colors.red.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.red.400");
--gradient-via-color: theme("colors.red.500");
--gradient-to-color: theme("colors.red.600");
}
@mixin pink-theme {
--accent-color: theme("colors.pink.500");
--accent-light-color: theme("colors.pink.400");
--accent-dark-color: theme("colors.pink.600");
--accent-contrast-color: theme("colors.white");
--gradient-from-color: theme("colors.pink.400");
--gradient-via-color: theme("colors.pink.500");
--gradient-to-color: theme("colors.pink.600");
}

View File

@@ -0,0 +1,140 @@
@mixin base-theme {
--font-sans: "Inter Variable", sans-serif;
--font-mono: "Roboto Mono Variable", monospace;
--font-size-body: 0.75rem;
--font-size-tiny: 0.625rem;
--line-height-body: 1rem;
--upper-primary-sticky-fold: 4.125rem;
--upper-secondary-sticky-fold: 6.188rem;
--upper-tertiary-sticky-fold: 8.25rem;
--upper-fourth-sticky-fold: 10.2rem;
--upper-mobile-primary-sticky-fold: 6.75rem;
--upper-mobile-secondary-sticky-fold: 8.813rem;
--upper-mobile-sticky-fold: 10.875rem;
--upper-mobile-tertiary-sticky-fold: 8.25rem;
--lower-primary-sticky-fold: 3rem;
--lower-secondary-sticky-fold: 5.063rem;
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
}
@mixin light-theme {
--primary-color: theme("colors.white");
--primary-light-color: theme("colors.gray.50");
--primary-dark-color: theme("colors.gray.100");
--primary-contrast-color: #fdfdfd;
--secondary-color: theme("colors.gray.500");
--secondary-light-color: theme("colors.gray.400");
--secondary-dark-color: theme("colors.gray.900");
--divider-color: theme("colors.gray.100");
--divider-light-color: theme("colors.gray.100");
--divider-dark-color: theme("colors.gray.300");
--banner-info-color: theme("colors.stone.100");
--banner-warning-color: theme("colors.yellow.100");
--banner-error-color: theme("colors.red.100");
--tooltip-color: theme("colors.neutral.800");
--popover-color: theme("colors.white");
--method-get-color: theme("colors.green.500");
--method-post-color: theme("colors.amber.500");
--method-put-color: theme("colors.blue.500");
--method-patch-color: theme("colors.purple.500");
--method-delete-color: theme("colors.red.500");
--method-head-color: theme("colors.lime.500");
--method-options-color: theme("colors.pink.500");
--method-default-color: theme("colors.gray.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "textmate";
}
@mixin dark-theme {
--primary-color: #181818;
--primary-light-color: #1c1c1e;
--primary-dark-color: theme("colors.neutral.800");
--primary-contrast-color: theme("colors.neutral.900");
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.zinc.50");
--divider-color: #1f1f1f;
--divider-light-color: #1f1f1f;
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.800");
--banner-warning-color: theme("colors.yellow.800");
--banner-error-color: theme("colors.red.800");
--tooltip-color: theme("colors.neutral.100");
--popover-color: #1b1b1b;
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.neutral.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "merbivore_soft";
}
@mixin black-theme {
--primary-color: #0f0f0f;
--primary-light-color: theme("colors.neutral.900");
--primary-dark-color: #181818;
--primary-contrast-color: #0f0f0f;
--secondary-color: theme("colors.neutral.400");
--secondary-light-color: theme("colors.neutral.500");
--secondary-dark-color: theme("colors.neutral.50");
--divider-color: theme("colors.neutral.900");
--divider-light-color: theme("colors.neutral.900");
--divider-dark-color: theme("colors.zinc.800");
--banner-info-color: theme("colors.stone.900");
--banner-warning-color: theme("colors.yellow.900");
--banner-error-color: theme("colors.red.900");
--tooltip-color: theme("colors.neutral.100");
--popover-color: theme("colors.stone.950");
--method-get-color: theme("colors.emerald.500");
--method-post-color: theme("colors.yellow.500");
--method-put-color: theme("colors.sky.500");
--method-patch-color: theme("colors.violet.500");
--method-delete-color: theme("colors.rose.500");
--method-head-color: theme("colors.teal.500");
--method-options-color: theme("colors.indigo.500");
--method-default-color: theme("colors.zinc.500");
--status-info-color: theme("colors.blue.500");
--status-success-color: theme("colors.green.500");
--status-redirect-color: theme("colors.amber.500");
--status-critical-error-color: theme("colors.red.500");
--status-server-error-color: theme("colors.rose.500");
--status-missing-data-color: theme("colors.slate.500");
--editor-theme: "twilight";
}

View File

@@ -0,0 +1,41 @@
@mixin light-editor-theme {
--editor-type-color: theme("colors.violet.600");
--editor-name-color: theme("colors.red.600");
--editor-operator-color: theme("colors.indigo.600");
--editor-invalid-color: theme("colors.red.600");
--editor-separator-color: theme("colors.gray.600");
--editor-meta-color: theme("colors.gray.600");
--editor-variable-color: theme("colors.emerald.600");
--editor-link-color: theme("colors.cyan.600");
--editor-process-color: theme("colors.blue.600");
--editor-constant-color: theme("colors.fuchsia.600");
--editor-keyword-color: theme("colors.pink.600");
}
@mixin dark-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.blue.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.fuchsia.400");
--editor-constant-color: theme("colors.violet.400");
--editor-keyword-color: theme("colors.pink.400");
}
@mixin black-editor-theme {
--editor-type-color: theme("colors.violet.400");
--editor-name-color: theme("colors.fuchsia.400");
--editor-operator-color: theme("colors.indigo.400");
--editor-invalid-color: theme("colors.red.400");
--editor-separator-color: theme("colors.gray.400");
--editor-meta-color: theme("colors.gray.400");
--editor-variable-color: theme("colors.emerald.400");
--editor-link-color: theme("colors.cyan.400");
--editor-process-color: theme("colors.violet.400");
--editor-constant-color: theme("colors.blue.400");
--editor-keyword-color: theme("colors.pink.400");
}

View 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;
}

View File

@@ -1,5 +1,6 @@
{
"action": {
"add": "Add",
"autoscroll": "Autoscroll",
"cancel": "Cancel",
"choose_file": "Choose a file",
@@ -10,6 +11,7 @@
"connect": "Connect",
"connecting": "Connecting",
"copy": "Copy",
"create": "Create",
"delete": "Delete",
"disconnect": "Disconnect",
"dismiss": "Dismiss",
@@ -39,6 +41,7 @@
"scroll_to_top": "Scroll to top",
"search": "Search",
"send": "Send",
"share": "Share",
"start": "Start",
"starting": "Starting",
"stop": "Stop",
@@ -54,10 +57,30 @@
"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_interface_type": "Copy interface type",
"copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
@@ -73,6 +96,7 @@
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options",
"proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload",
@@ -119,7 +143,21 @@
"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",
@@ -153,6 +191,7 @@
"remove_folder": "Are you sure you want to permanently delete this folder?",
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
@@ -194,7 +233,8 @@
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"schema": "Connect to a GraphQL endpoint to view schema",
"shortcodes": "Shortcodes are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"shared_requests": "Shared requests are empty",
"subscription": "Subscriptions are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
@@ -234,9 +274,13 @@
"variable": "Variable",
"variable_list": "Variable List"
},
"graphql_collections": {
"title": "GraphQL Collections"
},
"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:",
@@ -257,6 +301,7 @@
"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",
@@ -267,7 +312,8 @@
"create_secret_gist": "Create secret Gist",
"gist_created": "Gist created",
"require_github": "Login with GitHub to create secret gist",
"title": "Export"
"title": "Export",
"failed": "Something went wrong while exporting"
},
"filter": {
"all": "All",
@@ -304,8 +350,8 @@
"authorization": "The authorization header will be automatically generated when you send the request.",
"generate_documentation_first": "Generate documentation first",
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
"offline_short": "You seem to be offline.",
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
"offline_short": "You're using Hoppscotch offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
@@ -334,6 +380,7 @@
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
"from_postman_description": "Import from Postman collection",
"from_file": "Import from File",
"from_url": "Import from URL",
"gist_url": "Enter Gist URL",
"import_from_url_invalid_fetch": "Couldn't get data from the url",
@@ -341,7 +388,14 @@
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
"import_from_url_success": "Collections Imported",
"json_description": "Import collections from a Hoppscotch Collections JSON file",
"title": "Import"
"title": "Import",
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment JSON file",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
},
"inspections": {
"description": "Inspect possible errors",
@@ -378,7 +432,9 @@
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request",
"share_request": "Share Request",
"import_export": "Import / Export"
},
"mqtt": {
@@ -454,14 +510,14 @@
"structured": "Structured",
"text": "Text"
},
"copy_link": "Copy link",
"different_collection": "Cannot reorder requests from different collections",
"duplicated": "Request duplicated",
"duration": "Duration",
"enter_curl": "Enter cURL command",
"generate_code": "Generate code",
"generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization",
"go_to_authorization_tab": "Go to Authorization tab",
"go_to_body_tab": "Go to Body tab",
"header_list": "Header List",
"invalid_name": "Please provide a name for the request",
"method": "Method",
@@ -486,6 +542,7 @@
"saved": "Request saved",
"share": "Share",
"share_description": "Share Hoppscotch with your friends",
"share_request": "Share Request",
"stop": "Stop",
"title": "Request",
"type": "Request type",
@@ -563,16 +620,34 @@
"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": {
"actions": "Actions",
"created_on": "Created on",
"deleted": "Shortcode deleted",
"method": "Method",
"not_found": "Shortcode not found",
"short_code": "Short code",
"url": "URL"
"shared_requests": {
"button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"customize": "Customize",
"creating_widget": "Creating widget",
"copy_html": "Copy HTML",
"copy_link": "Copy Link",
"copy_markdown": "Copy Markdown",
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab",
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
},
"shortcut": {
"general": {
@@ -602,7 +677,6 @@
"title": "Others"
},
"request": {
"copy_request_link": "Copy Request Link",
"delete_method": "Select DELETE method",
"get_method": "Select GET method",
"head_method": "Select HEAD method",
@@ -618,6 +692,7 @@
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"show_code": "Generate code snippet",
"share_request": "Share Request",
"title": "Request"
},
"response": {
@@ -742,6 +817,7 @@
"connection_failed": "Connection failed",
"connection_lost": "Connection lost",
"copied_to_clipboard": "Copied to clipboard",
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
"deleted": "Deleted",
"deprecated": "DEPRECATED",
"disabled": "Disabled",
@@ -764,7 +840,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",
"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}",
@@ -800,6 +876,7 @@
"queries": "Queries",
"query": "Query",
"schema": "Schema",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Tests",

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.8.2",
"version": "2023.8.4-1",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -17,51 +17,46 @@
"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.11.0",
"@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.2",
"@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",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.22.0",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@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",
"@urql/core": "^4.1.1",
"@lezer/highlight": "1.2.0",
"@unhead/vue": "^1.8.8",
"@urql/core": "^4.2.0",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.2",
"@urql/exchange-graphcache": "^6.3.3",
"@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.3.0",
"@vueuse/head": "^1.3.1",
"acorn-walk": "^8.2.0",
"axios": "^1.4.0",
"@vueuse/core": "^10.6.1",
"acorn-walk": "^8.3.0",
"axios": "^1.6.2",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "workspace:^",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
"fuse.js": "^6.6.2",
"globalthis": "^1.0.3",
"graphql": "^16.8.0",
"graphql-language-service-interface": "^2.9.1",
"graphql": "^16.8.1",
"graphql-language-service-interface": "^2.10.2",
"graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0",
@@ -69,15 +64,18 @@
"js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21",
"lossless-json": "^2.0.11",
"minisearch": "^6.1.0",
"lossless-json": "^3.0.2",
"minisearch": "^6.3.0",
"nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0",
"path": "^0.12.7",
"postman-collection": "^4.2.0",
"postman-collection": "^4.3.0",
"process": "^0.11.10",
"qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"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",
@@ -88,19 +86,21 @@
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.1",
"url": "^0.11.3",
"util": "^0.12.5",
"uuid": "^9.0.0",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-pdf-embed": "^1.1.6",
"vue-router": "^4.2.4",
"verzod": "^0.2.0",
"uuid": "^9.0.1",
"vue": "^3.3.8",
"vue-i18n": "^9.7.1",
"vue-pdf-embed": "^1.2.1",
"vue-router": "^4.2.5",
"vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1"
"xml-formatter": "^3.6.0",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
@@ -110,53 +110,58 @@
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
"@graphql-codegen/urql-introspection": "^2.2.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
"@graphql-codegen/urql-introspection": "^3.0.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.119",
"@iconify-json/lucide": "^1.1.141",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.3.3",
"@types/har-format": "^1.2.12",
"@types/js-yaml": "^4.0.5",
"@types/lodash-es": "^4.17.8",
"@types/lossless-json": "^1.0.1",
"@types/nprogress": "^0.2.0",
"@types/paho-mqtt": "^1.0.7",
"@types/postman-collection": "^3.5.7",
"@types/splitpanes": "^2.2.1",
"@types/uuid": "^9.0.2",
"@types/yargs-parser": "^21.0.0",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"@vitejs/plugin-vue": "^4.3.1",
"@vue/compiler-sfc": "^3.3.4",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/runtime-core": "^3.3.4",
"@rushstack/eslint-patch": "^1.6.0",
"@types/har-format": "^1.2.15",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/lossless-json": "^1.0.4",
"@types/nprogress": "^0.2.3",
"@types/paho-mqtt": "^1.0.10",
"@types/postman-collection": "^3.5.10",
"@types/splitpanes": "^2.2.6",
"@types/uuid": "^9.0.7",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/compiler-sfc": "^3.3.8",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/runtime-core": "^3.3.8",
"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",
"eslint": "^8.54.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.18.1",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"rollup-plugin-polyfill-node": "^0.12.0",
"sass": "^1.66.0",
"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",
"postcss": "^8.4.23",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"rollup-plugin-polyfill-node": "^0.13.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.2",
"typescript": "^5.3.2",
"unplugin-fonts": "^1.1.1",
"unplugin-icons": "^0.17.4",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-fonts": "^0.7.0",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.38",
"vite-plugin-inspect": "^0.7.42",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.16.4",
"vite-plugin-pwa": "^0.17.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vite-plugin-windicss": "^1.9.1",
"vitest": "^0.34.2",
"vue-tsc": "^1.8.8",
"windicss": "^3.5.6"
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
}
}
}

View 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

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 926 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 871 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 510 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -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

View File

@@ -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>

View File

@@ -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,9 @@ 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']
Embeds: typeof import('./components/embeds/index.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']
@@ -105,6 +109,7 @@ declare module 'vue' {
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
@@ -140,6 +145,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']
@@ -157,6 +163,14 @@ declare module 'vue' {
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['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']
@@ -179,6 +193,16 @@ declare module 'vue' {
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
@@ -189,7 +213,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']
@@ -200,9 +223,11 @@ declare module 'vue' {
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
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']

View File

@@ -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>

View File

@@ -0,0 +1,70 @@
<template>
<div
:role="bannerRole"
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
:class="bannerColor"
>
<div class="flex items-center">
<component :is="bannerIcon" class="mr-2" />
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
{{ banner.text(t) }}
</span>
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
{{ banner.alternateText(t) }}
</span>
</div>
<icon-lucide-x
v-if="dismissible"
class="opacity-50 hover:cursor-pointer hover:opacity-100"
@click="emit('dismiss')"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { BannerContent, BannerType } from "~/services/banner.service"
import { useI18n } from "@composables/i18n"
import IconAlertCircle from "~icons/lucide/alert-circle"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
import IconInfo from "~icons/lucide/info"
const props = withDefaults(
defineProps<{
banner: BannerContent
dismissible?: boolean
}>(),
{
dismissible: false,
}
)
const t = useI18n()
const emit = defineEmits<{
(e: "dismiss"): void
}>()
const ariaRoles: Record<BannerType, string> = {
info: "status",
warning: "status",
error: "alert",
}
const bgColors: Record<BannerType, string> = {
info: "bg-bannerInfo",
warning: "bg-bannerWarning",
error: "bg-bannerError",
}
const icons = {
info: IconInfo,
warning: IconAlertCircle,
error: IconAlertTriangle,
}
const bannerColor = computed(() => bgColors[props.banner.type])
const bannerIcon = computed(() => icons[props.banner.type])
const bannerRole = computed(() => ariaRoles[props.banner.type])
</script>

View File

@@ -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">

View File

@@ -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">

View File

@@ -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")

View File

@@ -1,28 +1,32 @@
<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
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
:label="t('app.name')"
to="/"
/>
</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 self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
@click="invokeAction('modals.search.toggle')"
>
<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 +34,189 @@
<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-if="showInstallButton"
v-tippy="{ theme: 'tooltip' }"
:title="t('header.install_pwa')"
:icon="IconDownload"
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
@click="installPWA()"
/>
<HoppButtonPrimary
:label="t('header.login')"
@click="invokeAction('modals.login.toggle')"
<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 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">
<div
class="flex border divide-x rounded bg-green-500/15 divide-green-600/25 border-green-600/25 focus-within:bg-green-400/10 focus-within:border-green-800/50 focus-within:divide-green-800/50 hover:bg-green-400/10 hover:border-green-800/50 hover:divide-green-800/50"
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary
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()"
: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')"
/>
<HoppButtonSecondary
<HoppButtonPrimary
:label="t('header.login')"
class="h-8"
@click="invokeAction('modals.login.toggle')"
/>
</div>
<div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
selectedTeam.teamMembers.length > 1
"
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()"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-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">
<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="() => tippyActions.focus()"
:on-shown="() => accountActions.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
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'
"
/>
<HoppSmartSelectWrapper
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('workspace.change')"
:label="mdAndLarger ? workspaceName : ``"
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
/>
</HoppSmartSelectWrapper>
<template #content="{ hide }">
<div
ref="tippyActions"
ref="accountActions"
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()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
<WorkspaceSelector />
</div>
</template>
</tippy>
</span>
<span class="px-2">
<tippy
interactive
trigger="click"
theme="popover"
:on-shown="() => tippyActions.focus()"
>
<HoppSmartPicture
v-tippy="{
theme: 'tooltip',
}"
:name="currentUser.uid"
:title="
currentUser.displayName ||
currentUser.email ||
t('profile.default_hopp_displayname')
"
indicator
:indicator-styles="
network.isOnline ? 'bg-green-500' : 'bg-red-500'
"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
class="flex flex-col focus:outline-none"
tabindex="0"
@keyup.p="profile.$el.click()"
@keyup.s="settings.$el.click()"
@keyup.l="logout.$el.click()"
@keyup.escape="hide()"
>
<div class="flex flex-col px-2">
<span class="inline-flex truncate font-semibold">
{{
currentUser.displayName ||
t("profile.default_hopp_displayname")
}}
</span>
<span
class="inline-flex truncate text-secondaryLight text-tiny"
>
{{ currentUser.email }}
</span>
</div>
<hr />
<HoppSmartItem
ref="profile"
to="/profile"
:icon="IconUser"
:label="t('navigation.profile')"
:shortcut="['P']"
@click="hide()"
/>
<HoppSmartItem
ref="settings"
to="/settings"
:icon="IconSettings"
:label="t('navigation.settings')"
:shortcut="['S']"
@click="hide()"
/>
<FirebaseLogout
ref="logout"
:shortcut="['L']"
@confirm-logout="hide()"
/>
</div>
</template>
</tippy>
</span>
</div>
</div>
</div>
</header>
<AppAnnouncement v-if="!network.isOnline" />
<AppBanner
v-if="bannerContent"
:banner="bannerContent"
:dismissible="true"
@dismiss="dismissOfflineBanner"
/>
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
<TeamsInvite
v-if="workspace.type === 'team' && workspace.teamID"
@@ -231,7 +232,6 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams"
/>
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@@ -264,6 +264,11 @@ import IconUsers from "~icons/lucide/users"
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"
const t = useI18n()
const toast = useToast()
@@ -281,7 +286,32 @@ 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: "warning",
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
}
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
})
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
const currentUser = useReadonlyStream(
platform.auth.getProbableUserStream(),

View File

@@ -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
@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
},
{ severity: 0 }
)
} else {
return { severity: 0 }
}
return { severity: 0 }
})
const severityColor = (severity: number) => {

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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;

View File

@@ -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}`"

Some files were not shown because too many files have changed in this diff Show More