Compare commits

..

54 Commits

Author SHA1 Message Date
nivedin
00588bcc0a fix: interceptor error from extension issue 2023-11-14 17:17:07 +05:30
Akash K
e24d0ce605 fix: oauth 2.0 authentication type is breaking (#3531) 2023-11-14 00:12:04 +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
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
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
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
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
Gaurav K P
01df1663ad fix(common): handle false negatives in url validation (#3465) 2023-11-01 22:23:33 +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
Andrew Bastin
57cb59027b chore: bump codemirror dependencies 2023-10-19 13:37:07 +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
Mir Arif Hasan
f5db54484c HBE-266 Update NestJS packages (#3389)
* build: update npm nest packages

* build: removed depricated apollo-server-plugin package

* build: pnpm-lock file added

* build: swc integrated

* Revert "build: swc integrated"

This reverts commit 803a01f38f210dfbcd603665893d29af565c8908.

* feat: upgrade graphql* packages version

* feat: upgrade point release

* feat: update pnpm-lock file
2023-10-16 12:23:55 +05:30
Mir Arif Hasan
8deb6471b9 HBE-270 Test-Case timestamp issue fix in backend (#3415)
test: timestamp issue fix in user-history
2023-10-16 12:11:15 +05:30
Liyas Thomas
73b3ff8e41 feat: improve import-export UI (#3452)
* chore: uniform styles across components

* chore: removed absolute wrapper divs

* feat: add import button when graphql collections are empty

* chore: add icon for button

---------

Co-authored-by: nivedin <nivedinp@gmail.com>
2023-10-13 17:57:14 +05:30
James George
016a18d3b2 fix(common): use tab service within helpers (#3448) 2023-10-12 13:15:45 +05:30
Anwarul Islam
ba31cdabea feat: tab service added (#3367) 2023-10-11 18:21:07 +05:30
Nivedin
51510566bc refactor: add import buttons in empty state for collections & environments (#3438) 2023-10-11 11:08:51 +05:30
Anwarul Islam
cabee0ecc8 fix: memory leak issue on TeamInvite modal (#3440)
* fix: memory leak issue

* feat: added rerun ability

* chore: lint fix
2023-10-11 07:59:12 +05:30
Anwarul Islam
2c2b39a236 feat: no permission warning added for users except owner while deleting team (#3328)
* feat: no permission warning added
* chore: changed to function reference
2023-10-09 19:31:48 +05:30
Liyas Thomas
78450c9316 fix: tooltip position in editor instance (#3374) 2023-10-09 11:37:52 +05:30
Joel Jacob Stephen
b18fd90b64 fix: blank screen in admin dashboard on authentication problems (#3385)
* fix: dashboard logs out user when cookie expires or is unauthorized

* fix: handles the 401 error thrown when trying to refresh tokens

* chore: updated wrong logic when returning state in refresh token function

* feat: introduced auth exchange to urql client to check for errors on each backend call

* fix: prevent multiple window reloads

---------

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-10-09 10:08:35 +05:30
Andrew Bastin
0188a8d7db chore: bump version 2023-10-06 22:04:57 +05:30
Joel Jacob Stephen
6c63a8dc28 refactor: updated i18n implementation in the admin dashboard (#3395)
* feat: introduced new unplugin i18n and removed the old vite i18n package

* refactor: updated vite config to support the new plugin

* refactor: removed irrelevant logic from the i18n module
2023-10-06 17:36:19 +05:30
Rakibul Yeasin
17d6ae15a5 fix: Cannot set custom method #3406 (#3408)
* fix: #3406

* chore: remove console log

* fix: an unknown keyboard event issue

---------

Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
2023-10-06 11:57:26 +05:30
Andrew Bastin
40f72278a9 fix: team collection resetting on unmount within app lifecycle (#3396)
* fix: team collection resetting on unmount within app lifecycle

* chore: linting

* refactor: eliminate redundancy

* chore: update comment about the watcher purpose

---------

Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-10-06 11:34:44 +05:30
5idereal
f717704731 chore(i18n): update tw.json (#3409) 2023-10-06 11:27:24 +05:30
Joel Jacob Stephen
185c225297 feat: introduces ability to export single environment variables and allow CLI to accept the export format used by the app (#3380)
* feat: add ability to export a single environment

* refactor: export environment without id

* feat: introducing zod for checking json format for environment variables

* refactor: new zod specific type for HoppEnvPair

* feat: add ability to export single environment in team environment

* refactor: moved zod as a dependency to devDependency

* refactor: separated repeating logic to helper file

* refactor: removed unnecessary to string operation

* chore: rearranged smart item placement

* refactor: introduced error type when a bulk environment export is used in cli

* refactor: removed unnecssary type exports and updated logic and variable names across most files

* refactor: better logic for type shapes

* chore: bump hoppscotch-cli package version

---------

Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com>
2023-10-06 11:21:54 +05:30
James George
2694731c36 chore: remove stale type definitions (#3368) 2023-10-05 14:49:04 +05:30
James George
ae89af9978 feat: alert the user on empty collection/environment exports (#3416) 2023-10-05 14:38:38 +05:30
James George
87d617012f fix: environment variables usage in meta tags (#3418) 2023-10-05 13:51:42 +05:30
Liyas Thomas
2420b3fa42 chore: move deps in the root of monorepo into devDependencies (#3375)
chore: move deps in the root of monorepo into devDependencies
2023-09-28 22:25:22 +05:30
Anwarul Islam
175a991ec4 fix: gql teamID not being passed (#3392)
* chore: bump dependencies for path.charCodeAt issue
* fix: gql teamID is not passed issue
2023-09-28 22:04:02 +05:30
SamJakob
0301649aff chore: make devcontainer copy .env.example (#3318) 2023-09-28 21:58:17 +05:30
Joel Jacob Stephen
544b045300 fix: authorisation headers not being sent along with subscriptions when using graphql (#3354)
* fix: send auth headers to the payload

* refactor: alert user that headers are sent to connection_init

* refactor: send headers only when headers are populated

* chore: cleanup code
2023-09-28 21:57:07 +05:30
Andrew Bastin
65884293be chore: introduce docker buildx for multi-platform build 2023-09-18 21:16:23 +05:30
Andrew Bastin
3cb4861bac chore: pin netlify-cli version on ui deploy script 2023-09-18 20:51:42 +05:30
316 changed files with 19649 additions and 4943 deletions

View File

@@ -5,5 +5,5 @@
"features": { "features": {
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {} "ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
}, },
"postCreateCommand": "mv .env.example .env && pnpm i" "postCreateCommand": "cp .env.example .env && pnpm i"
} }

View File

@@ -18,6 +18,9 @@ jobs:
- name: Setup QEMU - name: Setup QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v3
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@v2 uses: docker/login-action@v2
with: with:

View File

@@ -36,7 +36,7 @@ jobs:
# Deploy the ui site with netlify-cli # Deploy the ui site with netlify-cli
- name: Deploy to Netlify (ui) - name: Deploy to Netlify (ui)
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod run: npx netlify-cli@15.11.0 deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
env: env:
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }} NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

View File

@@ -22,16 +22,14 @@
"workspaces": [ "workspaces": [
"./packages/*" "./packages/*"
], ],
"dependencies": {
"husky": "^7.0.4",
"lint-staged": "^12.3.8"
},
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^16.2.3", "@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1", "@commitlint/config-conventional": "^16.2.1",
"@types/node": "^17.0.24", "@types/node": "17.0.27",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"http-server": "^14.1.1" "http-server": "^14.1.1",
"husky": "^7.0.4",
"lint-staged": "12.4.0"
}, },
"pnpm": { "pnpm": {
"packageExtensions": { "packageExtensions": {

View File

@@ -17,16 +17,16 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"dependencies": { "dependencies": {
"@codemirror/language": "^6.9.0", "@codemirror/language": "6.9.0",
"@lezer/highlight": "^1.1.6", "@lezer/highlight": "1.1.4",
"@lezer/lr": "^1.3.10" "@lezer/lr": "^1.3.13"
}, },
"devDependencies": { "devDependencies": {
"@lezer/generator": "^1.5.0", "@lezer/generator": "^1.5.1",
"mocha": "^9.2.2", "mocha": "^9.2.2",
"rollup": "^2.70.2", "rollup": "^3.29.3",
"rollup-plugin-dts": "^4.2.1", "rollup-plugin-dts": "^6.0.2",
"rollup-plugin-ts": "^2.0.7", "rollup-plugin-ts": "^3.4.5",
"typescript": "^4.6.3" "typescript": "^5.2.2"
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "hoppscotch-backend", "name": "hoppscotch-backend",
"version": "2023.8.1", "version": "2023.8.3-1",
"description": "", "description": "",
"author": "", "author": "",
"private": true, "private": true,
@@ -24,18 +24,17 @@
"do-test": "pnpm run test" "do-test": "pnpm run test"
}, },
"dependencies": { "dependencies": {
"@nestjs-modules/mailer": "^1.8.1", "@apollo/server": "^4.9.4",
"@nestjs/apollo": "^10.1.6", "@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^9.2.1", "@nestjs/apollo": "^12.0.9",
"@nestjs/core": "^9.2.1", "@nestjs/common": "^10.2.6",
"@nestjs/graphql": "^10.1.6", "@nestjs/core": "^10.2.6",
"@nestjs/jwt": "^10.0.1", "@nestjs/graphql": "^12.0.9",
"@nestjs/passport": "^9.0.0", "@nestjs/jwt": "^10.1.1",
"@nestjs/platform-express": "^9.2.1", "@nestjs/passport": "^10.0.2",
"@nestjs/throttler": "^4.0.0", "@nestjs/platform-express": "^10.2.6",
"@nestjs/throttler": "^5.0.0",
"@prisma/client": "^4.16.2", "@prisma/client": "^4.16.2",
"apollo-server-express": "^3.11.1",
"apollo-server-plugin-base": "^3.7.1",
"argon2": "^0.30.3", "argon2": "^0.30.3",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"cookie": "^0.5.0", "cookie": "^0.5.0",
@@ -43,9 +42,9 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-session": "^1.17.3", "express-session": "^1.17.3",
"fp-ts": "^2.13.1", "fp-ts": "^2.13.1",
"graphql": "^15.5.0", "graphql": "^16.8.1",
"graphql-query-complexity": "^0.12.0", "graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.5.0", "graphql-redis-subscriptions": "^2.6.0",
"graphql-subscriptions": "^2.0.0", "graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"io-ts": "^2.2.16", "io-ts": "^2.2.16",
@@ -63,9 +62,9 @@
"rxjs": "^7.6.0" "rxjs": "^7.6.0"
}, },
"devDependencies": { "devDependencies": {
"@nestjs/cli": "^9.1.5", "@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^9.0.3", "@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^9.2.1", "@nestjs/testing": "^10.2.6",
"@relmify/jest-fp-ts": "^2.0.2", "@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0", "@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",

View File

@@ -74,7 +74,7 @@ export class AdminService {
try { try {
await this.mailerService.sendUserInvitationEmail(inviteeEmail, { await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
template: 'code-your-own', template: 'user-invitation',
variables: { variables: {
inviteeEmail: inviteeEmail, inviteeEmail: inviteeEmail,
magicLink: `${process.env.VITE_BASE_URL}`, magicLink: `${process.env.VITE_BASE_URL}`,

View File

@@ -27,12 +27,7 @@ import { AppController } from './app.controller';
buildSchemaOptions: { buildSchemaOptions: {
numberScalarMode: 'integer', numberScalarMode: 'integer',
}, },
cors: {
origin: process.env.WHITELISTED_ORIGINS.split(','),
credentials: true,
},
playground: process.env.PRODUCTION !== 'true', playground: process.env.PRODUCTION !== 'true',
debug: process.env.PRODUCTION !== 'true',
autoSchemaFile: true, autoSchemaFile: true,
installSubscriptionHandlers: true, installSubscriptionHandlers: true,
subscriptions: { subscriptions: {
@@ -62,10 +57,12 @@ import { AppController } from './app.controller';
}), }),
driver: ApolloDriver, driver: ApolloDriver,
}), }),
ThrottlerModule.forRoot({ ThrottlerModule.forRoot([
{
ttl: +process.env.RATE_LIMIT_TTL, ttl: +process.env.RATE_LIMIT_TTL,
limit: +process.env.RATE_LIMIT_MAX, limit: +process.env.RATE_LIMIT_MAX,
}), },
]),
UserModule, UserModule,
AuthModule, AuthModule,
AdminModule, AdminModule,

View File

@@ -229,7 +229,7 @@ export class AuthService {
} }
await this.mailerService.sendEmail(email, { await this.mailerService.sendEmail(email, {
template: 'code-your-own', template: 'user-invitation',
variables: { variables: {
inviteeEmail: email, inviteeEmail: email,
magicLink: `${url}/enter?token=${generatedTokens.token}`, magicLink: `${url}/enter?token=${generatedTokens.token}`,

View File

@@ -93,9 +93,7 @@ export async function emitGQLSchemaFile() {
numberScalarMode: 'integer', numberScalarMode: 'integer',
}); });
const schemaString = printSchema(schema, { const schemaString = printSchema(schema);
commentDescriptions: true,
});
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`); logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);

View File

@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard { export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected getTracker(req: Record<string, any>): string { protected async getTracker(req: Record<string, any>): Promise<string> {
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
// learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#directives
} }
} }

View File

@@ -8,7 +8,7 @@ export type MailDescription = {
}; };
export type UserMagicLinkMailDescription = { export type UserMagicLinkMailDescription = {
template: 'code-your-own'; template: 'user-invitation';
variables: { variables: {
inviteeEmail: string; inviteeEmail: string;
magicLink: string; magicLink: string;
@@ -16,7 +16,7 @@ export type UserMagicLinkMailDescription = {
}; };
export type AdminUserInvitationMailDescription = { export type AdminUserInvitationMailDescription = {
template: 'code-your-own'; template: 'user-invitation';
variables: { variables: {
inviteeEmail: string; inviteeEmail: string;
magicLink: string; magicLink: string;

View File

@@ -27,7 +27,7 @@ export class MailerService {
case 'team-invitation': case 'team-invitation':
return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`; return `${mailDesc.variables.invitee} invited you to join ${mailDesc.variables.invite_team_name} in Hoppscotch`;
case 'code-your-own': case 'user-invitation':
return 'Sign in to Hoppscotch'; return 'Sign in to Hoppscotch';
} }
} }

View File

@@ -1,8 +1,9 @@
import { GraphQLSchemaHost } from '@nestjs/graphql'; import { GraphQLSchemaHost } from '@nestjs/graphql';
import { import {
ApolloServerPlugin, ApolloServerPlugin,
BaseContext,
GraphQLRequestListener, GraphQLRequestListener,
} from 'apollo-server-plugin-base'; } from '@apollo/server';
import { Plugin } from '@nestjs/apollo'; import { Plugin } from '@nestjs/apollo';
import { GraphQLError } from 'graphql'; import { GraphQLError } from 'graphql';
import { import {
@@ -17,7 +18,7 @@ const COMPLEXITY_LIMIT = 50;
export class GQLComplexityPlugin implements ApolloServerPlugin { export class GQLComplexityPlugin implements ApolloServerPlugin {
constructor(private gqlSchemaHost: GraphQLSchemaHost) {} constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
async requestDidStart(): Promise<GraphQLRequestListener> { async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
const { schema } = this.gqlSchemaHost; const { schema } = this.gqlSchemaHost;
return { return {

View File

@@ -1,5 +1,5 @@
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client'; import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
import { mock, mockDeep, mockReset } from 'jest-mock-extended'; import { mockDeep, mockReset } from 'jest-mock-extended';
import { import {
TEAM_COLL_DEST_SAME, TEAM_COLL_DEST_SAME,
TEAM_COLL_INVALID_JSON, TEAM_COLL_INVALID_JSON,
@@ -17,9 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
import { PubSubService } from 'src/pubsub/pubsub.service'; import { PubSubService } from 'src/pubsub/pubsub.service';
import { AuthUser } from 'src/types/AuthUser'; import { AuthUser } from 'src/types/AuthUser';
import { TeamCollectionService } from './team-collection.service'; import { TeamCollectionService } from './team-collection.service';
import { TeamCollection } from './team-collection.model';
import { TeamCollectionModule } from './team-collection.module';
import * as E from 'fp-ts/Either';
const mockPrisma = mockDeep<PrismaService>(); const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>(); const mockPubSub = mockDeep<PubSubService>();

View File

@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
describe('createDuplicateEnvironment', () => { describe('createDuplicateEnvironment', () => {
test('should successfully duplicate an existing team environment', async () => { test('should successfully duplicate an existing team environment', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
teamEnvironment, teamEnvironment,
); );
@@ -322,7 +322,9 @@ describe('TeamEnvironmentsService', () => {
}); });
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => { test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError'); mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValue(
'NotFoundError',
);
const result = await teamEnvironmentsService.createDuplicateEnvironment( const result = await teamEnvironmentsService.createDuplicateEnvironment(
teamEnvironment.id, teamEnvironment.id,
@@ -332,7 +334,7 @@ describe('TeamEnvironmentsService', () => {
}); });
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => { test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce( mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
teamEnvironment, teamEnvironment,
); );

View File

@@ -183,11 +183,10 @@ export class TeamEnvironmentsService {
*/ */
async createDuplicateEnvironment(id: string) { async createDuplicateEnvironment(id: string) {
try { try {
const environment = await this.prisma.teamEnvironment.findFirst({ const environment = await this.prisma.teamEnvironment.findFirstOrThrow({
where: { where: {
id: id, id: id,
}, },
rejectOnNotFound: true,
}); });
const result = await this.prisma.teamEnvironment.create({ const result = await this.prisma.teamEnvironment.create({

View File

@@ -142,13 +142,15 @@ describe('UserHistoryService', () => {
}); });
describe('createUserHistory', () => { describe('createUserHistory', () => {
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => { test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}); });
@@ -158,7 +160,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}; };
@@ -172,13 +174,15 @@ describe('UserHistoryService', () => {
).toEqualRight(userHistory); ).toEqualRight(userHistory);
}); });
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => { test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}); });
@@ -188,7 +192,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}; };
@@ -212,13 +216,15 @@ describe('UserHistoryService', () => {
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE); ).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
}); });
test('Should create a GQL request to users history and publish a created subscription', async () => { test('Should create a GQL request to users history and publish a created subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}); });
@@ -228,7 +234,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.GQL, reqType: ReqType.GQL,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}; };
@@ -245,13 +251,15 @@ describe('UserHistoryService', () => {
); );
}); });
test('Should create a REST request to users history and publish a created subscription', async () => { test('Should create a REST request to users history and publish a created subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.create.mockResolvedValueOnce({ mockPrisma.userHistory.create.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}); });
@@ -261,7 +269,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}; };
@@ -323,13 +331,15 @@ describe('UserHistoryService', () => {
).toEqualLeft(USER_HISTORY_NOT_FOUND); ).toEqualLeft(USER_HISTORY_NOT_FOUND);
}); });
test('Should star/unstar a request in the history and publish a updated subscription', async () => { test('Should star/unstar a request in the history and publish a updated subscription', async () => {
const executedOn = new Date();
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({ mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
userUid: 'abc', userUid: 'abc',
id: '1', id: '1',
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: false, isStarred: false,
}); });
@@ -339,7 +349,7 @@ describe('UserHistoryService', () => {
request: [{}], request: [{}],
responseMetadata: [{}], responseMetadata: [{}],
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: true, isStarred: true,
}); });
@@ -349,7 +359,7 @@ describe('UserHistoryService', () => {
request: JSON.stringify([{}]), request: JSON.stringify([{}]),
responseMetadata: JSON.stringify([{}]), responseMetadata: JSON.stringify([{}]),
reqType: ReqType.REST, reqType: ReqType.REST,
executedOn: new Date(), executedOn,
isStarred: true, isStarred: true,
}; };

View File

@@ -1,6 +1,6 @@
{ {
"name": "@hoppscotch/cli", "name": "@hoppscotch/cli",
"version": "0.3.2", "version": "0.3.3",
"description": "A CLI to run Hoppscotch test scripts in CI environments.", "description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io", "homepage": "https://hoppscotch.io",
"main": "dist/index.js", "main": "dist/index.js",
@@ -40,9 +40,6 @@
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.0.2", "@relmify/jest-fp-ts": "^2.0.2",
"@swc/core": "^1.2.181", "@swc/core": "^1.2.181",
"@types/axios": "^0.14.0",
"@types/chalk": "^2.2.0",
"@types/commander": "^2.12.2",
"@types/jest": "^27.4.1", "@types/jest": "^27.4.1",
"@types/lodash": "^4.14.181", "@types/lodash": "^4.14.181",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
@@ -58,6 +55,7 @@
"qs": "^6.10.3", "qs": "^6.10.3",
"ts-jest": "^27.1.4", "ts-jest": "^27.1.4",
"tsup": "^5.12.7", "tsup": "^5.12.7",
"typescript": "^4.6.4" "typescript": "^4.6.4",
"zod": "^3.22.2"
} }
} }

View File

@@ -48,6 +48,11 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
ERROR_MSG = `Unavailable command: ${error.command}`; ERROR_MSG = `Unavailable command: ${error.command}`;
break; break;
case "MALFORMED_ENV_FILE": case "MALFORMED_ENV_FILE":
ERROR_MSG = `The environment file is not of the correct format.`;
break;
case "BULK_ENV_FILE":
ERROR_MSG = `CLI doesn't support bulk environments export.`;
break;
case "MALFORMED_COLLECTION": case "MALFORMED_COLLECTION":
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`; ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
break; break;

View File

@@ -1,27 +1,45 @@
import { error } from "../../types/errors"; import { error } from "../../types/errors";
import { HoppEnvs, HoppEnvPair } from "../../types/request"; import {
HoppEnvs,
HoppEnvPair,
HoppEnvKeyPairObject,
HoppEnvExportObject,
HoppBulkEnvExportObject,
} from "../../types/request";
import { readJsonFile } from "../../utils/mutators"; import { readJsonFile } from "../../utils/mutators";
/** /**
* Parses env json file for given path and validates the parsed env json object. * Parses env json file for given path and validates the parsed env json object.
* @param path Path of env.json file to be parsed. * @param path Path of env.json file to be parsed.
* @returns For successful parsing we get HoppEnvs object. * @returns For successful parsing we get HoppEnvs object.
*/ */
export async function parseEnvsData(path: string) { export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path) const contents = await readJsonFile(path);
const envPairs: Array<HoppEnvPair> = [];
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
const HoppBulkEnvExportObjectResult =
HoppBulkEnvExportObject.safeParse(contents);
if(!(contents && typeof contents === "object" && !Array.isArray(contents))) { // CLI doesnt support bulk environments export.
throw error({ code: "MALFORMED_ENV_FILE", path, data: null }) // Hence we check for this case and throw an error if it matches the format.
if (HoppBulkEnvExportObjectResult.success) {
throw error({ code: "BULK_ENV_FILE", path, data: error });
} }
const envPairs: Array<HoppEnvPair> = [] // Checks if the environment file is of the correct format.
// If it doesnt match either of them, we throw an error.
for( const [key,value] of Object.entries(contents)) { if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
if(typeof value !== "string") { throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
throw error({ code: "MALFORMED_ENV_FILE", path, data: {value: value} })
} }
envPairs.push({key, value}) if (HoppEnvKeyPairResult.success) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value });
} }
return <HoppEnvs>{ global: [], selected: envPairs } } else if (HoppEnvExportObjectResult.success) {
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
envPairs.push({ key, value });
}
return <HoppEnvs>{ global: [], selected: envPairs };
} }

View File

@@ -24,6 +24,7 @@ type HoppErrors = {
REQUEST_ERROR: HoppErrorData; REQUEST_ERROR: HoppErrorData;
INVALID_ARGUMENT: HoppErrorData; INVALID_ARGUMENT: HoppErrorData;
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData; MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
INVALID_FILE_TYPE: HoppErrorData; INVALID_FILE_TYPE: HoppErrorData;
}; };

View File

@@ -1,6 +1,7 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"; import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { TestReport } from "../interfaces/response"; import { TestReport } from "../interfaces/response";
import { HoppCLIError } from "./errors"; import { HoppCLIError } from "./errors";
import { z } from "zod";
export type FormDataEntry = { export type FormDataEntry = {
key: string; key: string;
@@ -9,6 +10,22 @@ export type FormDataEntry = {
export type HoppEnvPair = { key: string; value: string }; export type HoppEnvPair = { key: string; value: string };
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
// Shape of the single environment export object that is exported from the app.
export const HoppEnvExportObject = z.object({
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
),
});
// Shape of the bulk environment export object that is exported from the app.
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
export type HoppEnvs = { export type HoppEnvs = {
global: HoppEnvPair[]; global: HoppEnvPair[];
selected: HoppEnvPair[]; selected: HoppEnvPair[];

View File

@@ -137,10 +137,10 @@ a {
.cm-tooltip { .cm-tooltip {
.tippy-box { .tippy-box {
@apply shadow-none; @apply shadow-none #{!important};
@apply fixed; @apply fixed;
@apply inline-flex; @apply inline-flex;
@apply -mt-8; @apply -mt-7.5;
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"action": { "action": {
"add": "Add",
"autoscroll": "Autoscroll", "autoscroll": "Autoscroll",
"cancel": "Cancel", "cancel": "Cancel",
"choose_file": "Choose a file", "choose_file": "Choose a file",
@@ -54,9 +55,28 @@
"new": "Add new", "new": "Add new",
"star": "Add star" "star": "Add star"
}, },
"cookies": {
"modal": {
"new_domain_name": "New domain name",
"set": "Set a cookie",
"cookie_string": "Cookie string",
"enter_cookie_string": "Enter cookie string",
"cookie_name": "Name",
"cookie_value": "Value",
"cookie_path": "Path",
"cookie_expires": "Expires",
"managed_tab": "Managed",
"raw_tab": "Raw",
"interceptor_no_support": "Your currently selected interceptor does not support cookies. Select a different Interceptor and try again.",
"empty_domains": "Domain list is empty",
"empty_domain": "Domain is empty",
"no_cookies_in_domain": "No cookies set for this domain"
}
},
"app": { "app": {
"chat_with_us": "Chat with us", "chat_with_us": "Chat with us",
"contact_us": "Contact us", "contact_us": "Contact us",
"cookies": "Cookies",
"copy": "Copy", "copy": "Copy",
"copy_user_id": "Copy User Auth Token", "copy_user_id": "Copy User Auth Token",
"developer_option": "Developer options", "developer_option": "Developer options",
@@ -112,6 +132,7 @@
}, },
"authorization": { "authorization": {
"generate_token": "Generate Token", "generate_token": "Generate Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL", "include_in_url": "Include in URL",
"learn": "Learn how", "learn": "Learn how",
"pass_key_by": "Pass by", "pass_key_by": "Pass by",
@@ -124,6 +145,7 @@
"created": "Collection created", "created": "Collection created",
"different_parent": "Cannot reorder collection with different parent", "different_parent": "Cannot reorder collection with different parent",
"edit": "Edit Collection", "edit": "Edit Collection",
"import_or_create": "Import or create a collection",
"invalid_name": "Please provide a name for the collection", "invalid_name": "Please provide a name for the collection",
"invalid_root_move": "Collection already in the root", "invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully", "moved": "Moved Successfully",
@@ -209,6 +231,7 @@
"empty_variables": "No variables", "empty_variables": "No variables",
"global": "Global", "global": "Global",
"global_variables": "Global variables", "global_variables": "Global variables",
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment", "invalid_name": "Please provide a name for the environment",
"list": "Environment variables", "list": "Environment variables",
"my_environments": "My Environments", "my_environments": "My Environments",
@@ -234,6 +257,7 @@
"error": { "error": {
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.", "browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
"check_console_details": "Check console log for details.", "check_console_details": "Check console log for details.",
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly", "curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone", "danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:", "delete_account": "Your account is currently an owner in these teams:",
@@ -249,9 +273,12 @@
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again", "json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
"network_error": "There seems to be a network error. Please try again.", "network_error": "There seems to be a network error. Please try again.",
"network_fail": "Could not send request", "network_fail": "Could not send request",
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
"no_duration": "No duration", "no_duration": "No duration",
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
"no_results_found": "No matches found", "no_results_found": "No matches found",
"page_not_found": "This page could not be found", "page_not_found": "This page could not be found",
"please_install_extension": "Please install the extension and add origin to the extension.",
"proxy_error": "Proxy error", "proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script", "script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong", "something_went_wrong": "Something went wrong",
@@ -456,6 +483,7 @@
"enter_curl": "Enter cURL command", "enter_curl": "Enter cURL command",
"generate_code": "Generate code", "generate_code": "Generate code",
"generated_code": "Generated code", "generated_code": "Generated code",
"go_to_authorization_tab": "Go to Authorization",
"header_list": "Header List", "header_list": "Header List",
"invalid_name": "Please provide a name for the request", "invalid_name": "Please provide a name for the request",
"method": "Method", "method": "Method",
@@ -743,9 +771,11 @@
"disconnected_from": "Disconnected from {name}", "disconnected_from": "Disconnected from {name}",
"docs_generated": "Documentation generated", "docs_generated": "Documentation generated",
"download_started": "Download started", "download_started": "Download started",
"download_failed": "Download failed",
"enabled": "Enabled", "enabled": "Enabled",
"file_imported": "File imported", "file_imported": "File imported",
"finished_in": "Finished in {duration} ms", "finished_in": "Finished in {duration} ms",
"hide": "Hide",
"history_deleted": "History deleted", "history_deleted": "History deleted",
"linewrap": "Wrap lines", "linewrap": "Wrap lines",
"loading": "Loading...", "loading": "Loading...",
@@ -756,6 +786,7 @@
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}", "published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
"published_message": "Published message: {message} to topic: {topic}", "published_message": "Published message: {message} to topic: {topic}",
"reconnection_error": "Failed to reconnect", "reconnection_error": "Failed to reconnect",
"show": "Show",
"subscribed_failed": "Failed to subscribe to topic: {topic}", "subscribed_failed": "Failed to subscribe to topic: {topic}",
"subscribed_success": "Successfully subscribed to topic: {topic}", "subscribed_success": "Successfully subscribed to topic: {topic}",
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}", "unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
@@ -837,7 +868,7 @@
"new": "New Team", "new": "New Team",
"new_created": "New team created", "new_created": "New team created",
"new_name": "My New Team", "new_name": "My New Team",
"no_access": "You do not have edit access to these collections", "no_access": "You do not have edit access to this team",
"no_invite_found": "Invitation not found. Contact your team owner.", "no_invite_found": "Invitation not found. Contact your team owner.",
"no_request_found": "Request not found.", "no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.", "not_found": "Team not found. Contact your team owner.",

View File

@@ -5,7 +5,7 @@
"choose_file": "選擇一個檔案", "choose_file": "選擇一個檔案",
"clear": "清除", "clear": "清除",
"clear_all": "全部清除", "clear_all": "全部清除",
"clear_history": "Clear all History", "clear_history": "清除所有歷史記錄",
"close": "關閉", "close": "關閉",
"connect": "連線", "connect": "連線",
"connecting": "正在連接", "connecting": "正在連接",
@@ -79,8 +79,8 @@
"search": "搜尋", "search": "搜尋",
"share": "分享", "share": "分享",
"shortcuts": "快捷方式", "shortcuts": "快捷方式",
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.", "social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。",
"social_links": "Social links", "social_links": "社群連結",
"spotlight": "聚光燈", "spotlight": "聚光燈",
"status": "狀態", "status": "狀態",
"status_description": "檢查網站狀態", "status_description": "檢查網站狀態",
@@ -135,15 +135,15 @@
"renamed": "集合已重新命名", "renamed": "集合已重新命名",
"request_in_use": "請求正在使用中", "request_in_use": "請求正在使用中",
"save_as": "另存為", "save_as": "另存為",
"save_to_collection": "Save to Collection", "save_to_collection": "儲存到集合",
"select": "選擇一個集合", "select": "選擇一個集合",
"select_location": "選擇位置", "select_location": "選擇位置",
"select_team": "選擇一個團隊", "select_team": "選擇一個團隊",
"team_collections": "團隊集合" "team_collections": "團隊集合"
}, },
"confirm": { "confirm": {
"close_unsaved_tab": "Are you sure you want to close this tab?", "close_unsaved_tab": "您確定要關閉此分頁嗎?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.", "close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。",
"exit_team": "您確定要離開此團隊嗎?", "exit_team": "您確定要離開此團隊嗎?",
"logout": "您確定要登出嗎?", "logout": "您確定要登出嗎?",
"remove_collection": "您確定要永久刪除該集合嗎?", "remove_collection": "您確定要永久刪除該集合嗎?",
@@ -158,9 +158,9 @@
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。" "sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
}, },
"context_menu": { "context_menu": {
"add_parameters": "Add to parameters", "add_parameters": "新增至參數",
"open_request_in_new_tab": "Open request in new tab", "open_request_in_new_tab": "在新分頁開啟請求",
"set_environment_variable": "Set as variable" "set_environment_variable": "設為變數"
}, },
"count": { "count": {
"header": "請求標頭 {count}", "header": "請求標頭 {count}",
@@ -204,31 +204,31 @@
"create_new": "建立新環境", "create_new": "建立新環境",
"created": "已建立環境", "created": "已建立環境",
"deleted": "刪除環境", "deleted": "刪除環境",
"duplicated": "Environment duplicated", "duplicated": "已複製環境",
"edit": "編輯環境", "edit": "編輯環境",
"empty_variables": "No variables", "empty_variables": "無變數",
"global": "Global", "global": "全域",
"global_variables": "Global variables", "global_variables": "全域變數",
"invalid_name": "請提供有效的環境名稱", "invalid_name": "請提供有效的環境名稱",
"list": "Environment variables", "list": "環境變數",
"my_environments": "我的環境", "my_environments": "我的環境",
"name": "Name", "name": "名稱",
"nested_overflow": "巢狀環境變數不得大於 10 層", "nested_overflow": "巢狀環境變數不得大於 10 層",
"new": "建立環境", "new": "建立環境",
"no_active_environment": "No active environment", "no_active_environment": "無使用中的環境",
"no_environment": "無環境", "no_environment": "無環境",
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。", "no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
"quick_peek": "Environment Quick Peek", "quick_peek": "快速預覽環境",
"replace_with_variable": "Replace with variable", "replace_with_variable": "以變數替代",
"scope": "Scope", "scope": "範圍",
"select": "選擇環境", "select": "選擇環境",
"set": "Set environment", "set": "設定環境",
"set_as_environment": "Set as environment", "set_as_environment": "設為環境",
"team_environments": "團隊環境", "team_environments": "團隊環境",
"title": "環境", "title": "環境",
"updated": "更新環境", "updated": "更新環境",
"value": "Value", "value": "數值",
"variable": "Variable", "variable": "變數",
"variable_list": "變數列表" "variable_list": "變數列表"
}, },
"error": { "error": {
@@ -252,7 +252,7 @@
"no_duration": "無持續時間", "no_duration": "無持續時間",
"no_results_found": "找不到結果", "no_results_found": "找不到結果",
"page_not_found": "找不到此頁面", "page_not_found": "找不到此頁面",
"proxy_error": "Proxy error", "proxy_error": "Proxy 錯誤",
"script_fail": "無法執行預請求指令碼", "script_fail": "無法執行預請求指令碼",
"something_went_wrong": "發生了一些錯誤", "something_went_wrong": "發生了一些錯誤",
"test_script_fail": "無法執行測試指令碼" "test_script_fail": "無法執行測試指令碼"
@@ -278,13 +278,13 @@
"renamed": "資料夾已重新命名" "renamed": "資料夾已重新命名"
}, },
"graphql": { "graphql": {
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?", "connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?",
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is", "connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ",
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is", "connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ",
"mutations": "變體", "mutations": "變體",
"schema": "綱要", "schema": "綱要",
"subscriptions": "訂閱", "subscriptions": "訂閱",
"switch_connection": "Switch connection" "switch_connection": "切換連線"
}, },
"group": { "group": {
"time": "時間", "time": "時間",
@@ -339,27 +339,27 @@
"title": "匯入" "title": "匯入"
}, },
"inspections": { "inspections": {
"description": "Inspect possible errors", "description": "檢查潛在錯誤",
"environment": { "environment": {
"add_environment": "Add to Environment", "add_environment": "新增至環境",
"not_found": "Environment variable “{environment}” not found." "not_found": "找不到環境變數 “{environment}”"
}, },
"header": { "header": {
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead." "cookie": "瀏覽器不允許 Hoppscotch 設定 Cookie 標頭。在我們推出 Hoppscotch 桌面版前,請先使用 Authorization 標頭。"
}, },
"response": { "response": {
"401_error": "Please check your authentication credentials.", "401_error": "請檢查您的授權認證。",
"404_error": "Please check your request URL and method type.", "404_error": "請檢查您的請求網址和方式類型。",
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.", "cors_error": "請檢查您的跨來源資源共用設定。",
"default_error": "Please check your request.", "default_error": "請檢查您的請求。",
"network_error": "Please check your network connection." "network_error": "請檢查您的網路連線。"
}, },
"title": "Inspector", "title": "檢查工具",
"url": { "url": {
"extension_not_installed": "Extension not installed.", "extension_not_installed": "未安裝擴充套件。",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.", "extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。",
"extention_enable_action": "Enable Browser Extension", "extention_enable_action": "啟用瀏覽器擴充套件",
"extention_not_enabled": "Extension not enabled." "extention_not_enabled": "未啟用擴充套件。"
} }
}, },
"layout": { "layout": {
@@ -472,7 +472,7 @@
"payload": "負載", "payload": "負載",
"query": "查詢", "query": "查詢",
"raw_body": "原始請求本體", "raw_body": "原始請求本體",
"rename": "Rename Request", "rename": "重新命名請求",
"renamed": "請求已重新命名", "renamed": "請求已重新命名",
"run": "執行", "run": "執行",
"save": "儲存", "save": "儲存",
@@ -510,7 +510,7 @@
"accent_color": "強調色", "accent_color": "強調色",
"account": "帳號", "account": "帳號",
"account_deleted": "已刪除您的帳號", "account_deleted": "已刪除您的帳號",
"account_description": "自定義您的帳號設定。", "account_description": "自您的帳號設定。",
"account_email_description": "您的主要電子郵件地址。", "account_email_description": "您的主要電子郵件地址。",
"account_name_description": "這是您的顯示名稱。", "account_name_description": "這是您的顯示名稱。",
"background": "背景", "background": "背景",
@@ -542,7 +542,7 @@
"read_the": "閱讀", "read_the": "閱讀",
"reset_default": "重置為預設", "reset_default": "重置為預設",
"short_codes": "快捷碼", "short_codes": "快捷碼",
"short_codes_description": "我們為您打造的快捷碼。", "short_codes_description": "您建立的快捷碼。",
"sidebar_on_left": "左側邊欄", "sidebar_on_left": "左側邊欄",
"sync": "同步", "sync": "同步",
"sync_collections": "集合", "sync_collections": "集合",
@@ -551,9 +551,9 @@
"sync_history": "歷史", "sync_history": "歷史",
"system_mode": "系統", "system_mode": "系統",
"telemetry": "遙測服務", "telemetry": "遙測服務",
"telemetry_helps_us": "遙測服務幫助我們進行個化操作,為您提供最佳體驗。", "telemetry_helps_us": "遙測服務能夠幫助我們進行個化操作,為您提供最佳體驗。",
"theme": "主題", "theme": "主題",
"theme_description": "自定義您的應用程式主題。", "theme_description": "自您的應用程式主題。",
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄", "use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
"user": "使用者", "user": "使用者",
"verified_email": "已確認電子郵件地址", "verified_email": "已確認電子郵件地址",
@@ -592,26 +592,26 @@
"title": "導航" "title": "導航"
}, },
"others": { "others": {
"prettify": "Prettify Editor's Content", "prettify": "美化編輯器的內容",
"title": "Others" "title": "其他"
}, },
"request": { "request": {
"copy_request_link": "複製請求連結", "copy_request_link": "複製請求連結",
"delete_method": "選擇 DELETE 方法", "delete_method": "選擇 DELETE 方法",
"get_method": "選擇 GET 方法", "get_method": "選擇 GET 方法",
"head_method": "選擇 HEAD 方法", "head_method": "選擇 HEAD 方法",
"import_curl": "Import cURL", "import_curl": "匯入 cURL",
"method": "方法", "method": "方法",
"next_method": "選擇下一個方法", "next_method": "選擇下一個方法",
"post_method": "選擇 POST 方法", "post_method": "選擇 POST 方法",
"previous_method": "選擇上一個方法", "previous_method": "選擇上一個方法",
"put_method": "選擇 PUT 方法", "put_method": "選擇 PUT 方法",
"rename": "Rename Request", "rename": "重新命名請求",
"reset_request": "重置請求", "reset_request": "重置請求",
"save_request": "Save Request", "save_request": "儲存請求",
"save_to_collections": "儲存到集合", "save_to_collections": "儲存到集合",
"send_request": "傳送請求", "send_request": "傳送請求",
"show_code": "Generate code snippet", "show_code": "產生程式碼片段",
"title": "請求" "title": "請求"
}, },
"response": { "response": {
@@ -642,82 +642,82 @@
"url": "網址" "url": "網址"
}, },
"spotlight": { "spotlight": {
"change_language": "Change Language", "change_language": "變更語言",
"environments": { "environments": {
"delete": "Delete current environment", "delete": "刪除目前環境",
"duplicate": "Duplicate current environment", "duplicate": "複製目前環境",
"duplicate_global": "Duplicate global environment", "duplicate_global": "複製全域環境",
"edit": "Edit current environment", "edit": "編輯目前環境",
"edit_global": "Edit global environment", "edit_global": "編輯全域環境",
"new": "Create new environment", "new": "建立新環境",
"new_variable": "Create a new environment variable", "new_variable": "建立新環境變數",
"title": "Environments" "title": "環境"
}, },
"general": { "general": {
"chat": "Chat with support", "chat": "與客服對話",
"help_menu": "Help and support", "help_menu": "幫助與支援",
"open_docs": "Read Documentation", "open_docs": "閱讀說明文件",
"open_github": "Open GitHub repository", "open_github": "開啟 GitHub 儲存庫",
"open_keybindings": "Keyboard shortcuts", "open_keybindings": "鍵盤快捷鍵",
"social": "Social", "social": "社交",
"title": "General" "title": "一般"
}, },
"graphql": { "graphql": {
"connect": "Connect to server", "connect": "連接至伺服器",
"disconnect": "Disconnect from server" "disconnect": "斷開與伺服器的連線"
}, },
"miscellaneous": { "miscellaneous": {
"invite": "Invite your friends to Hoppscotch", "invite": "邀請您的朋友使用 Hoppscotch",
"title": "Miscellaneous" "title": "雜項"
}, },
"request": { "request": {
"save_as_new": "Save as new request", "save_as_new": "儲存為新請求",
"select_method": "Select method", "select_method": "選擇方法",
"switch_to": "Switch to", "switch_to": "切換至",
"tab_authorization": "Authorization tab", "tab_authorization": "授權分頁",
"tab_body": "Body tab", "tab_body": "本體分頁",
"tab_headers": "Headers tab", "tab_headers": "標頭分頁",
"tab_parameters": "Parameters tab", "tab_parameters": "參數分頁",
"tab_pre_request_script": "Pre-request script tab", "tab_pre_request_script": "預請求腳本分頁",
"tab_query": "Query tab", "tab_query": "查詢分頁",
"tab_tests": "Tests tab", "tab_tests": "測試分頁",
"tab_variables": "Variables tab" "tab_variables": "變數分頁"
}, },
"response": { "response": {
"copy": "Copy response", "copy": "複製回應",
"download": "Download response as file", "download": "下載回應",
"title": "Response" "title": "回應"
}, },
"section": { "section": {
"interceptor": "Interceptor", "interceptor": "攔截器",
"interface": "Interface", "interface": "介面",
"theme": "Theme", "theme": "主題",
"user": "User" "user": "使用者"
}, },
"settings": { "settings": {
"change_interceptor": "Change Interceptor", "change_interceptor": "變更攔截器",
"change_language": "Change Language", "change_language": "變更語言",
"theme": { "theme": {
"black": "Black", "black": "黑色",
"dark": "Dark", "dark": "暗色",
"light": "Light", "light": "亮色",
"system": "System preference" "system": "跟隨系統"
} }
}, },
"tab": { "tab": {
"close_current": "Close current tab", "close_current": "關閉目前分頁",
"close_others": "Close all other tabs", "close_others": "關閉所有其他分頁",
"duplicate": "Duplicate current tab", "duplicate": "複製目前分頁",
"new_tab": "Open a new tab", "new_tab": "開啟新分頁",
"title": "Tabs" "title": "分頁"
}, },
"workspace": { "workspace": {
"delete": "Delete current team", "delete": "刪除目前團隊",
"edit": "Edit current team", "edit": "編輯目前團隊",
"invite": "Invite people to team", "invite": "邀請他人加入團隊",
"new": "Create new team", "new": "建立新團隊",
"switch_to_personal": "Switch to your personal workspace", "switch_to_personal": "切換至您的個人工作區",
"title": "Teams" "title": "團隊"
} }
}, },
"sse": { "sse": {
@@ -777,11 +777,11 @@
"tab": { "tab": {
"authorization": "授權", "authorization": "授權",
"body": "請求本體", "body": "請求本體",
"close": "Close Tab", "close": "關閉分頁",
"close_others": "Close other Tabs", "close_others": "關閉其他分頁",
"collections": "集合", "collections": "集合",
"documentation": "幫助文件", "documentation": "幫助文件",
"duplicate": "Duplicate Tab", "duplicate": "複製分頁",
"environments": "環境", "environments": "環境",
"headers": "請求標頭", "headers": "請求標頭",
"history": "歷史記錄", "history": "歷史記錄",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@hoppscotch/common", "name": "@hoppscotch/common",
"private": true, "private": true,
"version": "2023.8.1", "version": "2023.8.3-1",
"scripts": { "scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*", "dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run", "test": "vitest --run",
@@ -17,22 +17,22 @@
"postinstall": "pnpm run gql-codegen", "postinstall": "pnpm run gql-codegen",
"do-test": "pnpm run test", "do-test": "pnpm run test",
"do-lint": "pnpm run prod-lint", "do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint", "do-typecheck": "node type-check.mjs",
"do-lintfix": "pnpm run lintfix" "do-lintfix": "pnpm run lintfix"
}, },
"dependencies": { "dependencies": {
"@apidevtools/swagger-parser": "^10.1.0", "@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.9.0", "@codemirror/autocomplete": "^6.10.2",
"@codemirror/commands": "^6.2.4", "@codemirror/commands": "^6.3.0",
"@codemirror/lang-javascript": "^6.1.9", "@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1", "@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2", "@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.0", "@codemirror/language": "6.9.0",
"@codemirror/legacy-modes": "^6.3.3", "@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.0", "@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.1", "@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.2.1", "@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.16.0", "@codemirror/view": "^6.22.0",
"@fontsource-variable/inter": "^5.0.8", "@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7", "@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9", "@fontsource-variable/roboto-mono": "^5.0.9",
@@ -41,9 +41,7 @@
"@hoppscotch/js-sandbox": "workspace:^", "@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "workspace:^", "@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0", "@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.1.6", "@lezer/highlight": "1.1.4",
"@sentry/tracing": "^7.64.0",
"@sentry/vue": "^7.64.0",
"@urql/core": "^4.1.1", "@urql/core": "^4.1.1",
"@urql/devtools": "^2.0.3", "@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6", "@urql/exchange-auth": "^2.1.6",
@@ -54,6 +52,7 @@
"acorn-walk": "^8.2.0", "acorn-walk": "^8.2.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "workspace:^", "dioc": "workspace:^",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"events": "^3.3.0", "events": "^3.3.0",
@@ -78,6 +77,8 @@
"process": "^0.11.10", "process": "^0.11.10",
"qs": "^6.11.2", "qs": "^6.11.2",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0", "socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3", "socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1", "socket.io-client-v4": "npm:socket.io-client@^4.4.1",
@@ -100,7 +101,8 @@
"wonka": "^6.3.4", "wonka": "^6.3.4",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"xml-formatter": "^3.5.0", "xml-formatter": "^3.5.0",
"yargs-parser": "^21.1.1" "yargs-parser": "^21.1.1",
"zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@esbuild-plugins/node-globals-polyfill": "^0.2.3",
@@ -138,6 +140,7 @@
"eslint": "^8.47.0", "eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"rollup-plugin-polyfill-node": "^0.12.0", "rollup-plugin-polyfill-node": "^0.12.0",

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

@@ -58,6 +58,8 @@ declare module 'vue' {
CollectionsRequest: typeof import('./components/collections/Request.vue')['default'] CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default'] Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default'] EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default'] EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -90,13 +92,11 @@ declare module 'vue' {
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartAutoComplete: typeof import('@hoppscotch/ui')['HoppSmartAutoComplete']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand'] HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem'] HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink'] HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal'] HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
@@ -143,7 +143,6 @@ declare module 'vue' {
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideBrush: typeof import('~icons/lucide/brush')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
@@ -153,10 +152,9 @@ declare module 'vue' {
IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideRss: typeof import('~icons/lucide/rss')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideVerified: typeof import('~icons/lucide/verified')['default'] InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default'] InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default'] LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default'] LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
@@ -189,7 +187,6 @@ declare module 'vue' {
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default'] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default'] SmartExpand: typeof import('./../../hoppscotch-ui/src/components/smart/Expand.vue')['default']
SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default'] SmartFileChip: typeof import('./../../hoppscotch-ui/src/components/smart/FileChip.vue')['default']
SmartFontSizePicker: typeof import('./components/smart/FontSizePicker.vue')['default']
SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default'] SmartInput: typeof import('./../../hoppscotch-ui/src/components/smart/Input.vue')['default']
SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default'] SmartIntersection: typeof import('./../../hoppscotch-ui/src/components/smart/Intersection.vue')['default']
SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default'] SmartItem: typeof import('./../../hoppscotch-ui/src/components/smart/Item.vue')['default']

View File

@@ -2,53 +2,16 @@
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" /> <AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" /> <AppShare :show="showShare" @hide-modal="showShare = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" /> <FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam()"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { pipe } from "fp-ts/function" import { defineActionHandler } from "~/helpers/actions"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useToast } from "~/composables/toast"
import { useI18n } from "~/composables/i18n"
const toast = useToast()
const t = useI18n()
const showShortcuts = ref(false) const showShortcuts = ref(false)
const showShare = ref(false) const showShare = ref(false)
const showLogin = ref(false) const showLogin = ref(false)
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
defineActionHandler("flyouts.keybinds.toggle", () => { defineActionHandler("flyouts.keybinds.toggle", () => {
showShortcuts.value = !showShortcuts.value showShortcuts.value = !showShortcuts.value
}) })
@@ -60,9 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
defineActionHandler("modals.login.toggle", () => { defineActionHandler("modals.login.toggle", () => {
showLogin.value = !showLogin.value showLogin.value = !showLogin.value
}) })
defineActionHandler("modals.team.delete", ({ teamId }) => {
teamID.value = teamId
confirmRemove.value = true
})
</script> </script>

View File

@@ -20,6 +20,12 @@
<AppInterceptor /> <AppInterceptor />
</template> </template>
</tippy> </tippy>
<HoppButtonSecondary
v-if="platform.platformFeatureFlags.cookiesEnabled ?? false"
:label="t('app.cookies')"
:icon="IconCookie"
@click="showCookiesModal = true"
/>
</div> </div>
<div class="flex"> <div class="flex">
<tippy <tippy
@@ -195,12 +201,17 @@
:show="showDeveloperOptions" :show="showDeveloperOptions"
@hide-modal="showDeveloperOptions = false" @hide-modal="showDeveloperOptions = false"
/> />
<CookiesAllModal
:show="showCookiesModal"
@hide-modal="showCookiesModal = false"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { ref } from "vue"
import { version } from "~/../package.json" import { version } from "~/../package.json"
import IconCookie from "~icons/lucide/cookie"
import IconSidebar from "~icons/lucide/sidebar" import IconSidebar from "~icons/lucide/sidebar"
import IconZap from "~icons/lucide/zap" import IconZap from "~icons/lucide/zap"
import IconShare2 from "~icons/lucide/share-2" import IconShare2 from "~icons/lucide/share-2"
@@ -223,7 +234,9 @@ import { invokeAction } from "@helpers/actions"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
const t = useI18n() const t = useI18n()
const showDeveloperOptions = ref(false) const showDeveloperOptions = ref(false)
const showCookiesModal = ref(false)
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION") const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR = useSetting("SIDEBAR") const SIDEBAR = useSetting("SIDEBAR")

View File

@@ -1,7 +1,9 @@
<template> <template>
<div> <div>
<header <header
ref="headerRef"
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden" class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
> >
<div <div
class="inline-flex items-center justify-start flex-1 space-x-2" class="inline-flex items-center justify-start flex-1 space-x-2"
@@ -231,29 +233,39 @@
@invite-team="inviteTeam(editingTeamName, editingTeamID)" @invite-team="inviteTeam(editingTeamName, editingTeamID)"
@refetch-teams="refetchTeams" @refetch-teams="refetchTeams"
/> />
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam"
/>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, reactive, ref, watch } from "vue"
import IconUser from "~icons/lucide/user"
import IconUsers from "~icons/lucide/users"
import IconSettings from "~icons/lucide/settings"
import IconDownload from "~icons/lucide/download"
import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUserPlus from "~icons/lucide/user-plus"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { defineActionHandler, invokeAction } from "@helpers/actions" import { defineActionHandler, invokeAction } from "@helpers/actions"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
import { computed, reactive, ref, watch } from "vue"
import { useToast } from "~/composables/toast"
import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql"
import { getPlatformSpecialKey } from "~/helpers/platformutils"
import { platform } from "~/platform"
import IconDownload from "~icons/lucide/download"
import IconLifeBuoy from "~icons/lucide/life-buoy"
import IconSettings from "~icons/lucide/settings"
import IconUploadCloud from "~icons/lucide/upload-cloud"
import IconUser from "~icons/lucide/user"
import IconUserPlus from "~icons/lucide/user-plus"
import IconUsers from "~icons/lucide/users"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -278,6 +290,9 @@ const currentUser = useReadonlyStream(
platform.auth.getProbableUser() platform.auth.getProbableUser()
) )
const confirmRemove = ref(false)
const teamID = ref<string | null>(null)
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>() const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
// TeamList-Adapter // TeamList-Adapter
@@ -377,6 +392,24 @@ const handleTeamEdit = () => {
} }
} }
const deleteTeam = () => {
if (!teamID.value) return
pipe(
backendDeleteTeam(teamID.value),
TE.match(
(err) => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
},
() => {
invokeAction("workspace.switch.personal")
toast.success(`${t("team.deleted")}`)
}
)
)() // Tasks (and TEs) are lazy, so call the function returned
}
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
const profile = ref<any | null>(null) const profile = ref<any | null>(null)
@@ -405,6 +438,12 @@ defineActionHandler(
computed(() => !currentUser.value) computed(() => !currentUser.value)
) )
defineActionHandler("modals.team.delete", ({ teamId }) => {
if (selectedTeam.value?.myRole !== TeamMemberRole.Owner) return noPermission()
teamID.value = teamId
confirmRemove.value = true
})
const noPermission = () => { const noPermission = () => {
toast.error(`${t("profile.no_permission")}`) toast.error(`${t("profile.no_permission")}`)
} }

View File

@@ -37,7 +37,8 @@
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { currentActiveTab } from "~/helpers/rest/tab" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
@@ -60,11 +61,12 @@ const emit = defineEmits<{
const editingName = ref("") const editingName = ref("")
const tabs = useService(RESTTabService)
watch( watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = currentActiveTab.value.document.request.name editingName.value = tabs.currentActiveTab.value.document.request.name
} }
} }
) )

View File

@@ -25,7 +25,7 @@
<HoppButtonSecondary <HoppButtonSecondary
v-if="!saveRequest" v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconArchive" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="emit('display-modal-import-export')" @click="emit('display-modal-import-export')"
/> />
@@ -257,12 +257,27 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
{{ t("collection.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')" :label="t('add.new')"
filled filled
outline outline
@click="emit('display-modal-add')" @click="emit('display-modal-add')"
/> />
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'" v-else-if="node.data.type === 'collections'"
@@ -288,8 +303,7 @@
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`" :alt="`${t('empty.folder')}`"
:text="t('empty.folder')" :text="t('empty.folder')"
> />
</HoppSmartPlaceholder>
</template> </template>
</HoppSmartTree> </HoppSmartTree>
</div> </div>
@@ -297,9 +311,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -312,7 +326,8 @@ import { useColorMode } from "@composables/theming"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js" import { Picked } from "~/helpers/types/HoppPicked.js"
import { currentActiveTab } from "~/helpers/rest/tab" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
export type Collection = { export type Collection = {
type: "collections" type: "collections"
@@ -520,7 +535,8 @@ const isSelected = ({
} }
} }
const active = computed(() => currentActiveTab.value.document.saveContext) const tabs = useService(RESTTabService)
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveRequest = (folderPath: string, requestIndex: number) => { const isActiveRequest = (folderPath: string, requestIndex: number) => {
return pipe( return pipe(

View File

@@ -82,12 +82,16 @@ import {
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core" import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform" import { platform } from "~/platform"
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab" import { useService } from "dioc/vue"
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab" import { RESTTabService } from "~/services/tab/rest"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType = type CollectionType =
@@ -123,13 +127,13 @@ const emit = defineEmits<{
}>() }>()
const gqlRequestName = computedWithControl( const gqlRequestName = computedWithControl(
() => activeGQLTab.value, () => GQLTabs.currentActiveTab.value,
() => activeGQLTab.value.document.request.name () => GQLTabs.currentActiveTab.value.document.request.name
) )
const restRequestName = computedWithControl( const restRequestName = computedWithControl(
() => activeRESTTab.value, () => RESTTabs.currentActiveTab.value,
() => activeRESTTab.value.document.request.name () => RESTTabs.currentActiveTab.value.document.request.name
) )
const reqName = computed(() => { const reqName = computed(() => {
@@ -145,12 +149,14 @@ const reqName = computed(() => {
const requestName = ref(reqName.value) const requestName = ref(reqName.value)
watch( watch(
() => [activeRESTTab.value, activeGQLTab.value], () => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
() => { () => {
if (props.mode === "rest") { if (props.mode === "rest") {
requestName.value = activeRESTTab.value?.document.request.name ?? "" requestName.value =
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
} else { } else {
requestName.value = activeGQLTab.value?.document.request.name ?? "" requestName.value =
GQLTabs.currentActiveTab.value?.document.request.name ?? ""
} }
} }
) )
@@ -210,8 +216,8 @@ const saveRequestAs = async () => {
const requestUpdated = const requestUpdated =
props.mode === "rest" props.mode === "rest"
? cloneDeep(activeRESTTab.value.document.request) ? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
: cloneDeep(activeGQLTab.value.document.request) : cloneDeep(GQLTabs.currentActiveTab.value.document.request)
requestUpdated.name = requestName.value requestUpdated.name = requestName.value
@@ -224,7 +230,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -251,7 +257,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -279,7 +285,7 @@ const saveRequestAs = async () => {
requestUpdated requestUpdated
) )
activeRESTTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -439,7 +445,7 @@ const updateTeamCollectionOrFolder = (
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
activeRESTTab.value.document = { RESTTabs.currentActiveTab.value.document = {
request: requestUpdated, request: requestUpdated,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -460,7 +466,7 @@ const updateTeamCollectionOrFolder = (
const requestSaved = () => { const requestSaved = () => {
toast.success(`${t("request.added")}`) toast.success(`${t("request.added")}`)
nextTick(() => { nextTick(() => {
activeRESTTab.value.document.isDirty = false RESTTabs.currentActiveTab.value.document.isDirty = false
}) })
hideModal() hideModal()
} }

View File

@@ -15,12 +15,12 @@
class="!rounded-none" class="!rounded-none"
:icon="IconPlus" :icon="IconPlus"
:title="t('team.no_access')" :title="t('team.no_access')"
:label="t('action.new')" :label="t('add.new')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('action.new')" :label="t('add.new')"
class="!rounded-none" class="!rounded-none"
@click="emit('display-modal-add')" @click="emit('display-modal-add')"
/> />
@@ -39,7 +39,7 @@
collectionsType.type === 'team-collections' && collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined collectionsType.selectedTeam === undefined
" "
:icon="IconArchive" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="emit('display-modal-import-export')" @click="emit('display-modal-import-export')"
/> />
@@ -261,55 +261,68 @@
/> />
</template> </template>
<template #emptyNode="{ node }"> <template #emptyNode="{ node }">
<div v-if="node === null">
<div @drop="(e) => e.stopPropagation()">
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="node === null"
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
@drop.stop
> >
<HoppButtonSecondary <div class="flex flex-col items-center space-y-4">
v-if="hasNoTeamAccess" <span class="text-secondaryLight text-center">
v-tippy="{ theme: 'tooltip' }" {{ t("collection.import_or_create") }}
disabled </span>
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled filled
outline outline
:title="t('team.no_access')" :disabled="hasNoTeamAccess"
:label="t('action.new')" :title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess ? null : emit('display-modal-import-export')
"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else
:icon="IconPlus" :icon="IconPlus"
:label="t('action.new')" :label="t('add.new')"
filled filled
outline outline
@click="emit('display-modal-add')" :disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/> />
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div>
</div>
<div
v-else-if="node.data.type === 'collections'"
@drop="(e) => e.stopPropagation()"
>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
@drop.stop
> >
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
</div>
<div
v-else-if="node.data.type === 'folders'"
@drop="(e) => e.stopPropagation()"
>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"
:src="`/images/states/${colorMode.value}/pack.svg`" :src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`" :alt="`${t('empty.folder')}`"
:text="t('empty.folder')" :text="t('empty.folder')"
> @drop.stop
</HoppSmartPlaceholder> />
</div>
</template> </template>
</HoppSmartTree> </HoppSmartTree>
</div> </div>
@@ -317,9 +330,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { computed, PropType, Ref, toRef } from "vue" import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -335,10 +348,12 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js" import { Picked } from "~/helpers/types/HoppPicked.js"
import { currentActiveTab } from "~/helpers/rest/tab" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(RESTTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
@@ -536,7 +551,7 @@ const isSelected = ({
} }
} }
const active = computed(() => currentActiveTab.value.document.saveContext) const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
const isActiveRequest = (requestID: string) => { const isActiveRequest = (requestID: string) => {
return pipe( return pipe(

View File

@@ -36,11 +36,14 @@
import { ref, watch } from "vue" import { ref, watch } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { currentActiveTab } from "~/helpers/graphql/tab" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
folderPath?: string folderPath?: string
@@ -63,7 +66,7 @@ watch(
() => props.show, () => props.show,
(show) => { (show) => {
if (show) { if (show) {
editingName.value = currentActiveTab.value?.document.request.name editingName.value = tabs.currentActiveTab.value?.document.request.name
} }
} }
) )

View File

@@ -220,7 +220,8 @@ import {
moveGraphqlRequest, moveGraphqlRequest,
} from "~/newstore/collections" } from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked" import { Picked } from "~/helpers/types/HoppPicked"
import { getTabsRefTo } from "~/helpers/graphql/tab" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -235,6 +236,8 @@ const colorMode = useColorMode()
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
// TODO: improve types plz // TODO: improve types plz
const emit = defineEmits<{ const emit = defineEmits<{
(e: "select", i: Picked | null): void (e: "select", i: Picked | null): void
@@ -295,7 +298,7 @@ const removeCollection = () => {
emit("select", null) emit("select", null)
} }
const possibleTabs = getTabsRefTo((tab) => { const possibleTabs = tabs.getTabsRefTo((tab) => {
const ctx = tab.document.saveContext const ctx = tab.document.saveContext
if (!ctx) return false if (!ctx) return false

View File

@@ -203,12 +203,15 @@ import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections" import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
import { computed, ref } from "vue" import { computed, ref } from "vue"
import { getTabsRefTo } from "~/helpers/graphql/tab" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const props = defineProps({ const props = defineProps({
picked: { type: Object, default: null }, picked: { type: Object, default: null },
// Whether the request is in a selectable mode (activates 'select' event) // Whether the request is in a selectable mode (activates 'select' event)
@@ -277,7 +280,7 @@ const removeFolder = () => {
emit("select", { picked: null }) emit("select", { picked: null })
} }
const possibleTabs = getTabsRefTo((tab) => { const possibleTabs = tabs.getTabsRefTo((tab) => {
const ctx = tab.document.saveContext const ctx = tab.document.saveContext
if (!ctx) return false if (!ctx) return false

View File

@@ -258,27 +258,42 @@ const importFromJSON = () => {
inputChooseFileToImportFrom.value.value = "" inputChooseFileToImportFrom.value.value = ""
} }
const exportJSON = () => { const exportJSON = async () => {
const dataToWrite = collectionJson.value const dataToWrite = collectionJson.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
const parsedCollections = JSON.parse(dataToWrite)
if (!parsedCollections.length) {
return toast.error(t("error.no_collections_to_export"))
}
const file = new Blob([dataToWrite], { type: "application/json" })
const url = URL.createObjectURL(file)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
platform?.analytics?.logEvent({ platform?.analytics?.logEvent({
type: "HOPP_EXPORT_COLLECTION", type: "HOPP_EXPORT_COLLECTION",
exporter: "json", exporter: "json",
platform: "gql", platform: "gql",
}) })
// TODO: get uri from meta
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString()) toast.success(t("state.download_started").toString())
setTimeout(() => { }
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
</script> </script>

View File

@@ -137,12 +137,8 @@ import { useToast } from "@composables/toast"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections" import { removeGraphqlRequest } from "~/newstore/collections"
import { import { useService } from "dioc/vue"
createNewTab, import { GQLTabService } from "~/services/tab/graphql"
getTabRefWithSaveContext,
currentTabID,
currentActiveTab,
} from "~/helpers/graphql/tab"
// Template refs // Template refs
const tippyActions = ref<any | null>(null) const tippyActions = ref<any | null>(null)
@@ -154,6 +150,8 @@ const deleteAction = ref<any | null>(null)
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(GQLTabService)
const props = defineProps({ const props = defineProps({
// Whether the object is selected (show the tick mark) // Whether the object is selected (show the tick mark)
picked: { type: Object, default: null }, picked: { type: Object, default: null },
@@ -165,7 +163,7 @@ const props = defineProps({
}) })
const isActive = computed(() => { const isActive = computed(() => {
const saveCtx = currentActiveTab.value?.document.saveContext const saveCtx = tabs.currentActiveTab.value?.document.saveContext
if (!saveCtx) return false if (!saveCtx) return false
@@ -201,7 +199,7 @@ const selectRequest = () => {
if (props.saveRequest) { if (props.saveRequest) {
pick() pick()
} else { } else {
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
requestIndex: props.requestIndex, requestIndex: props.requestIndex,
@@ -209,11 +207,11 @@ const selectRequest = () => {
// Switch to that request if that request is open // Switch to that request if that request is open
if (possibleTab) { if (possibleTab) {
currentTabID.value = possibleTab.value.id tabs.setActiveTab(possibleTab.value.id)
return return
} }
createNewTab({ tabs.createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
@@ -253,7 +251,7 @@ const removeRequest = () => {
} }
// Detach the request from any of the tabs // Detach the request from any of the tabs
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath: props.folderPath, folderPath: props.folderPath,
requestIndex: props.requestIndex, requestIndex: props.requestIndex,

View File

@@ -34,7 +34,7 @@
v-if="!saveRequest" v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="t('modal.import_export')" :title="t('modal.import_export')"
:icon="IconArchive" :icon="IconImport"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
</div> </div>
@@ -66,12 +66,27 @@
:alt="`${t('empty.collections')}`" :alt="`${t('empty.collections')}`"
:text="t('empty.collections')" :text="t('empty.collections')"
> >
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
{{ t("collection.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary <HoppButtonSecondary
:label="t('add.new')" :label="t('add.new')"
filled filled
outline outline
:icon="IconPlus"
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)" v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
@@ -140,12 +155,13 @@ import {
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconArchive from "~icons/lucide/archive" import IconImport from "~icons/lucide/folder-down"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming" import { useColorMode } from "@composables/theming"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
export default defineComponent({ export default defineComponent({
props: { props: {
@@ -158,14 +174,16 @@ export default defineComponent({
const collections = useReadonlyStream(graphqlCollections$, [], "deep") const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
return { return {
collections, collections,
colorMode, colorMode,
t, t,
tabs,
IconPlus, IconPlus,
IconHelpCircle, IconHelpCircle,
IconArchive, IconImport,
} }
}, },
data() { data() {
@@ -267,13 +285,13 @@ export default defineComponent({
}, },
onAddRequest({ name, path, index }) { onAddRequest({ name, path, index }) {
const newRequest = { const newRequest = {
...currentActiveTab.value.document.request, ...this.tabs.currentActiveTab.value.document.request,
name, name,
} }
saveGraphqlRequestAs(path, newRequest) saveGraphqlRequestAs(path, newRequest)
createNewTab({ this.tabs.createNewTab({
saveContext: { saveContext: {
originLocation: "user-collection", originLocation: "user-collection",
folderPath: path, folderPath: path,

View File

@@ -18,13 +18,12 @@
" "
> >
<WorkspaceCurrent :section="t('tab.collections')" /> <WorkspaceCurrent :section="t('tab.collections')" />
<input
<HoppSmartInput
v-model="filterTexts" v-model="filterTexts"
:placeholder="t('action.search')"
input-styles="py-2 pl-4 pr-2 bg-transparent !border-0"
type="search" type="search"
:autofocus="false" autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'" :disabled="collectionsType.type === 'team-collections'"
/> />
</div> </div>
@@ -219,12 +218,6 @@ import {
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { platform } from "~/platform" import { platform } from "~/platform"
import { createCollectionGists } from "~/helpers/gist" import { createCollectionGists } from "~/helpers/gist"
import {
createNewTab,
currentActiveTab,
currentTabID,
getTabRefWithSaveContext,
} from "~/helpers/rest/tab"
import { import {
getRequestsByPath, getRequestsByPath,
resolveSaveContextOnRequestReorder, resolveSaveContextOnRequestReorder,
@@ -239,9 +232,11 @@ import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service" import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps({ const props = defineProps({
saveRequest: { saveRequest: {
@@ -377,22 +372,26 @@ const updateSelectedTeam = (team: SelectedTeam) => {
const workspace = workspaceService.currentWorkspace const workspace = workspaceService.currentWorkspace
// Used to switch collection type and team when user switch workspace in the global workspace switcher // Used to switch collection type and team when user switch workspace in the global workspace switcher
// Check if there is a teamID in the workspace, if yes, switch to team collection and select the team // Check if there is a teamID in the workspace, if yes, switch to team collections and select the team
// If there is no teamID, switch to my environment // If there is no teamID, switch to my collections
watch( watch(
() => { () => {
const space = workspace.value const space = workspace.value
return space.type === "personal" ? undefined : space.teamID
if (space.type === "personal") return undefined
else return space.teamID
}, },
(teamID) => { (teamID) => {
if (!teamID) { if (teamID) {
switchToMyCollections()
} else if (teamID) {
const team = myTeams.value?.find((t) => t.id === teamID) const team = myTeams.value?.find((t) => t.id === teamID)
if (team) updateSelectedTeam(team) if (team) {
updateSelectedTeam(team)
} }
return
}
return switchToMyCollections()
},
{
immediate: true,
} }
) )
@@ -650,7 +649,7 @@ const addRequest = (payload: {
const onAddRequest = (requestName: string) => { const onAddRequest = (requestName: string) => {
const newRequest = { const newRequest = {
...cloneDeep(currentActiveTab.value.document.request), ...cloneDeep(tabs.currentActiveTab.value.document.request),
name: requestName, name: requestName,
} }
@@ -659,7 +658,7 @@ const onAddRequest = (requestName: string) => {
if (!path) return if (!path) return
const insertionIndex = saveRESTRequestAs(path, newRequest) const insertionIndex = saveRESTRequestAs(path, newRequest)
createNewTab({ tabs.createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -708,7 +707,7 @@ const onAddRequest = (requestName: string) => {
(result) => { (result) => {
const { createRequestInCollection } = result const { createRequestInCollection } = result
createNewTab({ tabs.createNewTab({
request: newRequest, request: newRequest,
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -931,7 +930,7 @@ const updateEditingRequest = (newName: string) => {
if (folderPath === null || requestIndex === null) return if (folderPath === null || requestIndex === null) return
const possibleActiveTab = getTabRefWithSaveContext({ const possibleActiveTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
requestIndex, requestIndex,
folderPath, folderPath,
@@ -975,7 +974,7 @@ const updateEditingRequest = (newName: string) => {
) )
)() )()
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID, requestID,
}) })
@@ -1211,7 +1210,7 @@ const onRemoveRequest = () => {
emit("select", null) emit("select", null)
} }
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath, folderPath,
requestIndex, requestIndex,
@@ -1271,7 +1270,7 @@ const onRemoveRequest = () => {
)() )()
// If there is a tab attached to this request, dissociate its state and mark it dirty // If there is a tab attached to this request, dissociate its state and mark it dirty
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID, requestID,
}) })
@@ -1304,14 +1303,14 @@ const selectRequest = (selectedRequest: {
let possibleTab = null let possibleTab = null
if (collectionsType.value.type === "team-collections") { if (collectionsType.value.type === "team-collections") {
possibleTab = getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
if (possibleTab) { if (possibleTab) {
currentTabID.value = possibleTab.value.id tabs.setActiveTab(possibleTab.value.id)
} else { } else {
createNewTab({ tabs.createNewTab({
request: cloneDeep(request), request: cloneDeep(request),
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -1321,16 +1320,16 @@ const selectRequest = (selectedRequest: {
}) })
} }
} else { } else {
possibleTab = getTabRefWithSaveContext({ possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
requestIndex: parseInt(requestIndex), requestIndex: parseInt(requestIndex),
folderPath: folderPath!, folderPath: folderPath!,
}) })
if (possibleTab) { if (possibleTab) {
currentTabID.value = possibleTab.value.id tabs.setActiveTab(possibleTab.value.id)
} else { } else {
// If not, open the request in a new tab // If not, open the request in a new tab
createNewTab({ tabs.createNewTab({
request: cloneDeep(request), request: cloneDeep(request),
isDirty: false, isDirty: false,
saveContext: { saveContext: {
@@ -1373,7 +1372,7 @@ const dropRequest = (payload: {
destinationCollectionIndex destinationCollectionIndex
) )
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection", originLocation: "user-collection",
folderPath, folderPath,
requestIndex: pathToLastIndex(requestIndex), requestIndex: pathToLastIndex(requestIndex),
@@ -1422,7 +1421,7 @@ const dropRequest = (payload: {
1 1
) )
const possibleTab = getTabRefWithSaveContext({ const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection", originLocation: "team-collection",
requestID: requestIndex, requestID: requestIndex,
}) })
@@ -1867,28 +1866,25 @@ const getJSONCollection = async () => {
* @param collectionJSON - JSON string of the collection * @param collectionJSON - JSON string of the collection
* @param name - Name of the collection set as the file name * @param name - Name of the collection set as the file name
*/ */
const initializeDownloadCollection = ( const initializeDownloadCollection = async (
collectionJSON: string, collectionJSON: string,
name: string | null name: string | null
) => { ) => {
const file = new Blob([collectionJSON], { type: "application/json" }) const result = await platform.io.saveFileWithDialog({
const a = document.createElement("a") data: collectionJSON,
const url = URL.createObjectURL(file) contentType: "application/json",
a.href = url suggestedFilename: `${name ?? "collection"}.json`,
filters: [
{
name: "Hoppscotch Collection JSON file",
extensions: ["json"],
},
],
})
if (name) { if (result.type === "unknown" || result.type === "saved") {
a.download = `${name}.json`
} else {
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
}
document.body.appendChild(a)
a.click()
toast.success(t("state.download_started").toString()) toast.success(t("state.download_started").toString())
setTimeout(() => { }
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
/** /**
@@ -1917,11 +1913,14 @@ const exportData = async (
exportLoading.value = false exportLoading.value = false
return return
}, },
(coll) => { async (coll) => {
const hoppColl = teamCollToHoppRESTColl(coll) const hoppColl = teamCollToHoppRESTColl(coll)
const collectionJSONString = JSON.stringify(hoppColl) const collectionJSONString = JSON.stringify(hoppColl)
initializeDownloadCollection(collectionJSONString, hoppColl.name) await initializeDownloadCollection(
collectionJSONString,
hoppColl.name
)
exportLoading.value = false exportLoading.value = false
} }
) )
@@ -1938,6 +1937,12 @@ const exportJSONCollection = async () => {
await getJSONCollection() await getJSONCollection()
const parsedCollections = JSON.parse(collectionJSON.value)
if (!parsedCollections.length) {
return toast.error(t("error.no_collections_to_export"))
}
initializeDownloadCollection(collectionJSON.value, null) initializeDownloadCollection(collectionJSON.value, null)
} }

View File

@@ -0,0 +1,269 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('app.cookies')"
aria-modal="true"
@close="hideModal"
>
<template #body>
<HoppSmartPlaceholder
v-if="!currentInterceptorSupportsCookies"
:text="t('cookies.modal.interceptor_no_support')"
>
<AppInterceptor class="p-2 border rounded border-dividerLight" />
</HoppSmartPlaceholder>
<div v-else class="flex flex-col">
<div
class="flex bg-primary space-x-2 border-b sticky border-dividerLight -mx-4 px-4 py-4 -mt-4"
style="top: calc(-1 * var(--line-height-body))"
>
<HoppSmartInput
v-model="newDomainText"
class="flex-1"
:placeholder="t('cookies.modal.new_domain_name')"
@keyup.enter="addNewDomain"
/>
<HoppButtonSecondary
outline
filled
:label="t('action.add')"
@click="addNewDomain"
/>
</div>
<div class="flex flex-col space-y-4">
<HoppSmartPlaceholder
v-if="workingCookieJar.size === 0"
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
>
</HoppSmartPlaceholder>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else
:key="domain"
class="flex flex-col"
>
<div class="flex items-center justify-between flex-1">
<label for="cookiesList" class="p-4">
{{ domain }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.delete')"
:icon="IconTrash2"
@click="deleteDomain(domain)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
@click="addCookieToDomain(domain)"
/>
</div>
</div>
<div class="border rounded border-divider">
<div class="divide-y divide-dividerLight">
<div
v-if="entries.length === 0"
class="flex flex-col gap-2 p-4 items-center"
>
{{ t("cookies.modal.no_cookies_in_domain") }}
</div>
<template v-else>
<div
v-for="(entry, entryIndex) in entries"
:key="`${entry}-${entryIndex}`"
class="flex divide-x divide-dividerLight"
>
<input
class="flex flex-1 px-4 py-2 bg-transparent"
:value="entry"
readonly
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.edit')"
:icon="IconEdit"
@click="editCookie(domain, entryIndex, entry)"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteCookie(domain, entryIndex)"
/>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<template v-if="currentInterceptorSupportsCookies" #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChanges"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChanges"
/>
</span>
<HoppButtonSecondary
:label="t('action.clear_all')"
outline
filled
@click="clearAllDomains"
/>
</template>
</HoppSmartModal>
<CookiesEditCookie
:show="!!showEditModalFor"
:entry="showEditModalFor"
@save-cookie="saveCookie"
@hide-modal="showEditModalFor = null"
/>
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useService } from "dioc/vue"
import { CookieJarService } from "~/services/cookie-jar.service"
import IconTrash from "~icons/lucide/trash"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconPlus from "~icons/lucide/plus"
import { cloneDeep } from "lodash-es"
import { ref, watch, computed } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { EditCookieConfig } from "./EditCookie.vue"
import { useColorMode } from "@composables/theming"
import { useToast } from "@composables/toast"
const props = defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
const t = useI18n()
const colorMode = useColorMode()
const toast = useToast()
const newDomainText = ref("")
const interceptorService = useService(InterceptorService)
const cookieJarService = useService(CookieJarService)
const workingCookieJar = ref(cloneDeep(cookieJarService.cookieJar.value))
const currentInterceptorSupportsCookies = computed(() => {
const currentInterceptor = interceptorService.currentInterceptor.value
if (!currentInterceptor) return true
return currentInterceptor.supportsCookies ?? false
})
function addNewDomain() {
if (newDomainText.value === "" || /^\s+$/.test(newDomainText.value)) {
toast.error(`${t("cookies.modal.empty_domain")}`)
return
}
workingCookieJar.value.set(newDomainText.value, [])
newDomainText.value = ""
}
function deleteDomain(domain: string) {
workingCookieJar.value.delete(domain)
}
function addCookieToDomain(domain: string) {
showEditModalFor.value = { type: "create", domain }
}
function clearAllDomains() {
workingCookieJar.value = new Map()
toast.success(`${t("state.cleared")}`)
}
watch(
() => props.show,
(show) => {
if (show) {
workingCookieJar.value = cloneDeep(cookieJarService.cookieJar.value)
}
}
)
const showEditModalFor = ref<EditCookieConfig | null>(null)
function saveCookieChanges() {
cookieJarService.cookieJar.value = workingCookieJar.value
hideModal()
}
function cancelCookieChanges() {
hideModal()
}
function editCookie(domain: string, entryIndex: number, cookieEntry: string) {
showEditModalFor.value = {
type: "edit",
domain,
entryIndex,
currentCookieEntry: cookieEntry,
}
}
function deleteCookie(domain: string, entryIndex: number) {
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry.splice(entryIndex, 1)
}
}
function saveCookie(cookie: string) {
if (showEditModalFor.value?.type === "create") {
const { domain } = showEditModalFor.value
const entry = workingCookieJar.value.get(domain)!
entry.push(cookie)
showEditModalFor.value = null
return
}
if (showEditModalFor.value?.type !== "edit") return
const { domain, entryIndex } = showEditModalFor.value!
const entry = workingCookieJar.value.get(domain)
if (entry) {
entry[entryIndex] = cookie
}
showEditModalFor.value = null
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -0,0 +1,195 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('cookies.modal.set')"
@close="hideModal"
>
<template #body>
<div class="border rounded border-dividerLight">
<div class="flex flex-col">
<div class="flex items-center justify-between pl-4">
<label class="font-semibold truncate text-secondaryLight">
{{ t("cookies.modal.cookie_string") }}
</label>
<div class="flex items-center">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': linewrapEnabled }"
:icon="IconWrapText"
@click.prevent="linewrapEnabled = !linewrapEnabled"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="t('action.download_file')"
:icon="downloadIcon"
@click="downloadResponse"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip', allowHTML: true }"
:title="t('action.copy')"
:icon="copyIcon"
@click="copyResponse"
/>
</div>
</div>
<div class="h-46">
<div
ref="cookieEditor"
class="h-full border-t rounded-b border-dividerLight"
></div>
</div>
</div>
</div>
</template>
<template #footer>
<div class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t('action.save')"
outline
@click="saveCookieChange"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="cancelCookieChange"
/>
</div>
<span class="flex">
<HoppButtonSecondary
:icon="pasteIcon"
:label="`${t('action.paste')}`"
filled
outline
@click="handlePaste"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script lang="ts">
export type EditCookieConfig =
| { type: "create"; domain: string }
| {
type: "edit"
domain: string
entryIndex: number
currentCookieEntry: string
}
</script>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useCodemirror } from "~/composables/codemirror"
import { watch, ref, reactive } from "vue"
import { refAutoReset } from "@vueuse/core"
import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2"
import { useToast } from "~/composables/toast"
import {
useCopyResponse,
useDownloadResponse,
} from "~/composables/lens-actions"
// TODO: Build Managed Mode!
const props = defineProps<{
show: boolean
entry: EditCookieConfig | null
}>()
const emit = defineEmits<{
(e: "save-cookie", cookie: string): void
(e: "hide-modal"): void
}>()
const t = useI18n()
const toast = useToast()
const cookieEditor = ref<HTMLElement>()
const rawCookieString = ref("")
const linewrapEnabled = ref(true)
useCodemirror(
cookieEditor,
rawCookieString,
reactive({
extendedEditorConfig: {
mode: "text/plain",
placeholder: `${t("cookies.modal.enter_cookie_string")}`,
lineWrapping: linewrapEnabled,
},
linter: null,
completer: null,
environmentHighlights: false,
})
)
const pasteIcon = refAutoReset<typeof IconClipboard | typeof IconCheck>(
IconClipboard,
1000
)
watch(
() => props.entry,
() => {
if (!props.entry) return
if (props.entry.type === "create") {
rawCookieString.value = ""
return
}
rawCookieString.value = props.entry.currentCookieEntry
}
)
function hideModal() {
emit("hide-modal")
}
function cancelCookieChange() {
hideModal()
}
async function handlePaste() {
try {
const text = await navigator.clipboard.readText()
if (text) {
rawCookieString.value = text
pasteIcon.value = IconCheck
}
} catch (e) {
console.error("Failed to copy: ", e)
toast.error(t("profile.no_permission").toString())
}
}
function saveCookieChange() {
emit("save-cookie", rawCookieString.value)
}
const { copyIcon, copyResponse } = useCopyResponse(rawCookieString)
const { downloadIcon, downloadResponse } = useDownloadResponse(
"",
rawCookieString
)
function clearContent() {
rawCookieString.value = ""
}
</script>

View File

@@ -83,11 +83,14 @@ import {
import * as TE from "fp-ts/TaskEither" import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function" import { pipe } from "fp-ts/function"
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment" import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { currentActiveTab } from "~/helpers/rest/tab" import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const props = defineProps<{ const props = defineProps<{
show: boolean show: boolean
position: { top: number; left: number } position: { top: number; left: number }
@@ -189,8 +192,8 @@ const addEnvironment = async () => {
//replace the current tab endpoint with the variable name with << and >> //replace the current tab endpoint with the variable name with << and >>
const variableName = `<<${editingName.value}>>` const variableName = `<<${editingName.value}>>`
//replace the currenttab endpoint containing the value in the text with variablename //replace the currenttab endpoint containing the value in the text with variablename
currentActiveTab.value.document.request.endpoint = tabs.currentActiveTab.value.document.request.endpoint =
currentActiveTab.value.document.request.endpoint.replace( tabs.currentActiveTab.value.document.request.endpoint.replace(
editingValue.value, editingValue.value,
variableName variableName
) )

View File

@@ -375,22 +375,37 @@ const importFromPostman = ({
importFromHoppscotch(environments) importFromHoppscotch(environments)
} }
const exportJSON = () => { const exportJSON = async () => {
const dataToWrite = environmentJson.value const dataToWrite = environmentJson.value
const file = new Blob([dataToWrite], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta const parsedCollections = JSON.parse(dataToWrite)
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a) if (!parsedCollections.length) {
a.click() return toast.error(t("error.no_environments_to_export"))
toast.success(t("state.download_started").toString()) }
setTimeout(() => {
document.body.removeChild(a) const file = new Blob([dataToWrite], { type: "application/json" })
const url = URL.createObjectURL(file)
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
}, 1000)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
toast.success(t("state.download_started").toString())
}
} }
const getErrorMessage = (err: GQLError<string>) => { const getErrorMessage = (err: GQLError<string>) => {

View File

@@ -66,7 +66,7 @@
/> />
<HoppSmartTabs <HoppSmartTabs
v-model="selectedEnvTab" v-model="selectedEnvTab"
:styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-0 top-0 bg-primary ${ :styles="`sticky overflow-x-auto my-2 border border-divider rounded flex-shrink-0 z-10 top-0 bg-primary ${
!isTeamSelected || workspace.type === 'personal' !isTeamSelected || workspace.type === 'personal'
? 'bg-primaryLight' ? 'bg-primaryLight'
: '' : ''
@@ -478,7 +478,8 @@ watch(
teamEnvListAdapter.changeTeamID(newVal.teamID) teamEnvListAdapter.changeTeamID(newVal.teamID)
} }
} }
} },
{ immediate: true }
) )
const selectedEnv = computed(() => { const selectedEnv = computed(() => {

View File

@@ -46,6 +46,7 @@
role="menu" role="menu"
@keyup.e="edit!.$el.click()" @keyup.e="edit!.$el.click()"
@keyup.d="duplicate!.$el.click()" @keyup.d="duplicate!.$el.click()"
@keyup.j="exportAsJsonEl!.$el.click()"
@keyup.delete=" @keyup.delete="
!(environmentIndex === 'Global') !(environmentIndex === 'Global')
? deleteAction!.$el.click() ? deleteAction!.$el.click()
@@ -77,6 +78,18 @@
} }
" "
/> />
<HoppSmartItem
ref="exportAsJsonEl"
:icon="IconEdit"
:label="`${t('export.as_json')}`"
:shortcut="['J']"
@click="
() => {
exportEnvironmentAsJSON()
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
v-if="environmentIndex !== 'Global'" v-if="environmentIndex !== 'Global'"
ref="deleteAction" ref="deleteAction"
@@ -121,6 +134,7 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -136,10 +150,18 @@ const emit = defineEmits<{
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () => {
const { environment, environmentIndex } = props
exportAsJSON(environment, environmentIndex)
? toast.success(t("state.download_started"))
: toast.error(t("state.download_failed"))
}
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof HoppSmartItem>() const edit = ref<typeof HoppSmartItem>()
const duplicate = ref<typeof HoppSmartItem>() const duplicate = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>() const deleteAction = ref<typeof HoppSmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {

View File

@@ -19,7 +19,7 @@
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconArchive" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
@@ -33,17 +33,32 @@
@edit-environment="editEnvironment(index)" @edit-environment="editEnvironment(index)"
/> />
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="environments.length === 0" v-if="!environments.length"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<div class="flex flex-col items-center space-y-4">
<span class="text-secondaryLight text-center">
{{ t("environment.import_or_create") }}
</span>
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary <HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
@click="displayModalAdd(true)" @click="displayModalAdd(true)"
/> />
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<EnvironmentsMyDetails <EnvironmentsMyDetails
:show="showModalDetails" :show="showModalDetails"
@@ -66,8 +81,8 @@ import { environments$ } from "~/newstore/environments"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import { useReadonlyStream } from "@composables/stream" import { useReadonlyStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import IconArchive from "~icons/lucide/archive"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconImport from "~icons/lucide/folder-down"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import { Environment } from "@hoppscotch/data" import { Environment } from "@hoppscotch/data"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"

View File

@@ -39,6 +39,7 @@
role="menu" role="menu"
@keyup.e="edit!.$el.click()" @keyup.e="edit!.$el.click()"
@keyup.d="duplicate!.$el.click()" @keyup.d="duplicate!.$el.click()"
@keyup.j="exportAsJsonEl!.$el.click()"
@keyup.delete="deleteAction!.$el.click()" @keyup.delete="deleteAction!.$el.click()"
@keyup.escape="options!.tippy().hide()" @keyup.escape="options!.tippy().hide()"
> >
@@ -54,6 +55,7 @@
} }
" "
/> />
<HoppSmartItem <HoppSmartItem
ref="duplicate" ref="duplicate"
:icon="IconCopy" :icon="IconCopy"
@@ -66,6 +68,18 @@
} }
" "
/> />
<HoppSmartItem
ref="exportAsJsonEl"
:icon="IconEdit"
:label="`${t('export.as_json')}`"
:shortcut="['J']"
@click="
() => {
exportEnvironmentAsJSON()
hide()
}
"
/>
<HoppSmartItem <HoppSmartItem
ref="deleteAction" ref="deleteAction"
:icon="IconTrash2" :icon="IconTrash2"
@@ -109,6 +123,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconMoreVertical from "~icons/lucide/more-vertical" import IconMoreVertical from "~icons/lucide/more-vertical"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { HoppSmartItem } from "@hoppscotch/ui" import { HoppSmartItem } from "@hoppscotch/ui"
import { exportAsJSON } from "~/helpers/import-export/export/environment"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -124,11 +139,17 @@ const emit = defineEmits<{
const confirmRemove = ref(false) const confirmRemove = ref(false)
const exportEnvironmentAsJSON = () =>
exportAsJSON(props.environment)
? toast.success(t("state.download_started"))
: toast.error(t("state.download_failed"))
const tippyActions = ref<TippyComponent | null>(null) const tippyActions = ref<TippyComponent | null>(null)
const options = ref<TippyComponent | null>(null) const options = ref<TippyComponent | null>(null)
const edit = ref<typeof HoppSmartItem>() const edit = ref<typeof HoppSmartItem>()
const duplicate = ref<typeof HoppSmartItem>() const duplicate = ref<typeof HoppSmartItem>()
const deleteAction = ref<typeof HoppSmartItem>() const deleteAction = ref<typeof HoppSmartItem>()
const exportAsJsonEl = ref<typeof HoppSmartItem>()
const removeEnvironment = () => { const removeEnvironment = () => {
pipe( pipe(

View File

@@ -31,40 +31,49 @@
v-if="team !== undefined && team.myRole === 'VIEWER'" v-if="team !== undefined && team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
disabled disabled
:icon="IconArchive" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:icon="IconArchive" :icon="IconImport"
:title="t('modal.import_export')" :title="t('modal.import_export')"
@click="displayModalImportExport(true)" @click="displayModalImportExport(true)"
/> />
</div> </div>
</div> </div>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="!loading && teamEnvironments.length === 0 && !adapterError" v-if="!loading && !teamEnvironments.length && !adapterError"
:src="`/images/states/${colorMode.value}/blockchain.svg`" :src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`" :alt="`${t('empty.environments')}`"
:text="t('empty.environments')" :text="t('empty.environments')"
> >
<HoppButtonSecondary <div class="flex flex-col items-center space-y-4">
v-if="team === undefined || team.myRole === 'VIEWER'" <span class="text-secondaryLight text-center">
v-tippy="{ theme: 'tooltip' }" {{ t("environment.import_or_create") }}
disabled </span>
<div class="flex gap-4 flex-col items-stretch">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled filled
:icon="IconPlus" outline
:title="t('team.no_access')" :title="isTeamViewer ? t('team.no_access') : ''"
:label="t('action.new')" :disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else
:label="`${t('add.new')}`" :label="`${t('add.new')}`"
filled filled
outline outline
@click="displayModalAdd(true)" :icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/> />
</div>
</div>
</HoppSmartPlaceholder> </HoppSmartPlaceholder>
<div v-else-if="!loading"> <div v-else-if="!loading">
<EnvironmentsTeamsEnvironment <EnvironmentsTeamsEnvironment
@@ -108,14 +117,14 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue" import { computed, ref } from "vue"
import { GQLError } from "~/helpers/backend/GQLClient" import { GQLError } from "~/helpers/backend/GQLClient"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment" import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useColorMode } from "~/composables/theming" import { useColorMode } from "~/composables/theming"
import IconPlus from "~icons/lucide/plus" import IconPlus from "~icons/lucide/plus"
import IconArchive from "~icons/lucide/archive"
import IconHelpCircle from "~icons/lucide/help-circle" import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql" import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -138,6 +147,8 @@ const action = ref<"new" | "edit">("edit")
const editingEnvironment = ref<TeamEnvironment | null>(null) const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("") const editingVariableName = ref("")
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => { const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new" action.value = "new"
showModalDetails.value = shouldDisplay showModalDetails.value = shouldDisplay

View File

@@ -1,6 +1,10 @@
<template> <template>
<div> <div>
<div class="field-title" :class="{ 'field-highlighted': isHighlighted }"> <div class="flex justify-between gap-2">
<div
class="field-title flex-1"
:class="{ 'field-highlighted': isHighlighted }"
>
{{ fieldName }} {{ fieldName }}
<span v-if="fieldArgs.length > 0"> <span v-if="fieldArgs.length > 0">
( (
@@ -8,43 +12,44 @@
{{ field.name }}: {{ field.name }}:
<GraphqlTypeLink <GraphqlTypeLink
:gql-type="field.type" :gql-type="field.type"
:jump-type-callback="jumpTypeCallback" @jump-to-type="jumpToType"
/> />
<span v-if="index !== fieldArgs.length - 1">, </span> <span v-if="index !== fieldArgs.length - 1">, </span>
</span> </span>
) </span ) </span
>: >:
<GraphqlTypeLink <GraphqlTypeLink :gql-type="gqlField.type" @jump-to-type="jumpToType" />
:gql-type="gqlField.type" </div>
:jump-type-callback="jumpTypeCallback" <div v-if="gqlField.deprecationReason">
/> <span
v-tippy="{ theme: 'tomato' }"
class="!text-red-500 hover:!text-red-600 text-xs flex items-center gap-2 cursor-pointer"
:title="gqlField.deprecationReason"
>
<IconAlertTriangle /> {{ t("state.deprecated") }}
</span>
</div>
</div> </div>
<div <div
v-if="gqlField.description" v-if="gqlField.description"
class="py-2 text-secondaryLight field-desc" class="field-desc py-2 text-secondaryLight"
> >
{{ gqlField.description }} {{ gqlField.description }}
</div> </div>
<div
v-if="gqlField.isDeprecated"
class="inline-block px-2 py-1 my-1 text-black bg-yellow-200 rounded field-deprecated"
>
{{ t("state.deprecated") }}
</div>
<div v-if="fieldArgs.length > 0"> <div v-if="fieldArgs.length > 0">
<h5 class="my-2">Arguments:</h5> <h5 class="my-2">Arguments:</h5>
<div class="pl-4 border-l-2 border-divider"> <div class="border-l-2 border-divider pl-4">
<div v-for="(field, index) in fieldArgs" :key="`field-${index}`"> <div v-for="(field, index) in fieldArgs" :key="`field-${index}`">
<span> <span>
{{ field.name }}: {{ field.name }}:
<GraphqlTypeLink <GraphqlTypeLink
:gql-type="field.type" :gql-type="field.type"
:jump-type-callback="jumpTypeCallback" @jump-to-type="jumpToType"
/> />
</span> </span>
<div <div
v-if="field.description" v-if="field.description"
class="py-2 text-secondaryLight field-desc" class="field-desc py-2 text-secondaryLight"
> >
{{ field.description }} {{ field.description }}
</div> </div>
@@ -54,37 +59,41 @@
</div> </div>
</template> </template>
<script> <script setup lang="ts">
// TypeScript + Script Setup this :)
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { GraphQLType } from "graphql"
import { computed } from "vue"
import IconAlertTriangle from "~icons/lucide/alert-triangle"
export default defineComponent({ const t = useI18n()
props: {
gqlField: { type: Object, default: () => ({}) }, const props = withDefaults(
jumpTypeCallback: { type: Function, default: () => ({}) }, defineProps<{
isHighlighted: { type: Boolean, default: false }, gqlField: any
}, isHighlighted: boolean
setup() { }>(),
return { {
t: useI18n(), gqlField: {},
isHighlighted: false,
} }
}, )
computed: {
fieldName() {
return this.gqlField.name
},
fieldArgs() { const emit = defineEmits<{
return this.gqlField.args || [] (e: "jump-to-type", type: GraphQLType): void
}, }>()
},
}) const fieldName = computed(() => props.gqlField.name)
const fieldArgs = computed(() => props.gqlField.args || [])
const jumpToType = (type: GraphQLType) => {
emit("jump-to-type", type)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.field-highlighted { .field-highlighted {
@apply border-accent border-b-2; @apply border-b-2 border-accent;
} }
.field-title { .field-title {

View File

@@ -64,7 +64,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { platform } from "~/platform" import { platform } from "~/platform"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { currentActiveTab } from "~/helpers/graphql/tab"
import { computed, ref, watch } from "vue" import { computed, ref, watch } from "vue"
import { connection } from "~/helpers/graphql/connection" import { connection } from "~/helpers/graphql/connection"
import { connect } from "~/helpers/graphql/connection" import { connect } from "~/helpers/graphql/connection"
@@ -72,8 +71,10 @@ import { disconnect } from "~/helpers/graphql/connection"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
@@ -82,9 +83,9 @@ const connectionSwitchModal = ref(false)
const connected = computed(() => connection.state === "CONNECTED") const connected = computed(() => connection.state === "CONNECTED")
const url = computed({ const url = computed({
get: () => currentActiveTab.value?.document.request.url ?? "", get: () => tabs.currentActiveTab.value?.document.request.url ?? "",
set: (value) => { set: (value) => {
currentActiveTab.value!.document.request.url = value tabs.currentActiveTab.value!.document.request.url = value
}, },
}) })
@@ -97,7 +98,7 @@ const onConnectClick = () => {
} }
const gqlConnect = () => { const gqlConnect = () => {
connect(url.value, currentActiveTab.value?.document.request.headers) connect(url.value, tabs.currentActiveTab.value?.document.request.headers)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REQUEST_RUN", type: "HOPP_REQUEST_RUN",
@@ -114,7 +115,7 @@ const switchConnection = () => {
const lastTwoUrls = ref<string[]>([]) const lastTwoUrls = ref<string[]>([])
watch( watch(
currentActiveTab, tabs.currentActiveTab,
(newVal) => { (newVal) => {
if (newVal) { if (newVal) {
lastTwoUrls.value.push(newVal.document.request.url) lastTwoUrls.value.push(newVal.document.request.url)

View File

@@ -58,8 +58,7 @@ import { computed, ref, watch } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { HoppGQLRequest } from "@hoppscotch/data" import { HoppGQLRequest } from "@hoppscotch/data"
import { platform } from "~/platform" import { platform } from "~/platform"
import { currentActiveTab } from "~/helpers/graphql/tab" import { computedWithControl, useVModel } from "@vueuse/core"
import { computedWithControl } from "@vueuse/core"
import { import {
GQLResponseEvent, GQLResponseEvent,
runGQLOperation, runGQLOperation,
@@ -68,26 +67,39 @@ import {
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections" import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
const VALID_GQL_OPERATIONS = [
"query",
"headers",
"variables",
"authorization",
] as const
export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number]
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
const selectedOptionTab = ref<GQLOptionTabs>("query")
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(GQLTabService)
// v-model integration with props and emit // v-model integration with props and emit
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue: HoppGQLRequest modelValue: HoppGQLRequest
response?: GQLResponseEvent[] | null response?: GQLResponseEvent[] | null
optionTab?: GQLOptionTabs
tabId: string tabId: string
}>(), }>(),
{ {
response: null, response: null,
optionTab: "query",
} }
) )
const emit = defineEmits(["update:modelValue", "update:response"]) const emit = defineEmits(["update:modelValue", "update:response"])
const selectedOptionTab = useVModel(props, "optionTab", emit)
const request = ref(props.modelValue) const request = ref(props.modelValue)
@@ -100,8 +112,8 @@ watch(
) )
const url = computedWithControl( const url = computedWithControl(
() => currentActiveTab.value, () => tabs.currentActiveTab.value,
() => currentActiveTab.value.document.request.url () => tabs.currentActiveTab.value.document.request.url
) )
const activeGQLHeadersCount = computed( const activeGQLHeadersCount = computed(
@@ -136,6 +148,9 @@ const runQuery = async (
const duration = Date.now() - startTime const duration = Date.now() - startTime
completePageProgress() completePageProgress()
toast.success(`${t("state.finished_in", { duration })}`) toast.success(`${t("state.finished_in", { duration })}`)
if (definition?.operation === "subscription" && request.value.auth) {
toast.success(t("authorization.graphql_headers"))
}
} catch (e: any) { } catch (e: any) {
console.log(e) console.log(e)
// response.value = [`${e}`] // response.value = [`${e}`]
@@ -182,17 +197,17 @@ const hideRequestModal = () => {
} }
const saveRequest = () => { const saveRequest = () => {
if ( if (
currentActiveTab.value.document.saveContext && tabs.currentActiveTab.value.document.saveContext &&
currentActiveTab.value.document.saveContext.originLocation === tabs.currentActiveTab.value.document.saveContext.originLocation ===
"user-collection" "user-collection"
) { ) {
editGraphqlRequest( editGraphqlRequest(
currentActiveTab.value.document.saveContext.folderPath, tabs.currentActiveTab.value.document.saveContext.folderPath,
currentActiveTab.value.document.saveContext.requestIndex, tabs.currentActiveTab.value.document.saveContext.requestIndex,
currentActiveTab.value.document.request tabs.currentActiveTab.value.document.request
) )
currentActiveTab.value.document.isDirty = false tabs.currentActiveTab.value.document.isDirty = false
} else { } else {
showSaveRequestModal.value = true showSaveRequestModal.value = true
} }

View File

@@ -3,12 +3,13 @@
<template #primary> <template #primary>
<GraphqlRequestOptions <GraphqlRequestOptions
v-model="tab.document.request" v-model="tab.document.request"
v-model:response="tab.response" v-model:response="tab.document.response"
v-model:option-tab="tab.document.optionTabPreference"
:tab-id="tab.id" :tab-id="tab.id"
/> />
</template> </template>
<template #secondary> <template #secondary>
<GraphqlResponse :response="tab.response" /> <GraphqlResponse :response="tab.document.response" />
</template> </template>
</AppPaneLayout> </AppPaneLayout>
</template> </template>
@@ -18,14 +19,15 @@ import { useVModel } from "@vueuse/core"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { watch } from "vue" import { watch } from "vue"
import { isEqualHoppGQLRequest } from "~/helpers/graphql" import { isEqualHoppGQLRequest } from "~/helpers/graphql"
import { HoppGQLTab } from "~/helpers/graphql/tab" import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppTab } from "~/services/tab"
// TODO: Move Response and Request execution code to over here // TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppGQLTab }>() const props = defineProps<{ modelValue: HoppTab<HoppGQLDocument> }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", val: HoppGQLTab): void (e: "update:modelValue", val: HoppTab<HoppGQLDocument>): void
}>() }>()
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)

View File

@@ -59,6 +59,7 @@ import { useToast } from "@composables/toast"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils" import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
import { GQLResponseEvent } from "~/helpers/graphql/connection" import { GQLResponseEvent } from "~/helpers/graphql/connection"
import { platform } from "~/platform"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -111,21 +112,31 @@ const copyResponse = (str: string) => {
toast.success(`${t("state.copied_to_clipboard")}`) toast.success(`${t("state.copied_to_clipboard")}`)
} }
const downloadResponse = (str: string) => { const downloadResponse = async (str: string) => {
const dataToWrite = str const dataToWrite = str
const file = new Blob([dataToWrite!], { type: "application/json" }) const file = new Blob([dataToWrite!], { type: "application/json" })
const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}` const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
document.body.appendChild(a)
a.click() URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/json",
suggestedFilename: filename,
filters: [
{
name: "JSON file",
extensions: ["json"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
downloadResponseIcon.value = IconCheck downloadResponseIcon.value = IconCheck
toast.success(`${t("state.download_started")}`) toast.success(`${t("state.download_started")}`)
setTimeout(() => { }
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
defineActionHandler( defineActionHandler(

View File

@@ -30,8 +30,8 @@
v-model="graphqlFieldsFilterText" v-model="graphqlFieldsFilterText"
type="search" type="search"
autocomplete="off" autocomplete="off"
class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="`${t('action.search')}`" :placeholder="`${t('action.search')}`"
class="flex flex-1 p-4 py-2 bg-transparent"
/> />
<div class="flex"> <div class="flex">
<HoppButtonSecondary <HoppButtonSecondary
@@ -58,8 +58,8 @@
v-for="(field, index) in filteredQueryFields" v-for="(field, index) in filteredQueryFields"
:key="`field-${index}`" :key="`field-${index}`"
:gql-field="field" :gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4" class="p-4"
@jump-to-type="handleJumpToType"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -72,8 +72,8 @@
v-for="(field, index) in filteredMutationFields" v-for="(field, index) in filteredMutationFields"
:key="`field-${index}`" :key="`field-${index}`"
:gql-field="field" :gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4" class="p-4"
@jump-to-type="handleJumpToType"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -86,8 +86,8 @@
v-for="(field, index) in filteredSubscriptionFields" v-for="(field, index) in filteredSubscriptionFields"
:key="`field-${index}`" :key="`field-${index}`"
:gql-field="field" :gql-field="field"
:jump-type-callback="handleJumpToType"
class="p-4" class="p-4"
@jump-to-type="handleJumpToType"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -103,7 +103,7 @@
:gql-types="graphqlTypes" :gql-types="graphqlTypes"
:is-highlighted="isGqlTypeHighlighted(type)" :is-highlighted="isGqlTypeHighlighted(type)"
:highlighted-fields="getGqlTypeHighlightedFields(type)" :highlighted-fields="getGqlTypeHighlightedFields(type)"
:jump-type-callback="handleJumpToType" @jump-to-type="handleJumpToType"
/> />
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
@@ -202,6 +202,7 @@ import {
schemaString, schemaString,
subscriptionFields, subscriptionFields,
} from "~/helpers/graphql/connection" } from "~/helpers/graphql/connection"
import { platform } from "~/platform"
type NavigationTabs = "history" | "collection" | "docs" | "schema" type NavigationTabs = "history" | "collection" | "docs" | "schema"
type GqlTabs = "queries" | "mutations" | "subscriptions" | "types" type GqlTabs = "queries" | "mutations" | "subscriptions" | "types"
@@ -372,21 +373,33 @@ useCodemirror(
}) })
) )
const downloadSchema = () => { const downloadSchema = async () => {
const dataToWrite = JSON.stringify(schemaString.value, null, 2) const dataToWrite = schemaString.value
const file = new Blob([dataToWrite], { type: "application/graphql" }) const file = new Blob([dataToWrite], { type: "application/graphql" })
const a = document.createElement("a")
const url = URL.createObjectURL(file) const url = URL.createObjectURL(file)
a.href = url
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.graphql` const filename = `${
document.body.appendChild(a) url.split("/").pop()!.split("#")[0].split("?")[0]
a.click() }.graphql`
URL.revokeObjectURL(url)
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: "application/graphql",
suggestedFilename: filename,
filters: [
{
name: "GraphQL Schema File",
extensions: ["graphql"],
},
],
})
if (result.type === "unknown" || result.type === "saved") {
downloadSchemaIcon.value = IconCheck downloadSchemaIcon.value = IconCheck
toast.success(`${t("state.download_started")}`) toast.success(`${t("state.download_started")}`)
setTimeout(() => { }
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
const copySchema = () => { const copySchema = () => {

View File

@@ -92,12 +92,13 @@ import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit" import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { HoppGQLTab } from "~/helpers/graphql/tab" import { HoppTab } from "~/services/tab"
import { HoppGQLDocument } from "~/helpers/graphql/document"
const t = useI18n() const t = useI18n()
defineProps<{ defineProps<{
tab: HoppGQLTab tab: HoppTab<HoppGQLDocument>
isRemovable: boolean isRemovable: boolean
}>() }>()

View File

@@ -7,38 +7,31 @@
</span> </span>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from "vue" import { GraphQLScalarType, GraphQLType } from "graphql"
import { GraphQLScalarType } from "graphql" import { computed } from "vue"
export default defineComponent({ const props = defineProps<{
props: { gqlType: GraphQLType
// eslint-disable-next-line vue/require-default-prop }>()
gqlType: null,
// (typeName: string) => void
// eslint-disable-next-line vue/require-default-prop
jumpTypeCallback: Function,
},
computed: { const emit = defineEmits<{
typeString() { (e: "jump-to-type", type: GraphQLType): void
return `${this.gqlType}` }>()
},
isScalar() {
return this.resolveRootType(this.gqlType) instanceof GraphQLScalarType
},
},
methods: { const typeString = computed(() => `${props.gqlType}`)
jumpToType() { const isScalar = computed(() => {
if (this.isScalar) return return resolveRootType(props.gqlType) instanceof GraphQLScalarType
this.jumpTypeCallback(this.gqlType) })
},
resolveRootType(type) { function resolveRootType(type: GraphQLType) {
let t = type let t = type as any
while (t.ofType != null) t = t.ofType while (t.ofType != null) t = t.ofType
return t return t
}, }
},
}) function jumpToType() {
if (isScalar.value) return
emit("jump-to-type", props.gqlType)
}
</script> </script>

View File

@@ -67,9 +67,11 @@ import IconMaximize2 from "~icons/lucide/maximize-2"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { makeGQLRequest } from "@hoppscotch/data" import { makeGQLRequest } from "@hoppscotch/data"
import { createNewTab } from "~/helpers/graphql/tab" import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n() const t = useI18n()
const tabs = useService(GQLTabService)
const props = defineProps<{ const props = defineProps<{
entry: GQLHistoryEntry entry: GQLHistoryEntry
@@ -93,7 +95,7 @@ const query = computed(() =>
) )
const useEntry = () => { const useEntry = () => {
createNewTab({ tabs.createNewTab({
request: makeGQLRequest({ request: makeGQLRequest({
name: props.entry.request.name, name: props.entry.request.name,
url: props.entry.request.url, url: props.entry.request.url,

View File

@@ -9,7 +9,7 @@
v-model="filterText" v-model="filterText"
type="search" type="search"
autocomplete="off" autocomplete="off"
class="flex flex-1 p-4 py-2 bg-transparent" class="flex w-full p-4 py-2 bg-transparent h-8"
:placeholder="`${t('action.search')}`" :placeholder="`${t('action.search')}`"
/> />
<div class="flex"> <div class="flex">
@@ -176,8 +176,9 @@ import {
import HistoryRestCard from "./rest/Card.vue" import HistoryRestCard from "./rest/Card.vue"
import HistoryGraphqlCard from "./graphql/Card.vue" import HistoryGraphqlCard from "./graphql/Card.vue"
import { createNewTab } from "~/helpers/rest/tab"
import { defineActionHandler, invokeAction } from "~/helpers/actions" import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
@@ -293,8 +294,9 @@ const clearHistory = () => {
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry // NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
// (That is not a really good behaviour tho ¯\_(ツ)_/¯) // (That is not a really good behaviour tho ¯\_(ツ)_/¯)
const tabs = useService(RESTTabService)
const useHistory = (entry: RESTHistoryEntry) => { const useHistory = (entry: RESTHistoryEntry) => {
createNewTab({ tabs.createNewTab({
request: entry.request, request: entry.request,
isDirty: false, isDirty: false,
}) })

View File

@@ -59,7 +59,9 @@
:key="`contentTypeItem-${contentTypeIndex}`" :key="`contentTypeItem-${contentTypeIndex}`"
:label="contentTypeItem" :label="contentTypeItem"
:info-icon=" :info-icon="
contentTypeItem === body.contentType ? IconDone : null contentTypeItem === body.contentType
? IconDone
: undefined
" "
:active-info-icon="contentTypeItem === body.contentType" :active-info-icon="contentTypeItem === body.contentType"
@click=" @click="
@@ -136,7 +138,7 @@ import IconDone from "~icons/lucide/check"
import IconExternalLink from "~icons/lucide/external-link" import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info" import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw" import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RequestOptionTabs } from "./RequestOptions.vue" import { RESTOptionTabs } from "./RequestOptions.vue"
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
@@ -147,7 +149,7 @@ const props = defineProps<{
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void (e: "change-tab", value: RESTOptionTabs): void
(e: "update:headers", value: HoppRESTHeader[]): void (e: "update:headers", value: HoppRESTHeader[]): void
(e: "update:body", value: HoppRESTReqBody): void (e: "update:body", value: HoppRESTReqBody): void
}>() }>()
@@ -164,7 +166,7 @@ const overridenContentType = computed(() =>
) )
) )
const contentTypeOverride = (tab: RequestOptionTabs) => { const contentTypeOverride = (tab: RESTOptionTabs) => {
emit("change-tab", tab) emit("change-tab", tab)
if (!isContentTypeAlreadyExist()) { if (!isContentTypeAlreadyExist()) {
// TODO: Fix this // TODO: Fix this

View File

@@ -157,9 +157,10 @@ import {
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconWrapText from "~icons/lucide/wrap-text" import IconWrapText from "~icons/lucide/wrap-text"
import { currentActiveTab } from "~/helpers/rest/tab"
import cloneDeep from "lodash-es/cloneDeep" import cloneDeep from "lodash-es/cloneDeep"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
@@ -173,7 +174,8 @@ const emit = defineEmits<{
const toast = useToast() const toast = useToast()
const request = ref(cloneDeep(currentActiveTab.value.document.request)) const tabs = useService(RESTTabService)
const request = ref(cloneDeep(tabs.currentActiveTab.value.document.request))
const codegenType = ref<CodegenName>("shell-curl") const codegenType = ref<CodegenName>("shell-curl")
const errorState = ref(false) const errorState = ref(false)
@@ -242,7 +244,7 @@ watch(
() => props.show, () => props.show,
(goingToShow) => { (goingToShow) => {
if (goingToShow) { if (goingToShow) {
request.value = cloneDeep(currentActiveTab.value.document.request) request.value = cloneDeep(tabs.currentActiveTab.value.document.request)
platform.analytics?.logEvent({ platform.analytics?.logEvent({
type: "HOPP_REST_CODEGEN_OPENED", type: "HOPP_REST_CODEGEN_OPENED",

View File

@@ -185,18 +185,24 @@
<span> <span>
<HoppButtonSecondary <HoppButtonSecondary
v-if="header.source === 'auth'" v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking ? IconEye : IconEyeOff" :icon="masking ? IconEye : IconEyeOff"
@click="toggleMask()" @click="toggleMask()"
/> />
<HoppButtonSecondary <HoppButtonSecondary
v-else v-else
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight" :icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
class="cursor-auto text-primary hover:text-primary" class="cursor-auto text-primary hover:text-primary"
/> />
</span> </span>
<span> <span>
<HoppButtonSecondary <HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconArrowUpRight" :icon="IconArrowUpRight"
:title="t('request.go_to_authorization_tab')"
@click="changeTab(header.source)" @click="changeTab(header.source)"
/> />
</span> </span>
@@ -250,7 +256,7 @@ import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option" import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array" import * as A from "fp-ts/Array"
import draggable from "vuedraggable-es" import draggable from "vuedraggable-es"
import { RequestOptionTabs } from "./RequestOptions.vue" import { RESTOptionTabs } from "./RequestOptions.vue"
import { useCodemirror } from "@composables/codemirror" import { useCodemirror } from "@composables/codemirror"
import { commonHeaders } from "~/helpers/headers" import { commonHeaders } from "~/helpers/headers"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
@@ -267,10 +273,13 @@ import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab" import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const colorMode = useColorMode() const colorMode = useColorMode()
const idTicker = ref(0) const idTicker = ref(0)
@@ -286,7 +295,7 @@ const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const props = defineProps<{ modelValue: HoppRESTRequest }>() const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "change-tab", value: RequestOptionTabs): void (e: "change-tab", value: RESTOptionTabs): void
(e: "update:modelValue", value: HoppRESTRequest): void (e: "update:modelValue", value: HoppRESTRequest): void
}>() }>()
@@ -509,13 +518,13 @@ const changeTab = (tab: ComputedHeader["source"]) => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const headerKeyResults = inspectionService.getResultViewFor( const headerKeyResults = inspectionService.getResultViewFor(
currentTabID.value, tabs.currentTabID.value,
(result) => (result) =>
result.locations.type === "header" && result.locations.position === "key" result.locations.type === "header" && result.locations.position === "key"
) )
const headerValueResults = inspectionService.getResultViewFor( const headerValueResults = inspectionService.getResultViewFor(
currentTabID.value, tabs.currentTabID.value,
(result) => (result) =>
result.locations.type === "header" && result.locations.position === "value" result.locations.type === "header" && result.locations.position === "value"
) )

View File

@@ -93,13 +93,16 @@ import IconWrapText from "~icons/lucide/wrap-text"
import IconClipboard from "~icons/lucide/clipboard" import IconClipboard from "~icons/lucide/clipboard"
import IconCheck from "~icons/lucide/check" import IconCheck from "~icons/lucide/check"
import IconTrash2 from "~icons/lucide/trash-2" import IconTrash2 from "~icons/lucide/trash-2"
import { currentActiveTab } from "~/helpers/rest/tab"
import { platform } from "~/platform" import { platform } from "~/platform"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const curl = ref("") const curl = ref("")
const curlEditor = ref<any | null>(null) const curlEditor = ref<any | null>(null)
@@ -149,7 +152,7 @@ const handleImport = () => {
type: "HOPP_REST_IMPORT_CURL", type: "HOPP_REST_IMPORT_CURL",
}) })
currentActiveTab.value.document.request = req tabs.currentActiveTab.value.document.request = req
} catch (e) { } catch (e) {
console.error(e) console.error(e)
toast.error(`${t("error.curl_invalid_format")}`) toast.error(`${t("error.curl_invalid_format")}`)

View File

@@ -43,6 +43,7 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast" import { useToast } from "@composables/toast"
import { tokenRequest } from "~/helpers/oauth" import { tokenRequest } from "~/helpers/oauth"
import { getCombinedEnvVariables } from "~/helpers/preRequest" import { getCombinedEnvVariables } from "~/helpers/preRequest"
import * as E from "fp-ts/Either"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@@ -98,7 +99,11 @@ const handleAccessTokenRequest = async () => {
clientSecret: parseTemplateString(clientSecret.value, envVars), clientSecret: parseTemplateString(clientSecret.value, envVars),
scope: parseTemplateString(scope.value, envVars), scope: parseTemplateString(scope.value, envVars),
} }
await tokenRequest(tokenReqParams) const res = await tokenRequest(tokenReqParams)
if (res && E.isLeft(res)) {
toast.error(res.left)
}
} catch (e) { } catch (e) {
toast.error(`${e}`) toast.error(`${e}`)
} }

View File

@@ -202,12 +202,13 @@ import { objRemoveKey } from "@functional/object"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection" import { InspectionService, InspectorResult } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab" import { RESTTabService } from "~/services/tab/rest"
const colorMode = useColorMode() const colorMode = useColorMode()
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
const tabs = useService(RESTTabService)
const idTicker = ref(0) const idTicker = ref(0)
@@ -410,13 +411,13 @@ const clearContent = () => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const parameterKeyResults = inspectionService.getResultViewFor( const parameterKeyResults = inspectionService.getResultViewFor(
currentTabID.value, tabs.currentTabID.value,
(result) => (result) =>
result.locations.type === "parameter" && result.locations.position === "key" result.locations.type === "parameter" && result.locations.position === "key"
) )
const parameterValueResults = inspectionService.getResultViewFor( const parameterValueResults = inspectionService.getResultViewFor(
currentTabID.value, tabs.currentTabID.value,
(result) => (result) =>
result.locations.type === "parameter" && result.locations.type === "parameter" &&
result.locations.position === "value" result.locations.position === "value"

View File

@@ -217,6 +217,7 @@
@hide-modal="showCurlImportModal = false" @hide-modal="showCurlImportModal = false"
/> />
<HttpCodegenModal <HttpCodegenModal
v-if="showCodegenModal"
:show="showCodegenModal" :show="showCodegenModal"
@hide-modal="showCodegenModal = false" @hide-modal="showCodegenModal = false"
/> />
@@ -257,7 +258,6 @@ import IconLink2 from "~icons/lucide/link-2"
import IconRotateCCW from "~icons/lucide/rotate-ccw" import IconRotateCCW from "~icons/lucide/rotate-ccw"
import IconSave from "~icons/lucide/save" import IconSave from "~icons/lucide/save"
import IconShare2 from "~icons/lucide/share-2" import IconShare2 from "~icons/lucide/share-2"
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
import { getDefaultRESTRequest } from "~/helpers/rest/default" import { getDefaultRESTRequest } from "~/helpers/rest/default"
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history" import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
import { platform } from "~/platform" import { platform } from "~/platform"
@@ -265,6 +265,9 @@ import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { InterceptorService } from "~/services/interceptor.service" import { InterceptorService } from "~/services/interceptor.service"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const interceptorService = useService(InterceptorService) const interceptorService = useService(InterceptorService)
@@ -286,7 +289,7 @@ const toast = useToast()
const { subscribeToStream } = useStreamSubscriber() const { subscribeToStream } = useStreamSubscriber()
const props = defineProps<{ modelValue: HoppRESTTab }>() const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
const emit = defineEmits(["update:modelValue"]) const emit = defineEmits(["update:modelValue"])
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)
@@ -347,7 +350,6 @@ const newSendRequest = async () => {
const streamResult = await streamPromise const streamResult = await streamPromise
requestCancelFunc.value = cancel requestCancelFunc.value = cancel
if (E.isRight(streamResult)) { if (E.isRight(streamResult)) {
subscribeToStream( subscribeToStream(
streamResult.right, streamResult.right,
@@ -362,6 +364,20 @@ const newSendRequest = async () => {
loading.value = false loading.value = false
}, },
() => { () => {
// TODO: Change this any to a proper type
const result = (streamResult.right as any).value
if (
result.type === "network_fail" &&
result.error?.error === "NO_PW_EXT_HOOK"
) {
const errorResponse: HoppRESTResponse = {
type: "extension_error",
error: result.error.humanMessage.heading,
component: result.error.component,
req: result.req,
}
updateRESTResponse(errorResponse)
}
loading.value = false loading.value = false
} }
) )
@@ -426,7 +442,7 @@ const updateMethod = (method: string) => {
const onSelectMethod = (e: Event | any) => { const onSelectMethod = (e: Event | any) => {
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though. // type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
updateMethod(e.value) updateMethod(e.target.value)
} }
const clearContent = () => { const clearContent = () => {
@@ -434,7 +450,7 @@ const clearContent = () => {
} }
const updateRESTResponse = (response: HoppRESTResponse | null) => { const updateRESTResponse = (response: HoppRESTResponse | null) => {
tab.value.response = response tab.value.document.response = response
} }
const copyLinkIcon = refAutoReset< const copyLinkIcon = refAutoReset<
@@ -642,5 +658,6 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor(currentTabID.value) const tabs = useService(RESTTabService)
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
</script> </script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<HoppSmartTabs <HoppSmartTabs
v-model="selectedOptionsTab" v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10" styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
render-inactive-tabs render-inactive-tabs
> >
@@ -15,7 +15,7 @@
<HttpBody <HttpBody
v-model:headers="request.headers" v-model:headers="request.headers"
v-model:body="request.body" v-model:body="request.body"
@change-tab="changeTab" @change-tab="changeOptionTab"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -23,7 +23,7 @@
:label="`${t('tab.headers')}`" :label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`" :info="`${newActiveHeadersCount$}`"
> >
<HttpHeaders v-model="request" @change-tab="changeTab" /> <HttpHeaders v-model="request" @change-tab="changeOptionTab" />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`"> <HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<HttpAuthorization v-model="request.auth" /> <HttpAuthorization v-model="request.auth" />
@@ -55,31 +55,43 @@
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { HoppRESTRequest } from "@hoppscotch/data" import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { computed, ref } from "vue" import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions" import { defineActionHandler } from "~/helpers/actions"
export type RequestOptionTabs = const VALID_OPTION_TABS = [
| "params" "params",
| "bodyParams" "bodyParams",
| "headers" "headers",
| "authorization" "authorization",
| "preRequestScript" "preRequestScript",
| "tests" "tests",
] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
const t = useI18n() const t = useI18n()
// v-model integration with props and emit // v-model integration with props and emit
const props = defineProps<{ modelValue: HoppRESTRequest }>() const props = withDefaults(
defineProps<{
modelValue: HoppRESTRequest
optionTab: RESTOptionTabs
}>(),
{
optionTab: "params",
}
)
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTRequest): void (e: "update:modelValue", value: HoppRESTRequest): void
(e: "update:optionTab", value: RESTOptionTabs): void
}>() }>()
const request = useVModel(props, "modelValue", emit) const request = useVModel(props, "modelValue", emit)
const selectedOptionTab = useVModel(props, "optionTab", emit)
const selectedOptionsTab = ref<RequestOptionTabs>("params") const changeOptionTab = (e: RESTOptionTabs) => {
selectedOptionTab.value = e
const changeTab = (e: RequestOptionTabs) => {
selectedOptionsTab.value = e
} }
const newActiveParamsCount$ = computed(() => { const newActiveParamsCount$ = computed(() => {
@@ -101,6 +113,6 @@ const newActiveHeadersCount$ = computed(() => {
}) })
defineActionHandler("request.open-tab", ({ tab }) => { defineActionHandler("request.open-tab", ({ tab }) => {
selectedOptionsTab.value = tab as RequestOptionTabs selectedOptionTab.value = tab as RESTOptionTabs
}) })
</script> </script>

View File

@@ -2,10 +2,13 @@
<AppPaneLayout layout-id="rest-primary"> <AppPaneLayout layout-id="rest-primary">
<template #primary> <template #primary>
<HttpRequest v-model="tab" /> <HttpRequest v-model="tab" />
<HttpRequestOptions v-model="tab.document.request" /> <HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="tab.document.optionTabPreference"
/>
</template> </template>
<template #secondary> <template #secondary>
<HttpResponse v-model:tab="tab" /> <HttpResponse v-model:document="tab.document" />
</template> </template>
</AppPaneLayout> </AppPaneLayout>
</template> </template>
@@ -13,16 +16,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { watch } from "vue" import { watch } from "vue"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
import { isEqualHoppRESTRequest } from "@hoppscotch/data" import { isEqualHoppRESTRequest } from "@hoppscotch/data"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
// TODO: Move Response and Request execution code to over here // TODO: Move Response and Request execution code to over here
const props = defineProps<{ modelValue: HoppRESTTab }>() const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:modelValue", val: HoppRESTTab): void (e: "update:modelValue", val: HoppTab<HoppRESTDocument>): void
}>() }>()
const tab = useVModel(props, "modelValue", emit) const tab = useVModel(props, "modelValue", emit)

View File

@@ -1,36 +1,33 @@
<template> <template>
<div class="flex flex-col flex-1 relative"> <div class="flex flex-col flex-1 relative">
<HttpResponseMeta :response="tab.response" /> <HttpResponseMeta :response="doc.response" />
<LensesResponseBodyRenderer <LensesResponseBodyRenderer
v-if="!loading && hasResponse" v-if="!loading && hasResponse"
v-model:selected-tab-preference="selectedTabPreference" v-model:document="doc"
v-model:tab="tab"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from "vue"
import { HoppRESTTab } from "~/helpers/rest/tab"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{ const props = defineProps<{
tab: HoppRESTTab document: HoppRESTDocument
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:tab", val: HoppRESTTab): void (e: "update:tab", val: HoppRESTDocument): void
}>() }>()
const tab = useVModel(props, "tab", emit) const doc = useVModel(props, "document", emit)
const selectedTabPreference = ref<string | null>(null)
const hasResponse = computed( const hasResponse = computed(
() => () =>
tab.value.response?.type === "success" || doc.value.response?.type === "success" ||
tab.value.response?.type === "fail" doc.value.response?.type === "fail"
) )
const loading = computed(() => tab.value.response?.type === "loading") const loading = computed(() => doc.value.response?.type === "loading")
</script> </script>

View File

@@ -11,6 +11,12 @@
<HoppSmartSpinner class="my-4" /> <HoppSmartSpinner class="my-4" />
<span class="text-secondaryLight">{{ t("state.loading") }}</span> <span class="text-secondaryLight">{{ t("state.loading") }}</span>
</div> </div>
<component
:is="response.component"
v-if="response.type === 'extension_error'"
class="flex-1"
/>
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="response.type === 'network_fail'" v-if="response.type === 'network_fail'"
:src="`/images/states/${colorMode.value}/youre_lost.svg`" :src="`/images/states/${colorMode.value}/youre_lost.svg`"
@@ -93,10 +99,11 @@ import { useColorMode } from "@composables/theming"
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes" import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue" import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection" import { InspectionService } from "~/services/inspection"
import { currentTabID } from "~/helpers/rest/tab" import { RESTTabService } from "~/services/tab/rest"
const t = useI18n() const t = useI18n()
const colorMode = useColorMode() const colorMode = useColorMode()
const tabs = useService(RESTTabService)
const props = defineProps<{ const props = defineProps<{
response: HoppRESTResponse | null | undefined response: HoppRESTResponse | null | undefined
@@ -146,7 +153,7 @@ const statusCategory = computed(() => {
const inspectionService = useService(InspectionService) const inspectionService = useService(InspectionService)
const tabResults = inspectionService.getResultViewFor( const tabResults = inspectionService.getResultViewFor(
currentTabID.value, tabs.currentTabID.value,
(result) => result.locations.type === "response" (result) => result.locations.type === "response"
) )
</script> </script>

View File

@@ -96,16 +96,17 @@ import { ref } from "vue"
import { TippyComponent } from "vue-tippy" import { TippyComponent } from "vue-tippy"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring" import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { HoppRESTTab } from "~/helpers/rest/tab"
import IconXCircle from "~icons/lucide/x-circle" import IconXCircle from "~icons/lucide/x-circle"
import IconXSquare from "~icons/lucide/x-square" import IconXSquare from "~icons/lucide/x-square"
import IconFileEdit from "~icons/lucide/file-edit" import IconFileEdit from "~icons/lucide/file-edit"
import IconCopy from "~icons/lucide/copy" import IconCopy from "~icons/lucide/copy"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
const t = useI18n() const t = useI18n()
defineProps<{ defineProps<{
tab: HoppRESTTab tab: HoppTab<HoppRESTDocument>
isRemovable: boolean isRemovable: boolean
}>() }>()

View File

@@ -0,0 +1,98 @@
<template>
<HoppSmartPlaceholder
:src="`/images/states/${colorMode.value}/youre_lost.svg`"
:alt="`${t('error.network_fail')}`"
:heading="t('error.network_fail')"
large
>
<div class="my-1 text-secondaryLight flex flex-col items-center">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
</div>
<div class="flex flex-col py-4 space-y-2">
<span>
<HoppSmartItem
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
blank
:icon="IconChrome"
label="Chrome"
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasChromeExtInstalled"
outline
/>
</span>
<span>
<HoppSmartItem
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
blank
:icon="IconFirefox"
label="Firefox"
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
:active-info-icon="hasFirefoxExtInstalled"
outline
/>
</span>
</div>
<div class="py-4 space-y-4">
<div class="flex items-center">
<HoppSmartToggle
:on="extensionEnabled"
@change="extensionEnabled = !extensionEnabled"
>
{{ t("settings.extensions_use_toggle") }}
</HoppSmartToggle>
</div>
</div>
</HoppSmartPlaceholder>
</template>
<script setup lang="ts">
import IconChrome from "~icons/brands/chrome"
import IconFirefox from "~icons/brands/firefox"
import IconCheckCircle from "~icons/lucide/check-circle"
import { useI18n } from "@composables/i18n"
import { ExtensionInterceptorService } from "~/platform/std/interceptors/extension"
import { useService } from "dioc/vue"
import { computed } from "vue"
import { InterceptorService } from "~/services/interceptor.service"
import { platform } from "~/platform"
import { useColorMode } from "~/composables/theming"
const colorMode = useColorMode()
const t = useI18n()
const interceptorService = useService(InterceptorService)
const extensionService = useService(ExtensionInterceptorService)
const hasChromeExtInstalled = extensionService.chromeExtensionInstalled
const hasFirefoxExtInstalled = extensionService.firefoxExtensionInstalled
const extensionEnabled = computed({
get() {
return (
interceptorService.currentInterceptorID.value ===
extensionService.interceptorID
)
},
set(active) {
if (active) {
interceptorService.currentInterceptorID.value =
extensionService.interceptorID
} else {
interceptorService.currentInterceptorID.value =
platform.interceptors.default
}
},
})
</script>

View File

@@ -1,6 +1,6 @@
<template> <template>
<HoppSmartTabs <HoppSmartTabs
v-if="tab.response" v-if="doc.response"
v-model="selectedLensTab" v-model="selectedLensTab"
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold" styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
> >
@@ -13,7 +13,7 @@
> >
<component <component
:is="lensRendererFor(lens.renderer)" :is="lensRendererFor(lens.renderer)"
:response="tab.response" :response="doc.response"
/> />
</HoppSmartTab> </HoppSmartTab>
<HoppSmartTab <HoppSmartTab
@@ -28,19 +28,10 @@
<HoppSmartTab <HoppSmartTab
id="results" id="results"
:label="t('test.results')" :label="t('test.results')"
:indicator=" :indicator="showIndicator"
tab.testResults &&
(tab.testResults.expectResults.length ||
tab.testResults.tests.length ||
tab.testResults.envDiff.selected.additions.length ||
tab.testResults.envDiff.selected.updations.length ||
tab.testResults.envDiff.global.updations.length)
? true
: false
"
class="flex flex-col flex-1" class="flex flex-col flex-1"
> >
<HttpTestResult v-model="tab.testResults" /> <HttpTestResult v-model="doc.testResults" />
</HoppSmartTab> </HoppSmartTab>
</HoppSmartTabs> </HoppSmartTabs>
</template> </template>
@@ -54,20 +45,30 @@ import {
} from "~/helpers/lenses/lenses" } from "~/helpers/lenses/lenses"
import { useI18n } from "@composables/i18n" import { useI18n } from "@composables/i18n"
import { useVModel } from "@vueuse/core" import { useVModel } from "@vueuse/core"
import { HoppRESTTab } from "~/helpers/rest/tab" import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{ const props = defineProps<{
tab: HoppRESTTab document: HoppRESTDocument
selectedTabPreference: string | null
}>() }>()
const emit = defineEmits<{ const emit = defineEmits<{
(e: "update:tab", val: HoppRESTTab): void (e: "update:document", document: HoppRESTDocument): void
(e: "update:selectedTabPreference", newTab: string): void
}>() }>()
const tab = useVModel(props, "tab", emit) const doc = useVModel(props, "document", emit)
const selectedTabPreference = useVModel(props, "selectedTabPreference", emit)
const showIndicator = computed(() => {
if (!doc.value.testResults) return false
const { expectResults, tests, envDiff } = doc.value.testResults
return Boolean(
expectResults.length ||
tests.length ||
envDiff.selected.additions.length ||
envDiff.selected.updations.length ||
envDiff.global.updations.length
)
})
const allLensRenderers = getLensRenderers() const allLensRenderers = getLensRenderers()
@@ -81,19 +82,19 @@ const selectedLensTab = ref("")
const maybeHeaders = computed(() => { const maybeHeaders = computed(() => {
if ( if (
!tab.value.response || !doc.value.response ||
!( !(
tab.value.response.type === "success" || doc.value.response.type === "success" ||
tab.value.response.type === "fail" doc.value.response.type === "fail"
) )
) )
return null return null
return tab.value.response.headers return doc.value.response.headers
}) })
const validLenses = computed(() => { const validLenses = computed(() => {
if (!tab.value.response) return [] if (!doc.value.response) return []
return getSuitableLenses(tab.value.response) return getSuitableLenses(doc.value.response)
}) })
watch( watch(
@@ -107,11 +108,13 @@ watch(
"results", "results",
] ]
const { responseTabPreference } = doc.value
if ( if (
selectedTabPreference.value && responseTabPreference &&
validRenderers.includes(selectedTabPreference.value) validRenderers.includes(responseTabPreference)
) { ) {
selectedLensTab.value = selectedTabPreference.value selectedLensTab.value = responseTabPreference
} else { } else {
selectedLensTab.value = newLenses[0].renderer selectedLensTab.value = newLenses[0].renderer
} }
@@ -120,6 +123,6 @@ watch(
) )
watch(selectedLensTab, (newLensID) => { watch(selectedLensTab, (newLensID) => {
selectedTabPreference.value = newLensID doc.value.responseTabPreference = newLensID
}) })
</script> </script>

View File

@@ -35,12 +35,12 @@
v-if=" v-if="
!teamDetails.loading && !teamDetails.loading &&
E.isRight(teamDetails.data) && E.isRight(teamDetails.data) &&
teamDetails.data.right.team.teamMembers teamDetails.data.right.team?.teamMembers
" "
class="border rounded border-divider" class="border rounded border-divider"
> >
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if="teamDetails.data.right.team.teamMembers === 0" v-if="teamDetails.data.right.team.teamMembers.length === 0"
:src="`/images/states/${colorMode.value}/add_group.svg`" :src="`/images/states/${colorMode.value}/add_group.svg`"
:alt="`${t('empty.members')}`" :alt="`${t('empty.members')}`"
:text="t('empty.members')" :text="t('empty.members')"

View File

@@ -88,7 +88,7 @@
> >
<div <div
v-for="(invitee, index) in pendingInvites.data.right.team v-for="(invitee, index) in pendingInvites.data.right.team
.teamInvitations" ?.teamInvitations"
:key="`invitee-${index}`" :key="`invitee-${index}`"
class="flex divide-x divide-dividerLight" class="flex divide-x divide-dividerLight"
> >
@@ -122,7 +122,7 @@
<HoppSmartPlaceholder <HoppSmartPlaceholder
v-if=" v-if="
E.isRight(pendingInvites.data) && E.isRight(pendingInvites.data) &&
pendingInvites.data.right.team.teamInvitations.length === 0 pendingInvites.data.right.team?.teamInvitations.length === 0
" "
:text="t('empty.pending_invites')" :text="t('empty.pending_invites')"
> >

View File

@@ -10,6 +10,7 @@ import {
WatchStopHandle, WatchStopHandle,
watchSyncEffect, watchSyncEffect,
watch, watch,
nextTick,
} from "vue" } from "vue"
import { import {
client, client,
@@ -101,7 +102,7 @@ export const useGQLQuery = <
const rerunQuery = () => { const rerunQuery = () => {
source.value = !isPaused.value source.value = !isPaused.value
? client.value.executeQuery<DocType, DocVarType>(request.value, { ? client.value?.executeQuery<DocType, DocVarType>(request.value, {
requestPolicy: "network-only", requestPolicy: "network-only",
}) })
: undefined : undefined
@@ -126,7 +127,7 @@ export const useGQLQuery = <
const invalidateStops = args.updateSubs!.map((sub) => { const invalidateStops = args.updateSubs!.map((sub) => {
return wonkaPipe( return wonkaPipe(
client.value.executeSubscription(sub), client.value!.executeSubscription(sub),
onEnd(() => { onEnd(() => {
if (source.value) execute() if (source.value) execute()
}), }),
@@ -191,10 +192,12 @@ export const useGQLQuery = <
} else { } else {
args.variables = updatedVars args.variables = updatedVars
} }
nextTick(rerunQuery)
} else {
rerunQuery()
} }
isPaused.value = false isPaused.value = false
rerunQuery()
} }
const pause = () => { const pause = () => {

View File

@@ -10,6 +10,7 @@ import { useI18n } from "./i18n"
import { refAutoReset } from "@vueuse/core" import { refAutoReset } from "@vueuse/core"
import { copyToClipboard } from "@helpers/utils/clipboard" import { copyToClipboard } from "@helpers/utils/clipboard"
import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse" import { HoppRESTResponse } from "@helpers/types/HoppRESTResponse"
import { platform } from "~/platform"
export function useCopyResponse(responseBodyText: Ref<any>) { export function useCopyResponse(responseBodyText: Ref<any>) {
const toast = useToast() const toast = useToast()
@@ -40,15 +41,14 @@ export function useDownloadResponse(
const toast = useToast() const toast = useToast()
const t = useI18n() const t = useI18n()
const downloadResponse = () => { const downloadResponse = async () => {
const dataToWrite = responseBody.value const dataToWrite = responseBody.value
const file = new Blob([dataToWrite], { type: contentType })
const a = document.createElement("a")
const url = URL.createObjectURL(file)
a.href = url
// TODO: get uri from meta // Guess extension and filename
a.download = pipe( const file = new Blob([dataToWrite], { type: contentType })
const url = URL.createObjectURL(file)
const filename = pipe(
url, url,
S.split("/"), S.split("/"),
RNEA.last, RNEA.last,
@@ -58,15 +58,24 @@ export function useDownloadResponse(
RNEA.head RNEA.head
) )
document.body.appendChild(a) URL.revokeObjectURL(url)
a.click()
console.log(filename)
// TODO: Look at the mime type and determine extension ?
const result = await platform.io.saveFileWithDialog({
data: dataToWrite,
contentType: contentType,
suggestedFilename: filename,
})
// Assume success if unknown as we cannot determine
if (result.type === "unknown" || result.type === "saved") {
downloadIcon.value = IconCheck downloadIcon.value = IconCheck
toast.success(`${t("state.download_started")}`) toast.success(`${t("state.download_started")}`)
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
}, 1000)
} }
}
return { return {
downloadIcon, downloadIcon,
downloadResponse, downloadResponse,

View File

@@ -152,12 +152,14 @@ export function useStreamSubscriber(): {
error?: (e: any) => void, error?: (e: any) => void,
complete?: () => void complete?: () => void
) => { ) => {
const sub = stream.subscribe({ let sub: Subscription | null = null
sub = stream.subscribe({
next, next,
error, error,
complete: () => { complete: () => {
if (complete) complete() if (complete) complete()
subs.splice(subs.indexOf(sub), 1) if (sub) subs.splice(subs.indexOf(sub), 1)
}, },
}) })

View File

@@ -29,8 +29,9 @@ import {
setGlobalEnvVariables, setGlobalEnvVariables,
updateEnvironment, updateEnvironment,
} from "~/newstore/environments" } from "~/newstore/environments"
import { HoppRESTTab } from "./rest/tab"
import { Ref } from "vue" import { Ref } from "vue"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "./rest/document"
const getTestableBody = ( const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" } res: HoppRESTResponse & { type: "success" | "fail" }
@@ -69,7 +70,7 @@ export const executedResponses$ = new Subject<
>() >()
export function runRESTRequest$( export function runRESTRequest$(
tab: Ref<HoppRESTTab> tab: Ref<HoppTab<HoppRESTDocument>>
): [ ): [
() => void, () => void,
Promise< Promise<
@@ -127,7 +128,7 @@ export function runRESTRequest$(
)() )()
if (E.isRight(runResult)) { if (E.isRight(runResult)) {
tab.value.testResults = translateToSandboxTestResults( tab.value.document.testResults = translateToSandboxTestResults(
runResult.right runResult.right
) )
@@ -163,7 +164,7 @@ export function runRESTRequest$(
)() )()
} }
} else { } else {
tab.value.testResults = { tab.value.document.testResults = {
description: "", description: "",
expectResults: [], expectResults: [],
tests: [], tests: [],

View File

@@ -6,7 +6,7 @@ import { Ref, onBeforeUnmount, onMounted, reactive, watch } from "vue"
import { BehaviorSubject } from "rxjs" import { BehaviorSubject } from "rxjs"
import { HoppRESTDocument } from "./rest/document" import { HoppRESTDocument } from "./rest/document"
import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data" import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
import { RequestOptionTabs } from "~/components/http/RequestOptions.vue" import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppGQLSaveContext } from "./graphql/document" import { HoppGQLSaveContext } from "./graphql/document"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue" import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { computed } from "vue" import { computed } from "vue"
@@ -113,7 +113,7 @@ type HoppActionArgsMap = {
request: HoppGQLRequest request: HoppGQLRequest
} }
"request.open-tab": { "request.open-tab": {
tab: RequestOptionTabs | GQLOptionTabs tab: RESTOptionTabs | GQLOptionTabs
} }
"tab.duplicate-tab": { "tab.duplicate-tab": {

View File

@@ -1,9 +1,10 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getTabsRefTo } from "../rest/tab"
import { getAffectedIndexes } from "./affectedIndex" import { getAffectedIndexes } from "./affectedIndex"
import { GetSingleRequestDocument } from "../backend/graphql" import { GetSingleRequestDocument } from "../backend/graphql"
import { runGQLQuery } from "../backend/GQLClient" import { runGQLQuery } from "../backend/GQLClient"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
/** /**
* Resolve save context on reorder * Resolve save context on reorder
@@ -56,7 +57,9 @@ export function resolveSaveContextOnCollectionReorder(
} }
} }
const tabs = getTabsRefTo((tab) => { const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return ( return (
tab.document.saveContext?.originLocation === "user-collection" && tab.document.saveContext?.originLocation === "user-collection" &&
affectedPaths.has(tab.document.saveContext.folderPath) affectedPaths.has(tab.document.saveContext.folderPath)
@@ -84,7 +87,8 @@ export function updateSaveContextForAffectedRequests(
oldFolderPath: string, oldFolderPath: string,
newFolderPath: string newFolderPath: string
) { ) {
const tabs = getTabsRefTo((tab) => { const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return ( return (
tab.document.saveContext?.originLocation === "user-collection" && tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(oldFolderPath) tab.document.saveContext.folderPath.startsWith(oldFolderPath)
@@ -105,7 +109,8 @@ export function updateSaveContextForAffectedRequests(
} }
function resetSaveContextForAffectedRequests(folderPath: string) { function resetSaveContextForAffectedRequests(folderPath: string) {
const tabs = getTabsRefTo((tab) => { const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return ( return (
tab.document.saveContext?.originLocation === "user-collection" && tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(folderPath) tab.document.saveContext.folderPath.startsWith(folderPath)
@@ -124,7 +129,8 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
*/ */
export async function resetTeamRequestsContext() { export async function resetTeamRequestsContext() {
const tabs = getTabsRefTo((tab) => { const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
return tab.document.saveContext?.originLocation === "team-collection" return tab.document.saveContext?.originLocation === "team-collection"
}) })

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