Compare commits

..

48 Commits

Author SHA1 Message Date
jamesgeorge007
787aab650f fix: redirect to the users list view on successful deletion from the profile view
SH Admin dashboard
2024-03-28 21:20:48 +05:30
Anwarul Islam
1f7a8edb14 fix: lint errors removed by using satisfies or as for type (#3934)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
2024-03-28 20:28:48 +05:30
James George
81f1e05a6c chore(sh-admin): alert the user while deleting users who are team owners (#3937)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
Co-authored-by: nivedin <nivedinp@gmail.com>
2024-03-28 20:17:24 +05:30
James George
0a71783eaa fix(common): ensure requests are translated to the latest version during import and search actions (#3931) 2024-03-25 17:09:54 +05:30
Nivedin
c326f54f7e feat: added mutation and function to platform for updating user profile name (#3929)
Co-authored-by: amk-dev <akash.k.mohan98@gmail.com>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-25 14:41:25 +05:30
Dmitry
1113c79e20 fix: can't import curl command with data-urlencode (#3905)
Co-authored-by: Anwarul Islam <anwaarulislaam@gmail.com>
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-25 13:12:48 +05:30
Akash K
6fd30f9aca chore: spotlight improvements for team requests search (#3930)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-25 12:41:18 +05:30
Anwarul Islam
2c5b0dcd1b feat: focus codemirror view when ImportCurl component launched (#3911)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-22 18:21:16 +05:30
Anwarul Islam
6f4455ba03 fix: request failing on change content type to multipart-formdata (#3922)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-22 18:11:16 +05:30
Nivedin
ba8c4480d9 fix: workspace list section bugs (#3925)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-22 18:02:28 +05:30
Joel Jacob Stephen
380397cc55 refactor(sh-admin): improvements to pending invites page in dashboard (#3926) 2024-03-22 17:54:40 +05:30
Akash K
d19807b212 chore: split axios request options into platform (#3927)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-22 13:42:41 +05:30
Balu Babu
c89c2a5f5c feat: added new mutation to update username in hopp app (#3924)
* feat: added new mutation to update username in hopp app

* feat: display name length validation added

---------

Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
2024-03-22 12:07:38 +05:30
Anwarul Islam
256553b9bb chore: update copy for header inspection (#3907)
Co-authored-by: James George <jamesgeorge998001@gmail.com>
2024-03-21 22:15:23 +05:30
Akash K
89d9951f3b fix: fix typo in team search url (#3923)
fix: fix typo in team search endpoint url

Co-authored-by: James George <jamesgeorge998001@gmail.com>
2024-03-21 16:44:58 +05:30
Balu Babu
dd65ad3103 chore: added input validation to search query (#3921) 2024-03-21 16:13:11 +05:30
Balu Babu
018ed3db26 refactor: AIO healthcheck bash script (#3920)
* chore: added logic to make script with with subpath

* chore: removed variable from failed echo message
2024-03-21 16:12:13 +05:30
Andrew Bastin
a9cd6c0c01 chore: update internal deployment docker compose config 2024-03-21 02:38:55 +05:30
James George
e53382666a fix(common): prevent exception with ShortcodeListAdapter initialization (#3917) 2024-03-20 20:29:04 +05:30
James George
7621ff2961 feat: add extended support for versioned entities in the CLI (#3912) 2024-03-20 20:13:22 +05:30
Akash K
fc20b76080 fix: direct import from url failing (#3918) 2024-03-20 20:06:51 +05:30
Nivedin
146c73d7b6 feat: github enterprise SSO provider addition (#3914) 2024-03-20 20:01:56 +05:30
Akash K
6b58915caa feat: oauth revamp + support for multiple grant types in oauth (#3885)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-20 00:18:03 +05:30
Akash K
457857a711 feat: team search in workspace search and spotlight (#3896)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-19 18:50:35 +05:30
Balu Babu
a3f3e3e62d refactor: collection search query (#3908) 2024-03-19 17:12:35 +05:30
Andrew Bastin
66f20d10e1 chore: enable subpath based access in test deploy docker compose 2024-03-19 16:26:04 +05:30
Andrew Bastin
32e9366609 chore: update test deploy docker compose aio port 2024-03-19 15:53:08 +05:30
Andrew Bastin
e41e956273 chore: add test deployment docker compose file 2024-03-19 14:39:57 +05:30
Nivedin
a14870f3f0 fix: collection auth headers active tab update bug and type fix (#3899) 2024-03-15 21:17:34 +05:30
Andrew Bastin
0e96665254 refactor: use trigram search index instead of full text search (#3900)
Co-authored-by: Balu Babu <balub997@gmail.com>
2024-03-15 20:10:12 +05:30
kaifulee
efdc1c2f5d chore: fix some typos (#3895)
Signed-off-by: kaifulee <cuishuang@outlook.com>
2024-03-15 20:06:34 +05:30
Andrew Bastin
c5334d4c06 chore(sh-admin): bump @hoppscotch/ui version to 0.1.3 2024-03-15 12:43:05 +05:30
Balu Babu
4f549974ed fix: reset infra-config bug (#3898) 2024-03-14 21:46:34 +05:30
Nivedin
41d617b507 fix: secret env bug in firebase due to undefined value (#3881)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 17:11:51 +05:30
Joel Jacob Stephen
be7387ed19 refactor(sh-admin): updated data sharing doc links + remove disabled property from all inputs in configurations (#3894) 2024-03-13 16:18:27 +05:30
Joel Jacob Stephen
acfb0189df feat(sh-admin): enhanced user management in admin dashboard (#3814)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 14:45:13 +05:30
Nivedin
8fdba760a2 refactor: personal workspace nomenclature update (#3893)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-13 14:21:23 +05:30
Nivedin
bf98009abb fix: request variable version syncing bug (#3889)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-12 11:42:05 +05:30
James George
dce396c164 chore: bump codemirror dependencies (#3888) 2024-03-11 14:21:39 +05:30
Nivedin
07e8af7947 refactor: update team nomenclature (#3880)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-08 23:54:32 +05:30
Joel Jacob Stephen
e69d5a6253 feat(sh-admin): introducing additional SSO related server configurations to dashboard (#3737)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-08 15:18:53 +05:30
Akash K
6d66d12a9e feat: common changes for site protection (#3878) 2024-03-07 23:43:20 +05:30
James George
439cd82c88 chore: pin dependencies across packages (#3876) 2024-03-07 23:37:48 +05:30
Akash K
6dbaf524ce feat: use tags as folders when importing from openapi (#3846) 2024-03-07 19:55:46 +05:30
Andrew Bastin
68e439d1a4 chore: bump version to 2024.3.0 2024-03-07 19:22:46 +05:30
Nivedin
8deba7a28e fix: context menu bug and incorrect position (#3874) 2024-03-07 17:59:06 +05:30
Nivedin
7ec8659381 feat: request variables (#3825)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2024-03-07 12:50:44 +05:30
Mir Arif Hasan
3611cac241 feat(backend): sso callback url and scope added in infra-config (#3718) 2024-03-07 12:07:51 +05:30
185 changed files with 9933 additions and 7333 deletions

1
.npmrc
View File

@@ -1 +1,2 @@
shamefully-hoist=false
save-prefix=''

48
docker-compose.deploy.yml Normal file
View File

@@ -0,0 +1,48 @@
# THIS IS NOT TO BE USED FOR PERSONAL DEPLOYMENTS!
# Internal Docker Compose Image used for internal testing deployments
version: "3.7"
services:
hoppscotch-db:
image: postgres:15
user: postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpass
POSTGRES_DB: hoppscotch
healthcheck:
test:
[
"CMD-SHELL",
"sh -c 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'"
]
interval: 5s
timeout: 5s
retries: 10
hoppscotch-aio:
container_name: hoppscotch-aio
build:
dockerfile: prod.Dockerfile
context: .
target: aio
environment:
- DATABASE_URL=postgresql://postgres:testpass@hoppscotch-db:5432/hoppscotch
- ENABLE_SUBPATH_BASED_ACCESS=true
env_file:
- ./.env
depends_on:
hoppscotch-db:
condition: service_healthy
command: ["sh", "-c", "pnpm exec prisma migrate deploy && node /usr/src/app/aio_run.mjs"]
healthcheck:
test:
- CMD
- curl
- '-f'
- 'http://localhost:80'
interval: 2s
timeout: 10s
retries: 30

View File

@@ -112,7 +112,7 @@ services:
build:
dockerfile: packages/hoppscotch-backend/Dockerfile
context: .
target: dev
target: prod
env_file:
- ./.env
restart: always
@@ -122,7 +122,7 @@ services:
- PORT=3000
volumes:
# Uncomment the line below when modifying code. Only applicable when using the "dev" target.
- ./packages/hoppscotch-backend/:/usr/src/app
# - ./packages/hoppscotch-backend/:/usr/src/app
- /usr/src/app/node_modules/
depends_on:
hoppscotch-db:

View File

@@ -9,6 +9,10 @@ curlCheck() {
fi
}
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"
if [ "$ENABLE_SUBPATH_BASED_ACCESS" = "true" ]; then
curlCheck "http://localhost:80/backend/ping"
else
curlCheck "http://localhost:3000"
curlCheck "http://localhost:3100"
curlCheck "http://localhost:3170/ping"
fi

View File

@@ -23,13 +23,13 @@
"./packages/*"
],
"devDependencies": {
"@commitlint/cli": "^16.2.3",
"@commitlint/config-conventional": "^16.2.1",
"@hoppscotch/ui": "^0.1.0",
"@commitlint/cli": "16.3.0",
"@commitlint/config-conventional": "16.2.4",
"@hoppscotch/ui": "0.1.0",
"@types/node": "17.0.27",
"cross-env": "^7.0.3",
"http-server": "^14.1.1",
"husky": "^7.0.4",
"cross-env": "7.0.3",
"http-server": "14.1.1",
"husky": "7.0.4",
"lint-staged": "12.4.0"
},
"pnpm": {
@@ -37,7 +37,7 @@
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"httpsnippet@3.0.1": {
"peerDependencies": {
"ajv": "6.12.3"
}

View File

@@ -17,16 +17,16 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "6.9.3",
"@codemirror/language": "6.10.1",
"@lezer/highlight": "1.2.0",
"@lezer/lr": "^1.3.14"
"@lezer/lr": "1.3.14"
},
"devDependencies": {
"@lezer/generator": "^1.5.1",
"mocha": "^9.2.2",
"rollup": "^3.29.3",
"rollup-plugin-dts": "^6.0.2",
"rollup-plugin-ts": "^3.4.5",
"typescript": "^5.2.2"
"@lezer/generator": "1.5.1",
"mocha": "9.2.2",
"rollup": "3.29.4",
"rollup-plugin-dts": "6.0.2",
"rollup-plugin-ts": "3.4.5",
"typescript": "5.2.2"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "hoppscotch-backend",
"version": "2023.12.6",
"version": "2024.3.0",
"description": "",
"author": "",
"private": true,
@@ -24,83 +24,83 @@
"do-test": "pnpm run test"
},
"dependencies": {
"@apollo/server": "^4.9.4",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/apollo": "^12.0.9",
"@nestjs/common": "^10.2.6",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.6",
"@nestjs/graphql": "^12.0.9",
"@nestjs/jwt": "^10.1.1",
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.6",
"@nestjs/schedule": "^4.0.1",
"@nestjs/throttler": "^5.0.0",
"@prisma/client": "^5.8.0",
"argon2": "^0.30.3",
"bcrypt": "^5.1.0",
"cookie": "^0.5.0",
"cookie-parser": "^1.4.6",
"cron": "^3.1.6",
"express": "^4.17.1",
"express-session": "^1.17.3",
"fp-ts": "^2.13.1",
"graphql": "^16.8.1",
"graphql-query-complexity": "^0.12.0",
"graphql-redis-subscriptions": "^2.6.0",
"graphql-subscriptions": "^2.0.0",
"handlebars": "^4.7.7",
"io-ts": "^2.2.16",
"luxon": "^3.2.1",
"nodemailer": "^6.9.1",
"passport": "^0.6.0",
"passport-github2": "^0.1.12",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
"passport-microsoft": "^1.0.0",
"posthog-node": "^3.6.3",
"prisma": "^5.8.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.6.0"
"@apollo/server": "4.9.5",
"@nestjs-modules/mailer": "1.9.1",
"@nestjs/apollo": "12.0.9",
"@nestjs/common": "10.2.7",
"@nestjs/config": "3.1.1",
"@nestjs/core": "10.2.7",
"@nestjs/graphql": "12.0.9",
"@nestjs/jwt": "10.1.1",
"@nestjs/passport": "10.0.2",
"@nestjs/platform-express": "10.2.7",
"@nestjs/schedule": "4.0.1",
"@nestjs/throttler": "5.0.1",
"@prisma/client": "5.8.1",
"argon2": "0.30.3",
"bcrypt": "5.1.0",
"cookie": "0.5.0",
"cookie-parser": "1.4.6",
"cron": "3.1.6",
"express": "4.18.2",
"express-session": "1.17.3",
"fp-ts": "2.13.1",
"graphql": "16.8.1",
"graphql-query-complexity": "0.12.0",
"graphql-redis-subscriptions": "2.6.0",
"graphql-subscriptions": "2.0.0",
"handlebars": "4.7.7",
"io-ts": "2.2.16",
"luxon": "3.2.1",
"nodemailer": "6.9.1",
"passport": "0.6.0",
"passport-github2": "0.1.12",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"passport-microsoft": "1.0.0",
"posthog-node": "3.6.3",
"prisma": "5.8.1",
"reflect-metadata": "0.1.13",
"rimraf": "3.0.2",
"rxjs": "7.6.0"
},
"devDependencies": {
"@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.6",
"@relmify/jest-fp-ts": "^2.0.2",
"@types/argon2": "^0.15.0",
"@types/bcrypt": "^5.0.0",
"@types/cookie": "^0.5.1",
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.14",
"@types/jest": "^29.4.0",
"@types/luxon": "^3.2.0",
"@types/node": "^18.11.10",
"@types/nodemailer": "^6.4.7",
"@types/passport-github2": "^1.2.5",
"@types/passport-google-oauth20": "^2.0.11",
"@types/passport-jwt": "^3.0.8",
"@types/passport-microsoft": "^0.0.0",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^5.45.0",
"@typescript-eslint/parser": "^5.45.0",
"cross-env": "^7.0.3",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.4.1",
"jest-mock-extended": "^3.0.1",
"@nestjs/cli": "10.2.1",
"@nestjs/schematics": "10.0.3",
"@nestjs/testing": "10.2.7",
"@relmify/jest-fp-ts": "2.0.2",
"@types/argon2": "0.15.0",
"@types/bcrypt": "5.0.0",
"@types/cookie": "0.5.1",
"@types/cookie-parser": "1.4.3",
"@types/express": "4.17.14",
"@types/jest": "29.4.0",
"@types/luxon": "3.2.0",
"@types/node": "18.11.10",
"@types/nodemailer": "6.4.7",
"@types/passport-github2": "1.2.5",
"@types/passport-google-oauth20": "2.0.11",
"@types/passport-jwt": "3.0.8",
"@types/passport-microsoft": "0.0.0",
"@types/supertest": "2.0.12",
"@typescript-eslint/eslint-plugin": "5.45.0",
"@typescript-eslint/parser": "5.45.0",
"cross-env": "7.0.3",
"eslint": "8.29.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-prettier": "4.2.1",
"jest": "29.4.1",
"jest-mock-extended": "3.0.1",
"jwt": "link:@types/nestjs/jwt",
"prettier": "^2.8.4",
"source-map-support": "^0.5.21",
"supertest": "^6.3.2",
"prettier": "2.8.4",
"source-map-support": "0.5.21",
"supertest": "6.3.2",
"ts-jest": "29.0.5",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"ts-loader": "9.4.2",
"ts-node": "10.9.1",
"tsconfig-paths": "4.1.1",
"typescript": "^4.9.3"
"typescript": "4.9.3"
},
"jest": {
"moduleFileExtensions": [
@@ -121,4 +121,4 @@
"^src/(.*)$": "<rootDir>/$1"
}
}
}
}

View File

@@ -1,17 +1,22 @@
-- AlterTable
ALTER TABLE
-- This is a custom migration file which is not generated by Prisma.
-- The aim of this migration is to add text search indices to the TeamCollection and TeamRequest tables.
-- Create Extension
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Create GIN Trigram Index for Team Collection title
CREATE INDEX
"TeamCollection_title_trgm_idx"
ON
"TeamCollection"
ADD
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
USING
GIN (title gin_trgm_ops);
-- AlterTable
ALTER TABLE
-- Create GIN Trigram Index for Team Collection title
CREATE INDEX
"TeamRequest_title_trgm_idx"
ON
"TeamRequest"
ADD
titleSearch tsvector GENERATED ALWAYS AS (to_tsvector('english', title)) STORED;
USING
GIN (title gin_trgm_ops);
-- CreateIndex
CREATE INDEX "TeamCollection_textSearch_idx" ON "TeamCollection" USING GIN (titleSearch);
-- CreateIndex
CREATE INDEX "TeamRequest_textSearch_idx" ON "TeamRequest" USING GIN (titleSearch);

View File

@@ -84,6 +84,12 @@ export const USER_ALREADY_INVITED = 'admin/user_already_invited' as const;
*/
export const USER_UPDATE_FAILED = 'user/update_failed' as const;
/**
* User display name validation failure
* (UserService)
*/
export const USER_SHORT_DISPLAY_NAME = 'user/short_display_name' as const;
/**
* User deletion failure
* (UserService)
@@ -750,3 +756,8 @@ export const DATABASE_TABLE_NOT_EXIST =
* (InfraConfigService)
*/
export const POSTHOG_CLIENT_NOT_INITIALIZED = 'posthog/client_not_initialized';
/**
* Inputs supplied are invalid
*/
export const INVALID_PARAMS = 'invalid_parameters' as const;

View File

@@ -321,25 +321,28 @@ export class InfraConfigService implements OnModuleInit {
* Reset all the InfraConfigs to their default values (from .env)
*/
async reset() {
// These are all the infra-configs that should not be reset
const RESET_EXCLUSION_LIST = [
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
];
try {
const infraConfigDefaultObjs = await getDefaultInfraConfigs();
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(p) => RESET_EXCLUSION_LIST.includes(p.name) === false,
);
await this.prisma.infraConfig.deleteMany({
where: { name: { in: infraConfigDefaultObjs.map((p) => p.name) } },
where: {
name: {
in: updatedInfraConfigDefaultObjs.map((p) => p.name),
},
},
});
// Hardcode t
const updatedInfraConfigDefaultObjs = infraConfigDefaultObjs.filter(
(obj) => obj.name !== InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
);
await this.prisma.infraConfig.createMany({
data: [
...updatedInfraConfigDefaultObjs,
{
name: InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
value: 'true',
},
],
data: updatedInfraConfigDefaultObjs,
});
stopApp();

View File

@@ -1,4 +1,11 @@
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import {
Controller,
Get,
HttpStatus,
Param,
Query,
UseGuards,
} from '@nestjs/common';
import { TeamCollectionService } from './team-collection.service';
import * as E from 'fp-ts/Either';
import { ThrottlerBehindProxyGuard } from 'src/guards/throttler-behind-proxy.guard';
@@ -7,13 +14,15 @@ import { RequiresTeamRole } from 'src/team/decorators/requires-team-role.decorat
import { TeamMemberRole } from '@prisma/client';
import { RESTTeamMemberGuard } from 'src/team/guards/rest-team-member.guard';
import { throwHTTPErr } from 'src/utils';
import { RESTError } from 'src/types/RESTError';
import { INVALID_PARAMS } from 'src/errors';
@UseGuards(ThrottlerBehindProxyGuard)
@Controller({ path: 'team-collection', version: '1' })
export class TeamCollectionController {
constructor(private readonly teamCollectionService: TeamCollectionService) {}
@Get('search/:teamID/:searchQuery')
@Get('search/:teamID')
@RequiresTeamRole(
TeamMemberRole.VIEWER,
TeamMemberRole.EDITOR,
@@ -21,13 +30,20 @@ export class TeamCollectionController {
)
@UseGuards(JwtAuthGuard, RESTTeamMemberGuard)
async searchByTitle(
@Param('searchQuery') searchQuery: string,
@Query('searchQuery') searchQuery: string,
@Param('teamID') teamID: string,
@Query('take') take: string,
@Query('skip') skip: string,
) {
if (!teamID || !searchQuery) {
return <RESTError>{
message: INVALID_PARAMS,
statusCode: HttpStatus.BAD_REQUEST,
};
}
const res = await this.teamCollectionService.searchByTitle(
searchQuery,
searchQuery.trim(),
teamID,
parseInt(take),
parseInt(skip),

View File

@@ -20,7 +20,7 @@ import {
TEAM_COLL_PARENT_TREE_GEN_FAILED,
} from '../errors';
import { PubSubService } from '../pubsub/pubsub.service';
import { isValidLength } from 'src/utils';
import { escapeSqlLikeString, isValidLength } from 'src/utils';
import * as E from 'fp-ts/Either';
import * as O from 'fp-ts/Option';
import { Prisma, TeamCollection as DBTeamCollection } from '@prisma/client';
@@ -1125,7 +1125,7 @@ export class TeamCollectionService {
id: searchResults[i].id,
path: !fetchedParentTree
? []
: ([fetchedParentTree.right] as CollectionSearchNode[]),
: (fetchedParentTree.right as CollectionSearchNode[]),
});
}
@@ -1148,14 +1148,20 @@ export class TeamCollectionService {
skip: number,
) {
const query = Prisma.sql`
select id,title,'collection' AS type
from "TeamCollection"
where "TeamCollection"."teamID"=${teamID}
and titlesearch @@ to_tsquery(${searchQuery})
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
limit ${take}
SELECT
id,title,'collection' AS type
FROM
"TeamCollection"
WHERE
"TeamCollection"."teamID"=${teamID}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
try {
const res = await this.prisma.$queryRaw<SearchQueryReturnType[]>(query);
return E.right(res);
@@ -1180,12 +1186,17 @@ export class TeamCollectionService {
skip: number,
) {
const query = Prisma.sql`
select id,title,request->>'method' as method,'request' AS type
from "TeamRequest"
where "TeamRequest"."teamID"=${teamID}
and titlesearch @@ to_tsquery(${searchQuery})
order by ts_rank(titlesearch,to_tsquery(${searchQuery}))
limit ${take}
SELECT
id,title,request->>'method' as method,'request' AS type
FROM
"TeamRequest"
WHERE
"TeamRequest"."teamID"=${teamID}
AND
title ILIKE ${`%${escapeSqlLikeString(searchQuery)}%`}
ORDER BY
similarity(title, ${searchQuery})
LIMIT ${take}
OFFSET ${skip === 0 ? 0 : (skip - 1) * take};
`;
@@ -1250,45 +1261,53 @@ export class TeamCollectionService {
* @returns The parent tree of the parent collections
*/
private generateParentTree(parentCollections: ParentTreeQueryReturnType[]) {
function findChildren(id) {
function findChildren(id: string): CollectionSearchNode[] {
const collection = parentCollections.filter((item) => item.id === id)[0];
if (collection.parentID == null) {
return {
id: collection.id,
title: collection.title,
type: 'collection',
path: [],
};
return <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: [],
},
];
}
const res = {
id: collection.id,
title: collection.title,
type: 'collection',
path: findChildren(collection.parentID),
};
const res = <CollectionSearchNode[]>[
{
id: collection.id,
title: collection.title,
type: 'collection' as const,
path: findChildren(collection.parentID),
},
];
return res;
}
if (parentCollections.length > 0) {
if (parentCollections[0].parentID == null) {
return {
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: [],
},
];
}
return <CollectionSearchNode[]>[
{
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: [],
};
}
return {
id: parentCollections[0].id,
title: parentCollections[0].title,
type: 'collection',
path: findChildren(parentCollections[0].parentID),
};
path: findChildren(parentCollections[0].parentID),
},
];
}
return null;
return <CollectionSearchNode[]>[];
}
/**

View File

@@ -58,6 +58,29 @@ export class UserResolver {
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
return updatedUser.right;
}
@Mutation(() => User, {
description: 'Update a users display name',
})
@UseGuards(GqlAuthGuard)
async updateDisplayName(
@GqlUser() user: AuthUser,
@Args({
name: 'updatedDisplayName',
description: 'New name of user',
type: () => String,
})
updatedDisplayName: string,
) {
const updatedUser = await this.userService.updateUserDisplayName(
user.uid,
updatedDisplayName,
);
if (E.isLeft(updatedUser)) throwErr(updatedUser.left);
return updatedUser.right;
}
@Mutation(() => Boolean, {
description: 'Delete an user account',
})

View File

@@ -1,4 +1,9 @@
import { JSON_INVALID, USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
import {
JSON_INVALID,
USERS_NOT_FOUND,
USER_NOT_FOUND,
USER_SHORT_DISPLAY_NAME,
} from 'src/errors';
import { mockDeep, mockReset } from 'jest-mock-extended';
import { PrismaService } from 'src/prisma/prisma.service';
import { AuthUser } from 'src/types/AuthUser';
@@ -480,6 +485,14 @@ describe('UserService', () => {
);
expect(result).toEqualLeft(USER_NOT_FOUND);
});
test('should resolve left and error when short display name is passed', async () => {
const newDisplayName = '';
const result = await userService.updateUserDisplayName(
user.uid,
newDisplayName,
);
expect(result).toEqualLeft(USER_SHORT_DISPLAY_NAME);
});
});
describe('fetchAllUsers', () => {

View File

@@ -8,7 +8,11 @@ import * as T from 'fp-ts/Task';
import * as A from 'fp-ts/Array';
import { pipe, constVoid } from 'fp-ts/function';
import { AuthUser } from 'src/types/AuthUser';
import { USERS_NOT_FOUND, USER_NOT_FOUND } from 'src/errors';
import {
USERS_NOT_FOUND,
USER_NOT_FOUND,
USER_SHORT_DISPLAY_NAME,
} from 'src/errors';
import { SessionType, User } from './user.model';
import { USER_UPDATE_FAILED } from 'src/errors';
import { PubSubService } from 'src/pubsub/pubsub.service';
@@ -291,6 +295,10 @@ export class UserService {
* @returns a Either of User or error
*/
async updateUserDisplayName(userUID: string, displayName: string) {
if (!displayName || displayName.length === 0) {
return E.left(USER_SHORT_DISPLAY_NAME);
}
try {
const dbUpdatedUser = await this.prisma.user.update({
where: { uid: userUID },

View File

@@ -250,3 +250,39 @@ export function checkEnvironmentAuthProvider(
}
}
}
/**
* Adds escape backslashes to the input so that it can be used inside
* SQL LIKE/ILIKE queries. Inspired by PHP's `mysql_real_escape_string`
* function.
*
* Eg. "100%" -> "100\\%"
*
* Source: https://stackoverflow.com/a/32648526
*/
export function escapeSqlLikeString(str: string) {
if (typeof str != 'string') return str;
return str.replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
switch (char) {
case '\0':
return '\\0';
case '\x08':
return '\\b';
case '\x09':
return '\\t';
case '\x1a':
return '\\z';
case '\n':
return '\\n';
case '\r':
return '\\r';
case '"':
case "'":
case '\\':
case '%':
return '\\' + char; // prepends a backslash to backslash, percent,
// and double/single quotes
}
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "@hoppscotch/cli",
"version": "0.6.0",
"version": "0.7.0",
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
"homepage": "https://hoppscotch.io",
"type": "module",
@@ -41,30 +41,28 @@
"license": "MIT",
"private": false,
"dependencies": {
"axios": "^1.6.6",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"lodash-es": "^4.17.21",
"qs": "^6.11.2",
"zod": "^3.22.4"
"axios": "1.6.7",
"chalk": "5.3.0",
"commander": "11.1.0",
"lodash-es": "4.17.21",
"qs": "6.11.2",
"verzod": "0.2.2",
"zod": "3.22.4"
},
"devDependencies": {
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@relmify/jest-fp-ts": "^2.1.1",
"@swc/core": "^1.3.105",
"@types/jest": "^29.5.11",
"@types/lodash-es": "^4.17.12",
"@types/qs": "^6.9.11",
"fp-ts": "^2.16.2",
"jest": "^29.7.0",
"lodash": "^4.17.21",
"prettier": "^3.2.4",
"qs": "^6.11.2",
"ts-jest": "^29.1.2",
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"verzod": "^0.2.2",
"zod": "^3.22.4"
"@relmify/jest-fp-ts": "2.1.1",
"@swc/core": "1.4.2",
"@types/jest": "29.5.12",
"@types/lodash-es": "4.17.12",
"@types/qs": "6.9.12",
"fp-ts": "2.16.2",
"jest": "29.7.0",
"prettier": "3.2.5",
"qs": "6.11.2",
"ts-jest": "29.1.2",
"tsup": "8.0.2",
"typescript": "5.3.3"
}
}
}

View File

@@ -20,7 +20,7 @@ describe("Test `hopp test <file>` command:", () => {
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
})
});
describe("Supplied collection export file validations", () => {
test("Errors with the code `FILE_NOT_FOUND` if the supplied collection export file doesn't exist", async () => {
@@ -66,6 +66,43 @@ describe("Test `hopp test <file>` command:", () => {
});
});
describe("Versioned entities", () => {
describe("Collections & Requests", () => {
const testFixtures = [
{ fileName: "coll-v1-req-v0.json", collVersion: 1, reqVersion: 0 },
{ fileName: "coll-v1-req-v1.json", collVersion: 1, reqVersion: 1 },
{ fileName: "coll-v2-req-v2.json", collVersion: 2, reqVersion: 2 },
{ fileName: "coll-v2-req-v3.json", collVersion: 2, reqVersion: 3 },
];
testFixtures.forEach(({ collVersion, fileName, reqVersion }) => {
test(`Successfully processes a supplied collection export file where the collection is based on the "v${collVersion}" schema and the request following the "v${reqVersion}" schema`, async () => {
const args = `test ${getTestJsonFilePath(fileName, "collection")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
describe("Environments", () => {
const testFixtures = [
{ fileName: "env-v0.json", version: 0 },
{ fileName: "env-v1.json", version: 1 },
];
testFixtures.forEach(({ fileName, version }) => {
test(`Successfully processes the supplied collection and environment export files where the environment is based on the "v${version}" schema`, async () => {
const ENV_PATH = getTestJsonFilePath(fileName, "environment");
const args = `test ${getTestJsonFilePath("sample-coll.json", "collection")} --env ${ENV_PATH}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
});
});
test("Successfully processes a supplied collection export file of the expected format", async () => {
const args = `test ${getTestJsonFilePath("passes-coll.json", "collection")}`;
const { error } = await runCLI(args);
@@ -75,7 +112,8 @@ describe("Test `hopp test <file>` command:", () => {
test("Successfully inherits headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath(
"collection-level-headers-auth-coll.json", "collection"
"collection-level-headers-auth-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
@@ -84,7 +122,8 @@ describe("Test `hopp test <file>` command:", () => {
test("Persists environment variables set in the pre-request script for consumption in the test script", async () => {
const args = `test ${getTestJsonFilePath(
"pre-req-script-env-var-persistence-coll.json", "collection"
"pre-req-script-env-var-persistence-coll.json",
"collection"
)}`;
const { error } = await runCLI(args);
@@ -106,7 +145,8 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
test("Errors with the code `INVALID_FILE_TYPE` if the supplied environment export file doesn't end with the `.json` extension", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath(
"notjson-coll.txt", "collection"
"notjson-coll.txt",
"collection"
)}`;
const { stderr } = await runCLI(args);
@@ -123,7 +163,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
});
test("Errors with the code `MALFORMED_ENV_FILE` on supplying a malformed environment export file", async () => {
const ENV_PATH = getTestJsonFilePath("malformed-envs.json", "environment");
const ENV_PATH = getTestJsonFilePath(
"malformed-envs.json",
"environment"
);
const args = `${VALID_TEST_ARGS} --env ${ENV_PATH}`;
const { stderr } = await runCLI(args);
@@ -142,7 +185,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
});
test("Successfully resolves values from the supplied environment export file", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
@@ -151,8 +197,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
});
test("Successfully resolves environment variables referenced in the request body", async () => {
const COLL_PATH = getTestJsonFilePath("req-body-env-vars-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("req-body-env-vars-envs.json", "environment");
const COLL_PATH = getTestJsonFilePath(
"req-body-env-vars-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"req-body-env-vars-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error } = await runCLI(args);
@@ -160,7 +212,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
});
test("Works with shorth `-e` flag", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests-coll.json", "collection");
const TESTS_PATH = getTestJsonFilePath(
"env-flag-tests-coll.json",
"collection"
);
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json", "environment");
const args = `test ${TESTS_PATH} -e ${ENV_PATH}`;
@@ -183,7 +238,10 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
secretHeaderValue: "secret-header-value",
};
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath("secret-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
@@ -197,8 +255,14 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
// Prefers values specified in the environment export file over values set in the system environment
test("Successfully picks the values for secret environment variables set directly in the environment export file and persists the environment variables set from the pre-request script", async () => {
const COLL_PATH = getTestJsonFilePath("secret-envs-coll.json", "collection");
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const COLL_PATH = getTestJsonFilePath(
"secret-envs-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
@@ -212,9 +276,13 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
// Values set from the scripting context takes the highest precedence
test("Setting values for secret environment variables from the pre-request script overrides values set at the supplied environment export file", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-coll.json", "collection"
"secret-envs-persistence-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-supplied-values-envs.json",
"environment"
);
const ENVS_PATH = getTestJsonFilePath("secret-supplied-values-envs.json", "environment");
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;
const { error, stdout } = await runCLI(args);
@@ -227,10 +295,12 @@ describe("Test `hopp test <file> --env <file>` command:", () => {
test("Persists secret environment variable values set from the pre-request script for consumption in the request and post-request script context", async () => {
const COLL_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-coll.json", "collection"
"secret-envs-persistence-scripting-coll.json",
"collection"
);
const ENVS_PATH = getTestJsonFilePath(
"secret-envs-persistence-scripting-envs.json", "environment"
"secret-envs-persistence-scripting-envs.json",
"environment"
);
const args = `test ${COLL_PATH} --env ${ENVS_PATH}`;

View File

@@ -1,84 +0,0 @@
import { isRESTCollection } from "../../../utils/checks";
describe("isRESTCollection", () => {
test("Undefined collection value.", () => {
expect(isRESTCollection(undefined)).toBeFalsy();
});
test("Invalid id value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: 1,
})
).toBeFalsy();
});
test("Invalid requests value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: null,
})
).toBeFalsy();
});
test("Invalid folders value.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: undefined,
})
).toBeFalsy();
});
test("Invalid RESTCollection(s) in folders.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: [
{
v: 1,
name: "test1",
id: "2",
requests: undefined,
folders: [],
},
],
})
).toBeFalsy();
});
test("Invalid HoppRESTRequest(s) in requests.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [{}],
folders: [],
})
).toBeFalsy();
});
test("Valid RESTCollection.", () => {
expect(
isRESTCollection({
v: 1,
name: "test",
id: "1",
requests: [],
folders: [],
})
).toBeTruthy();
});
});

View File

@@ -0,0 +1,27 @@
{
"v": 1,
"name": "coll-v1",
"folders": [],
"requests": [
{
"url": "https://httpbin.org",
"path": "/get",
"headers": [
{ "key": "Inactive-Header", "value": "Inactive Header", "active": false },
{ "key": "Authorization", "value": "Bearer token123", "active": true }
],
"params": [
{ "key": "key", "value": "value", "active": true },
{ "key": "inactive-key", "value": "inactive-param", "active": false }
],
"name": "req-v0",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"contentType": "application/json",
"body": "",
"auth": "Bearer Token",
"bearerToken": "token123"
}
]
}

View File

@@ -0,0 +1,48 @@
{
"v": 1,
"name": "coll-v1",
"folders": [],
"requests": [
{
"v": "1",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v1",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
}
}
]
}

View File

@@ -0,0 +1,54 @@
{
"v": 2,
"name": "coll-v2",
"folders": [],
"requests": [
{
"v": "2",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v2",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

View File

@@ -0,0 +1,54 @@
{
"v": 2,
"name": "coll-v2",
"folders": [],
"requests": [
{
"v": "3",
"endpoint": "https://httpbin.org/get",
"headers": [
{
"key": "Inactive-Header",
"value": "Inactive Header",
"active": false
},
{
"key": "Authorization",
"value": "Bearer token123",
"active": true
}
],
"params": [
{
"key": "key",
"value": "value",
"active": true
},
{
"key": "inactive-key",
"value": "inactive-param",
"active": false
}
],
"name": "req-v3",
"method": "GET",
"preRequestScript": "",
"testScript": "pw.test(\"Asserts request params\", () => {\n pw.expect(pw.response.body.args.key).toBe(\"value\")\n pw.expect(pw.response.body.args[\"inactive-key\"]).toBe(undefined)\n})\n\npw.test(\"Asserts request headers\", () => {\n pw.expect(pw.response.body.headers[\"Authorization\"]).toBe(\"Bearer token123\")\n pw.expect(pw.response.body.headers[\"Inactive-Header\"]).toBe(undefined)\n})",
"body": {
"contentType": null,
"body": null
},
"auth": {
"authType": "bearer",
"authActive": true,
"token": "token123"
},
"requestVariables": []
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}

View File

@@ -1,23 +1,23 @@
[
{
"v": 1,
"v": 2,
"name": "CollectionA",
"folders": [
{
"v": 1,
"v": 2,
"name": "FolderA",
"folders": [
{
"v": 1,
"v": 2,
"name": "FolderB",
"folders": [
{
"v": 1,
"v": 2,
"name": "FolderC",
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestD",
"params": [],
@@ -40,7 +40,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -52,7 +53,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestC",
"params": [],
@@ -67,7 +68,8 @@
"body": {
"contentType": null,
"body": null
}
},
"requestVariables": []
}
],
"auth": {
@@ -88,7 +90,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -104,6 +106,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -116,7 +119,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -132,6 +135,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -149,16 +153,16 @@
}
},
{
"v": 1,
"v": 2,
"name": "CollectionB",
"folders": [
{
"v": 1,
"v": 2,
"name": "FolderA",
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
@@ -174,6 +178,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -186,7 +191,7 @@
],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
@@ -202,6 +207,7 @@
"contentType": null,
"body": null
},
"requestVariables": [],
"id": "clpttpdq00003qp16kut6doqv"
}
],
@@ -218,4 +224,4 @@
"token": "BearerToken"
}
}
]
]

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "<<URL>>",
"name": "test1",
"params": [],
@@ -16,7 +16,8 @@
"body": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -13,20 +13,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npwd.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.dio/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -34,17 +32,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.setd(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -12,20 +12,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"string\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": [],
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -33,17 +31,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(300);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -2,9 +2,9 @@
{
"v": 1,
"folders": [],
"requests":
"requests":
{
"v": "1",
"v": "2",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "fail",
"params": [],
@@ -22,7 +22,8 @@
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -5,7 +5,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE1>>",
"name": "",
"params": [],
@@ -13,20 +13,18 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE1\", \"devblin_local1\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE1>>\"\n}"
}
},
"requestVariables": []
},
{
"v": "1",
"v": "3",
"endpoint": "https://echo.hoppscotch.io/<<HEADERS_TYPE2>>",
"name": "success",
"params": [],
@@ -34,17 +32,15 @@
"method": "GET",
"auth": {
"authType": "none",
"authActive": true,
"addTo": "Headers",
"key": "",
"value": ""
"authActive": true
},
"preRequestScript": "pw.env.set(\"HEADERS_TYPE2\", \"devblin_local2\");",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": "application/json",
"body": "{\n\"test\": \"<<HEADERS_TYPE2>>\"\n}"
}
},
"requestVariables": []
}
]
}

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "sample-req",
@@ -13,7 +13,8 @@
"headers": [],
"endpoint": "https://echo.hoppscotch.io",
"testScript": "pw.expect(pw.env.get(\"variable\")).toBe(\"value\")",
"preRequestScript": "pw.env.set(\"variable\", \"value\");"
"preRequestScript": "pw.env.set(\"variable\", \"value\");",
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": true },

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"name": "test-request",
"endpoint": "https://echo.hoppscotch.io",
"method": "POST",
@@ -19,7 +19,8 @@
"body": "{\n \"firstName\": \"<<firstName>>\",\n \"lastName\": \"<<lastName>>\",\n \"greetText\": \"<<salutation>>, <<fullName>>\",\n \"fullName\": \"<<fullName>>\",\n \"id\": \"<<id>>\"\n}"
},
"preRequestScript": "",
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});"
"testScript": "pw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\npw.test(\"Successfully resolves environments recursively\", ()=> {\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n});\n\npw.test(\"Successfully resolves environments referenced in the request body\", () => {\n const expectedId = \"7\"\n const expectedFirstName = \"John\"\n const expectedLastName = \"Doe\"\n const expectedFullName = `${expectedFirstName} ${expectedLastName}`\n const expectedGreetText = `Hello, ${expectedFullName}`\n\n pw.expect(pw.env.getResolve(\"recursiveVarX\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"recursiveVarY\")).toBe(\"Hello\")\n pw.expect(pw.env.getResolve(\"salutation\")).toBe(\"Hello\")\n\n const { id, firstName, lastName, fullName, greetText } = JSON.parse(pw.response.body.data)\n\n pw.expect(id).toBe(expectedId)\n pw.expect(expectedFirstName).toBe(firstName)\n pw.expect(expectedLastName).toBe(lastName)\n pw.expect(fullName).toBe(expectedFullName)\n pw.expect(greetText).toBe(expectedGreetText)\n});",
"requestVariables": []
}
],
"auth": {

View File

@@ -0,0 +1,26 @@
{
"v": 1,
"name": "tests",
"folders": [],
"requests": [
{
"v": "2",
"endpoint": "<<baseURL>>",
"name": "",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "none",
"authActive": true
},
"preRequestScript": "",
"testScript": "// Check status code is 200\npw.test(\"Status code is 200\", ()=> {\n pw.expect(pw.response.status).toBe(200);\n});\n\n// Check JSON response property\npw.test(\"Check JSON response property\", ()=> {\n pw.expect(pw.response.body.method).toBe(\"GET\");\n pw.expect(pw.response.body.headers).toBeType(\"object\");\n});",
"body": {
"contentType": null,
"body": null
},
"requestVariables": []
}
]
}

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-headers",
@@ -17,12 +17,13 @@
"active": true
}
],
"requestVariables": [],
"endpoint": "<<baseURL>>/headers",
"testScript": "pw.test(\"Successfully parses secret variable holding the header value\", () => {\n const secretHeaderValue = pw.env.get(\"secretHeaderValue\")\n pw.expect(secretHeaderValue).toBe(\"secret-header-value\")\n \n if (secretHeaderValue) {\n pw.expect(pw.response.body.headers[\"Secret-Header-Key\"]).toBe(secretHeaderValue)\n }\n\n pw.expect(pw.env.get(\"secretHeaderValueFromPreReqScript\")).toBe(\"secret-header-value\")\n})",
"preRequestScript": "const secretHeaderValueFromPreReqScript = pw.env.get(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": {
"body": "{\n \"secretBodyKey\": \"<<secretBodyValue>>\"\n}",
@@ -32,12 +33,13 @@
"method": "POST",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-query-params",
@@ -50,12 +52,13 @@
}
],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
@@ -67,12 +70,13 @@
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": ""
},
{
"v": "1",
"v": "3",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
@@ -85,18 +89,20 @@
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.get(\"secretBearerToken\")\n const preReqSecretBearerToken = pw.env.get(\"preReqSecretBearerToken\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",
"preRequestScript": "const secretBearerToken = pw.env.get(\"secretBearerToken\")\npw.env.set(\"preReqSecretBearerToken\", secretBearerToken)"
},
{
"v": "1",
"v": "3",
"auth": { "authType": "none", "authActive": true },
"body": { "body": null, "contentType": null },
"name": "test-secret-fallback",
"method": "GET",
"params": [],
"headers": [],
"requestVariables": [],
"endpoint": "<<baseURL>>",
"testScript": "pw.test(\"Returns an empty string if the value for a secret environment variable is not found in the system environment\", () => {\n pw.expect(pw.env.get(\"nonExistentValueInSystemEnv\")).toBe(\"\")\n})",
"preRequestScript": ""

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"auth": {
"authType": "none",
"authActive": true
@@ -16,6 +16,7 @@
"name": "test-secret-headers",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
@@ -28,7 +29,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": {
"authType": "none",
"authActive": true
@@ -40,6 +41,7 @@
"name": "test-secret-headers-overrides",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [
{
"key": "Secret-Header-Key",
@@ -52,7 +54,7 @@
"preRequestScript": "pw.env.set(\"secretHeaderValue\", \"secret-header-value-overriden\")\n\nconst secretHeaderValueFromPreReqScript = pw.env.getResolve(\"secretHeaderValue\")\npw.env.set(\"secretHeaderValueFromPreReqScript\", secretHeaderValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": {
"authType": "none",
"authActive": true
@@ -64,13 +66,14 @@
"name": "test-secret-body",
"method": "POST",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/post",
"testScript": "pw.test(\"Successfully parses secret variable holding the request body value\", () => {\n const secretBodyValue = pw.env.get(\"secretBodyValue\")\n pw.expect(secretBodyValue).toBe(\"secret-body-value\")\n \n if (secretBodyValue) {\n pw.expect(pw.response.body.json.secretBodyKey).toBe(secretBodyValue)\n }\n\n pw.expect(pw.env.get(\"secretBodyValueFromPreReqScript\")).toBe(\"secret-body-value\")\n})",
"preRequestScript": "const secretBodyValue = pw.env.get(\"secretBodyValue\")\n\nif (!secretBodyValue) { \n pw.env.set(\"secretBodyValue\", \"secret-body-value\")\n}\n\nconst secretBodyValueFromPreReqScript = pw.env.get(\"secretBodyValue\")\npw.env.set(\"secretBodyValueFromPreReqScript\", secretBodyValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": {
"authType": "none",
"authActive": true
@@ -88,13 +91,14 @@
"active": true
}
],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/get",
"testScript": "pw.test(\"Successfully parses secret variable holding the query param value\", () => {\n const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n pw.expect(secretQueryParamValue).toBe(\"secret-query-param-value\")\n \n if (secretQueryParamValue) {\n pw.expect(pw.response.body.args.secretQueryParamKey).toBe(secretQueryParamValue)\n }\n\n pw.expect(pw.env.get(\"secretQueryParamValueFromPreReqScript\")).toBe(\"secret-query-param-value\")\n})",
"preRequestScript": "const secretQueryParamValue = pw.env.get(\"secretQueryParamValue\")\n\nif (!secretQueryParamValue) {\n pw.env.set(\"secretQueryParamValue\", \"secret-query-param-value\")\n}\n\nconst secretQueryParamValueFromPreReqScript = pw.env.get(\"secretQueryParamValue\")\npw.env.set(\"secretQueryParamValueFromPreReqScript\", secretQueryParamValueFromPreReqScript)"
},
{
"v": "1",
"v": "3",
"auth": {
"authType": "basic",
"password": "<<secretBasicAuthPassword>>",
@@ -108,13 +112,14 @@
"name": "test-secret-basic-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/basic-auth/<<secretBasicAuthUsername>>/<<secretBasicAuthPassword>>",
"testScript": "pw.test(\"Successfully parses secret variables holding basic auth credentials\", () => {\n\tconst secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n \tconst secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\n pw.expect(secretBasicAuthUsername).toBe(\"test-user\")\n pw.expect(secretBasicAuthPassword).toBe(\"test-pass\")\n\n if (secretBasicAuthUsername && secretBasicAuthPassword) {\n const { authenticated, user } = pw.response.body\n pw.expect(authenticated).toBe(true)\n pw.expect(user).toBe(secretBasicAuthUsername)\n }\n});",
"preRequestScript": "let secretBasicAuthUsername = pw.env.get(\"secretBasicAuthUsername\")\n\nlet secretBasicAuthPassword = pw.env.get(\"secretBasicAuthPassword\")\n\nif (!secretBasicAuthUsername) {\n pw.env.set(\"secretBasicAuthUsername\", \"test-user\")\n}\n\nif (!secretBasicAuthPassword) {\n pw.env.set(\"secretBasicAuthPassword\", \"test-pass\")\n}"
},
{
"v": "1",
"v": "3",
"auth": {
"token": "<<secretBearerToken>>",
"authType": "bearer",
@@ -129,6 +134,7 @@
"name": "test-secret-bearer-auth",
"method": "GET",
"params": [],
"requestVariables": [],
"headers": [],
"endpoint": "<<baseURL>>/bearer",
"testScript": "pw.test(\"Successfully parses secret variable holding the bearer token\", () => {\n const secretBearerToken = pw.env.resolve(\"<<secretBearerToken>>\")\n const preReqSecretBearerToken = pw.env.resolve(\"<<preReqSecretBearerToken>>\")\n\n pw.expect(secretBearerToken).toBe(\"test-token\")\n\n if (secretBearerToken) { \n pw.expect(pw.response.body.token).toBe(secretBearerToken)\n pw.expect(preReqSecretBearerToken).toBe(\"test-token\")\n }\n});",

View File

@@ -4,7 +4,7 @@
"folders": [],
"requests": [
{
"v": "1",
"v": "3",
"endpoint": "https://httpbin.org/post",
"name": "req",
"params": [],
@@ -22,7 +22,8 @@
"body": {
"contentType": "application/json",
"body": "{\n \"key\": \"<<customBodyValue>>\"\n}"
}
},
"requestVariables": []
}
],
"auth": { "authType": "inherit", "authActive": false },

View File

@@ -0,0 +1,9 @@
{
"name": "env-v0",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"name": "env-v0",
"variables": [
{
"key": "baseURL",
"value": "https://echo.hoppscotch.io",
"secret": false
}
]
}

View File

@@ -6,7 +6,7 @@ import { error } from "../../types/errors";
import {
HoppEnvKeyPairObject,
HoppEnvPair,
HoppEnvs
HoppEnvs,
} from "../../types/request";
import { readJsonFile } from "../../utils/mutators";
@@ -17,7 +17,7 @@ import { readJsonFile } from "../../utils/mutators";
*/
export async function parseEnvsData(path: string) {
const contents = await readJsonFile(path);
const envPairs: Array<Environment["variables"][number] | HoppEnvPair> = [];
const envPairs: Array<HoppEnvPair | Record<string, string>> = [];
// The legacy key-value pair format that is still supported
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
@@ -26,7 +26,9 @@ export async function parseEnvsData(path: string) {
const HoppEnvExportObjectResult = Environment.safeParse(contents);
// Shape of the bulk environment export object that is exported from the app
const HoppBulkEnvExportObjectResult = z.array(entityReference(Environment)).safeParse(contents)
const HoppBulkEnvExportObjectResult = z
.array(entityReference(Environment))
.safeParse(contents);
// CLI doesnt support bulk environments export
// Hence we check for this case and throw an error if it matches the format
@@ -36,13 +38,16 @@ export async function parseEnvsData(path: string) {
// Checks if the environment file is of the correct format
// If it doesnt match either of them, we throw an error
if (!HoppEnvKeyPairResult.success && HoppEnvExportObjectResult.type === "err") {
if (
!HoppEnvKeyPairResult.success &&
HoppEnvExportObjectResult.type === "err"
) {
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
}
if (HoppEnvKeyPairResult.success) {
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
envPairs.push({ key, value });
envPairs.push({ key, value, secret: false });
}
} else if (HoppEnvExportObjectResult.type === "ok") {
envPairs.push(...HoppEnvExportObjectResult.value.variables);

View File

@@ -1,5 +1,3 @@
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
import * as A from "fp-ts/Array";
import { CommanderError } from "commander";
import { HoppCLIError, HoppErrnoException } from "../types/errors";
@@ -14,48 +12,6 @@ export const hasProperty = <P extends PropertyKey>(
prop: P
): target is Record<P, unknown> => prop in target;
/**
* Typeguard to check valid Hoppscotch REST Collection.
* @param param The object to be checked.
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
* False, otherwise.
*/
export const isRESTCollection = (param: unknown): param is HoppCollection => {
if (!!param && typeof param === "object") {
if (!hasProperty(param, "v") || typeof param.v !== "number") {
return false;
}
if (!hasProperty(param, "name") || typeof param.name !== "string") {
return false;
}
if (hasProperty(param, "id") && typeof param.id !== "string") {
return false;
}
if (!hasProperty(param, "requests") || !Array.isArray(param.requests)) {
return false;
} else {
// Checks each requests array to be valid HoppRESTRequest.
const checkRequests = A.every(isHoppRESTRequest)(param.requests);
if (!checkRequests) {
return false;
}
}
if (!hasProperty(param, "folders") || !Array.isArray(param.folders)) {
return false;
} else {
// Checks each folder to be valid REST collection.
const checkFolders = A.every(isRESTCollection)(param.folders);
if (!checkFolders) {
return false;
}
}
return true;
}
return false;
};
/**
* Checks if given error data is of type HoppCLIError, based on existence
* of code property.

View File

@@ -131,7 +131,7 @@ const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
* path of each request within collection-json file, failed-tests-report, errors,
* total execution duration for requests, pre-request-scripts, test-scripts.
* @returns True, if collection runner executed without any errors or failed test-cases.
* False, if errors occured or test-cases failed.
* False, if errors occurred or test-cases failed.
*/
export const collectionsRunnerResult = (
requestsReport: RequestReport[]

View File

@@ -112,7 +112,7 @@ export const printTestsMetrics = (testsMetrics: TestMetrics) => {
/**
* Prints details of each reported error for a request with error code.
* @param path Request's path in collection for which errors occured.
* @param path Request's path in collection for which errors occurred.
* @param errorsReport List of errors reported.
*/
export const printErrorsReport = (

View File

@@ -1,8 +1,11 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import fs from "fs/promises";
import { FormDataEntry } from "../types/request";
import { entityReference } from "verzod";
import { z } from "zod";
import { error } from "../types/errors";
import { isRESTCollection, isHoppErrnoException } from "./checks";
import { HoppCollection } from "@hoppscotch/data";
import { FormDataEntry } from "../types/request";
import { isHoppErrnoException } from "./checks";
/**
* Parses array of FormDataEntry to FormData.
@@ -67,7 +70,11 @@ export async function parseCollectionData(
? contents
: [contents];
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
const collectionSchemaParsedResult = z
.array(entityReference(HoppCollection))
.safeParse(maybeArrayOfCollections);
if (!collectionSchemaParsedResult.success) {
throw error({
code: "MALFORMED_COLLECTION",
path,
@@ -75,5 +82,22 @@ export async function parseCollectionData(
});
}
return maybeArrayOfCollections as HoppCollection[];
return collectionSchemaParsedResult.data.map((collection) => {
const requestSchemaParsedResult = z
.array(entityReference(HoppRESTRequest))
.safeParse(collection.requests);
if (!requestSchemaParsedResult.success) {
throw error({
code: "MALFORMED_COLLECTION",
path,
data: "Please check the collection data.",
});
}
return {
...collection,
requests: requestSchemaParsedResult.data,
};
});
}

View File

@@ -109,18 +109,31 @@ export function getEffectiveRESTRequest(
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
});
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
} else if (request.auth.authType === "bearer") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(
request.auth.token,
envVariables
)}`,
value: `Bearer ${parseTemplateString(request.auth.token, envVariables)}`,
});
} else if (request.auth.authType === "oauth-2") {
const { addTo } = request.auth;
if (addTo === "HEADERS") {
effectiveFinalHeaders.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.grantTypeInfo.token, envVariables)}`,
});
} else if (addTo === "QUERY_PARAMS") {
effectiveFinalParams.push({
active: true,
key: "access_token",
value: parseTemplateString(
request.auth.grantTypeInfo.token,
envVariables
),
});
}
} else if (request.auth.authType === "api-key") {
const { key, value, addTo } = request.auth;
if (addTo === "Headers") {

View File

@@ -41,10 +41,10 @@ const processVariables = (variable: Environment["variables"][number]) => {
...variable,
value:
"value" in variable ? variable.value : process.env[variable.key] || "",
}
};
}
return variable
}
return variable;
};
/**
* Processes given envs, which includes processing each variable in global
@@ -56,10 +56,10 @@ const processEnvs = (envs: HoppEnvs) => {
const processedEnvs = {
global: envs.global.map(processVariables),
selected: envs.selected.map(processVariables),
}
};
return processedEnvs
}
return processedEnvs;
};
/**
* Transforms given request data to request-config used by request-runner to
@@ -70,7 +70,7 @@ const processEnvs = (envs: HoppEnvs) => {
export const createRequest = (req: EffectiveHoppRESTRequest): RequestConfig => {
const config: RequestConfig = {
supported: true,
displayUrl: req.effectiveFinalDisplayURL
displayUrl: req.effectiveFinalDisplayURL,
};
const { finalBody, finalEndpoint, finalHeaders, finalParams } = getRequest;
const reqParams = finalParams(req);
@@ -131,6 +131,7 @@ export const requestRunner =
let status: number;
const baseResponse = await axios(requestConfig);
const { config } = baseResponse;
// PR-COMMENT: type error
const runnerResponse: RequestRunnerResponse = {
...baseResponse,
endpoint: getRequest.endpoint(config.url),
@@ -257,10 +258,13 @@ export const processRequest =
let updatedEnvs = <HoppEnvs>{};
// Fetch values for secret environment variables from system environment
const processedEnvs = processEnvs(envs)
const processedEnvs = processEnvs(envs);
// Executing pre-request-script
const preRequestRes = await preRequestScriptRunner(request, processedEnvs)();
const preRequestRes = await preRequestScriptRunner(
request,
processedEnvs
)();
if (E.isLeft(preRequestRes)) {
printPreRequestRunner.fail();
@@ -347,7 +351,7 @@ export const processRequest =
*/
export const preProcessRequest = (
request: HoppRESTRequest,
collection: HoppCollection,
collection: HoppCollection
): HoppRESTRequest => {
const tempRequest = Object.assign({}, request);
const { headers: parentHeaders, auth: parentAuth } = collection;
@@ -372,8 +376,10 @@ export const preProcessRequest = (
// Filter out header entries present in the parent (folder/collection) under the same name
// This ensures the child headers take precedence over the parent headers
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
})
return !tempRequest.headers.some(
(reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key
);
});
tempRequest.headers.push(...filteredEntries);
} else if (!tempRequest.headers) {
tempRequest.headers = [];

View File

@@ -10,6 +10,9 @@ module.exports = {
parserOptions: {
sourceType: "module",
requireConfigFile: false,
ecmaFeatures: {
jsx: false,
},
},
extends: [
"@vue/typescript/recommended",

View File

@@ -563,12 +563,22 @@ details[open] summary .indicator {
.env-highlight {
@apply text-accentContrast;
&.env-found {
@apply bg-accentDark;
@apply hover:bg-accent;
&.request-variable-highlight {
@apply bg-amber-500;
@apply hover:bg-amber-600;
}
&.env-not-found {
&.environment-variable-highlight {
@apply bg-green-500;
@apply hover:bg-green-600;
}
&.global-variable-highlight {
@apply bg-blue-500;
@apply hover:bg-blue-600;
}
&.environment-not-found-highlight {
@apply bg-red-500;
@apply hover:bg-red-600;
}

View File

@@ -27,6 +27,7 @@
"hide_secret": "Hide secret",
"label": "Label",
"learn_more": "Learn more",
"download_here": "Download here",
"less": "Less",
"more": "More",
"new": "New",
@@ -103,8 +104,10 @@
"auth": {
"account_exists": "Account exists with different credential - Login to link both accounts",
"all_sign_in_options": "All sign in options",
"continue_with_auth_provider": "Continue with {provider}",
"continue_with_email": "Continue with Email",
"continue_with_github": "Continue with GitHub",
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
"continue_with_google": "Continue with Google",
"continue_with_microsoft": "Continue with Microsoft",
"email": "Email",
@@ -137,7 +140,26 @@
"redirect_no_token_endpoint": "No Token Endpoint Defined",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
"grant_type": "Grant Type",
"grant_type_auth_code": "Authorization Code",
"token_fetched_successfully": "Token fetched successfully",
"token_fetch_failed": "Failed to fetch token",
"validation_failed": "Validation Failed, please check the form fields",
"label_authorization_endpoint": "Authorization Endpoint",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",
"label_code_challenge": "Code Challenge",
"label_code_challenge_method": "Code Challenge Method",
"label_code_verifier": "Code Verifier",
"label_scopes": "Scopes",
"label_token_endpoint": "Token Endpoint",
"label_use_pkce": "Use PKCE",
"label_implicit": "Implicit",
"label_password": "Password",
"label_username": "Username",
"label_auth_code": "Authorization Code",
"label_client_credentials": "Client Credentials"
},
"pass_key_by": "Pass by",
"password": "Password",
@@ -154,7 +176,7 @@
"invalid_name": "Please provide a name for the collection",
"invalid_root_move": "Collection already in the root",
"moved": "Moved Successfully",
"my_collections": "My Collections",
"my_collections": "Personal Collections",
"name": "My New Collection",
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection",
@@ -166,14 +188,12 @@
"save_as": "Save as",
"save_to_collection": "Save to Collection",
"select": "Select a Collection",
"select_location": "Select location",
"select_team": "Select a team",
"team_collections": "Team Collections"
"select_location": "Select location"
},
"confirm": {
"close_unsaved_tab": "Are you sure you want to close this tab?",
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
"exit_team": "Are you sure you want to leave this team?",
"exit_team": "Are you sure you want to leave this workspace?",
"logout": "Are you sure you want to logout?",
"remove_collection": "Are you sure you want to permanently delete this collection?",
"remove_environment": "Are you sure you want to permanently delete this environment?",
@@ -181,7 +201,7 @@
"remove_history": "Are you sure you want to permanently delete all history?",
"remove_request": "Are you sure you want to permanently delete this request?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Are you sure you want to delete this team?",
"remove_team": "Are you sure you want to delete this workspace?",
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
"save_unsaved_tab": "Do you want to save changes made in this tab?",
@@ -234,18 +254,19 @@
"headers": "This request does not have any headers",
"history": "History is empty",
"invites": "Invite list is empty",
"members": "Team is empty",
"members": "Workspace is empty",
"parameters": "This request does not have any parameters",
"pending_invites": "There are no pending invites for this team",
"pending_invites": "There are no pending invites for this workspace",
"profile": "Login to view your profile",
"protocols": "Protocols are empty",
"request_variables": "This request does not have any request variables",
"schema": "Connect to a GraphQL endpoint to view schema",
"secret_environments": "Secrets are not synced to Hoppscotch",
"shared_requests": "Shared requests are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"subscription": "Subscriptions are empty",
"team_name": "Team name empty",
"teams": "You don't belong to any teams",
"team_name": "Workspace name empty",
"teams": "You don't belong to any workspaces",
"tests": "There are no tests for this request"
},
"environment": {
@@ -262,7 +283,7 @@
"import_or_create": "Import or create a environment",
"invalid_name": "Please provide a name for the environment",
"list": "Environment variables",
"my_environments": "My Environments",
"my_environments": "Personal Environments",
"name": "Name",
"nested_overflow": "nested environment variables are limited to 10 levels",
"new": "New Environment",
@@ -277,12 +298,12 @@
"select": "Select environment",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Team Environments",
"team_environments": "Workspace Environments",
"title": "Environments",
"updated": "Environment updated",
"value": "Value",
"variable": "Variable",
"variables":"Variables",
"variables": "Variables",
"variable_list": "Variable List"
},
"error": {
@@ -292,8 +313,9 @@
"check_how_to_add_origin": "Check how you can add an origin",
"curl_invalid_format": "cURL is not formatted properly",
"danger_zone": "Danger zone",
"delete_account": "Your account is currently an owner in these teams:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these teams before you can delete your account.",
"delete_account": "Your account is currently an owner in these workspaces:",
"delete_account_description": "You must either remove yourself, transfer ownership, or delete these workspaces before you can delete your account.",
"empty_profile_name": "Profile name cannot be empty",
"empty_req_name": "Empty Request Name",
"f12_details": "(F12 for details)",
"gql_prettify_invalid_query": "Couldn't prettify an invalid query, solve query syntax errors and try again",
@@ -313,6 +335,7 @@
"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",
"same_profile_name": "Updated profile name is same as the current profile name",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script",
@@ -395,8 +418,8 @@
"from_insomnia_description": "Import from Insomnia collection",
"from_json": "Import from Hoppscotch",
"from_json_description": "Import from Hoppscotch collection file",
"from_my_collections": "Import from My Collections",
"from_my_collections_description": "Import from My Collections file",
"from_my_collections": "Import from Personal Collections",
"from_my_collections_description": "Import from Personal Collections file",
"from_openapi": "Import from OpenAPI",
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
"from_postman": "Import from Postman",
@@ -428,7 +451,7 @@
"not_found": "Environment variable “{environment}” not found."
},
"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": "The browser doesn't allow Hoppscotch to set Cookie Headers. Please use Authorization Headers instead. However, our Hoppscotch Desktop App is live now and supports Cookies."
},
"response": {
"401_error": "Please check your authentication credentials.",
@@ -513,7 +536,7 @@
"email_verification_mail": "A verification email has been sent to your email address. Please click on the link to verify your email address.",
"no_permission": "You do not have permission to perform this action.",
"owner": "Owner",
"owner_description": "Owners can add, edit, and delete requests, collections and team members.",
"owner_description": "Owners can add, edit, and delete requests, collections and workspace members.",
"roles": "Roles",
"roles_description": "Roles are used to control access to the shared collections.",
"updated": "Profile updated",
@@ -560,6 +583,7 @@
"raw_body": "Raw Request Body",
"rename": "Rename Request",
"renamed": "Request renamed",
"request_variables": "Request variables",
"run": "Run",
"save": "Save",
"save_as": "Save as",
@@ -817,12 +841,12 @@
"title": "Tabs"
},
"workspace": {
"delete": "Delete current team",
"edit": "Edit current team",
"invite": "Invite people to team",
"new": "Create new team",
"delete": "Delete current workspace",
"edit": "Edit current workspace",
"invite": "Invite people to workspace",
"new": "Create new workspace",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
"title": "Workspaces"
}
},
"sse": {
@@ -879,7 +903,6 @@
"forum": "Ask questions and get answers",
"github": "Follow us on Github",
"shortcuts": "Browse app faster",
"team": "Get in touch with the team",
"title": "Support",
"twitter": "Follow us on Twitter"
},
@@ -910,60 +933,60 @@
"websocket": "WebSocket"
},
"team": {
"already_member": "You are already a member of this team. Contact your team owner.",
"create_new": "Create new team",
"deleted": "Team deleted",
"edit": "Edit Team",
"already_member": "You are already a member of this workspace. Contact your workspace owner.",
"create_new": "Create new workspace",
"deleted": "Workspace deleted",
"edit": "Edit Workspace",
"email": "E-mail",
"email_do_not_match": "Email doesn't match with your account details. Contact your team owner.",
"exit": "Exit Team",
"exit_disabled": "Only owner cannot exit the team",
"email_do_not_match": "Email doesn't match with your account details. Contact your workspace owner.",
"exit": "Exit Workspace",
"exit_disabled": "Only owner cannot exit the workspace",
"failed_invites": "Failed invites",
"invalid_coll_id": "Invalid collection ID",
"invalid_email_format": "Email format is invalid",
"invalid_id": "Invalid team ID. Contact your team owner.",
"invalid_id": "Invalid workspace ID. Contact your workspace owner.",
"invalid_invite_link": "Invalid invite link",
"invalid_invite_link_description": "The link you followed is invalid. Contact your team owner.",
"invalid_member_permission": "Please provide a valid permission to the team member",
"invalid_invite_link_description": "The link you followed is invalid. Contact your workspace owner.",
"invalid_member_permission": "Please provide a valid permission to the workspace member",
"invite": "Invite",
"invite_more": "Invite more",
"invite_tooltip": "Invite people to this workspace",
"invited_to_team": "{owner} invited you to join {team}",
"invited_to_team": "{owner} invited you to join {workspace}",
"join": "Invitation accepted",
"join_beta": "Join the beta program to access teams.",
"join_team": "Join {team}",
"joined_team": "You have joined {team}",
"joined_team_description": "You are now a member of this team",
"left": "You left the team",
"join_team": "Join {workspace}",
"joined_team": "You have joined {workspace}",
"joined_team_description": "You are now a member of this workspace",
"left": "You left the workspace",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to join a team.",
"login_to_continue_description": "You need to be logged in to join a workspace.",
"logout_and_try_again": "Logout and sign in with another account",
"member_has_invite": "This email ID already has an invite. Contact your team owner.",
"member_not_found": "Member not found. Contact your team owner.",
"member_has_invite": "This email ID already has an invite. Contact your workspace owner.",
"member_not_found": "Member not found. Contact your workspace owner.",
"member_removed": "User removed",
"member_role_updated": "User roles updated",
"members": "Members",
"more_members": "+{count} more",
"name_length_insufficient": "Team name should be at least 6 characters long",
"name_updated": "Team name updated",
"new": "New Team",
"new_created": "New team created",
"new_name": "My New Team",
"no_access": "You do not have edit access to this team",
"no_invite_found": "Invitation not found. Contact your team owner.",
"name_length_insufficient": "Workspace name should be at least 6 characters long",
"name_updated": "Workspace name updated",
"new": "New Workspace",
"new_created": "New workspace created",
"new_name": "My New Workspace",
"no_access": "You do not have edit access to this workspace",
"no_invite_found": "Invitation not found. Contact your workspace owner.",
"no_request_found": "Request not found.",
"not_found": "Team not found. Contact your team owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your team owner.",
"not_found": "Workspace not found. Contact your workspace owner.",
"not_valid_viewer": "You are not a valid viewer. Contact your workspace owner.",
"parent_coll_move": "Cannot move collection to a child collection",
"pending_invites": "Pending invites",
"permissions": "Permissions",
"same_target_destination": "Same target and destination",
"saved": "Team saved",
"select_a_team": "Select a team",
"saved": "Workspace saved",
"select_a_team": "Select a workspace",
"success_invites": "Success invites",
"title": "Teams",
"title": "Workspaces",
"we_sent_invite_link": "We sent an invite link to all invitees!",
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the team."
"we_sent_invite_link_description": "Ask all invitees to check their inbox. Click on the link to join the workspace.",
"search_title": "Team Requests"
},
"team_environment": {
"deleted": "Environment Deleted",
@@ -989,8 +1012,14 @@
},
"workspace": {
"change": "Change workspace",
"personal": "My Workspace",
"team": "Team Workspace",
"personal": "Personal Workspace",
"other_workspaces": "My Workspaces",
"team": "Workspace",
"title": "Workspaces"
},
"site_protection": {
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance.",
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "@hoppscotch/common",
"private": true,
"version": "2023.12.6",
"version": "2024.3.0",
"scripts": {
"dev": "pnpm exec npm-run-all -p -l dev:*",
"test": "vitest --run",
@@ -21,147 +21,147 @@
"do-lintfix": "pnpm run lintfix"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.11.1",
"@codemirror/commands": "^6.3.2",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "6.9.3",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@codemirror/state": "^6.3.3",
"@codemirror/view": "^6.22.3",
"@apidevtools/swagger-parser": "10.1.0",
"@codemirror/autocomplete": "6.13.0",
"@codemirror/commands": "6.3.3",
"@codemirror/lang-javascript": "6.2.2",
"@codemirror/lang-json": "6.0.1",
"@codemirror/lang-xml": "6.1.0",
"@codemirror/language": "6.10.1",
"@codemirror/legacy-modes": "6.3.3",
"@codemirror/lint": "6.5.0",
"@codemirror/search": "6.5.6",
"@codemirror/state": "6.4.1",
"@codemirror/view": "6.25.1",
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
"@hoppscotch/data": "workspace:^",
"@hoppscotch/js-sandbox": "workspace:^",
"@hoppscotch/ui": "^0.1.0",
"@hoppscotch/vue-toasted": "^0.1.0",
"@hoppscotch/ui": "0.1.0",
"@hoppscotch/vue-toasted": "0.1.0",
"@lezer/highlight": "1.2.0",
"@unhead/vue": "^1.8.8",
"@urql/core": "^4.2.0",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
"@urql/exchange-graphcache": "^6.3.3",
"@vitejs/plugin-legacy": "^4.1.1",
"@vueuse/core": "^10.6.1",
"acorn-walk": "^8.3.0",
"axios": "^1.6.2",
"buffer": "^6.0.3",
"cookie-es": "^1.0.0",
"dioc": "^1.0.1",
"esprima": "^4.0.1",
"events": "^3.3.0",
"fp-ts": "^2.16.1",
"globalthis": "^1.0.3",
"graphql": "^16.8.1",
"graphql-language-service-interface": "^2.10.2",
"graphql-tag": "^2.12.6",
"httpsnippet": "^3.0.1",
"insomnia-importers": "^3.6.0",
"io-ts": "^2.2.20",
"js-yaml": "^4.1.0",
"jsonpath-plus": "^7.2.0",
"lodash-es": "^4.17.21",
"lossless-json": "^3.0.2",
"minisearch": "^6.3.0",
"nprogress": "^0.2.0",
"paho-mqtt": "^1.1.0",
"path": "^0.12.7",
"postman-collection": "^4.3.0",
"process": "^0.11.10",
"qs": "^6.11.2",
"quicktype-core": "^23.0.79",
"rxjs": "^7.8.1",
"set-cookie-parser": "^2.6.0",
"set-cookie-parser-es": "^1.0.5",
"socket.io-client-v2": "npm:socket.io-client@^2.4.0",
"socket.io-client-v3": "npm:socket.io-client@^3.1.3",
"socket.io-client-v4": "npm:socket.io-client@^4.4.1",
"socketio-wildcard": "^2.0.0",
"splitpanes": "^3.1.5",
"stream-browserify": "^3.0.0",
"subscriptions-transport-ws": "^0.11.0",
"tern": "^0.24.3",
"timers": "^0.1.1",
"tippy.js": "^6.3.7",
"url": "^0.11.3",
"util": "^0.12.5",
"uuid": "^9.0.1",
"verzod": "^0.2.0",
"vue": "^3.3.8",
"vue-i18n": "^9.7.1",
"vue-pdf-embed": "^1.2.1",
"vue-router": "^4.2.5",
"@unhead/vue": "1.8.8",
"@urql/core": "4.2.0",
"@urql/devtools": "2.0.3",
"@urql/exchange-auth": "2.1.6",
"@urql/exchange-graphcache": "6.4.0",
"@vitejs/plugin-legacy": "4.1.1",
"@vueuse/core": "10.7.0",
"acorn-walk": "8.3.0",
"axios": "1.6.2",
"buffer": "6.0.3",
"cookie-es": "1.0.0",
"dioc": "1.0.1",
"esprima": "4.0.1",
"events": "3.3.0",
"fp-ts": "2.16.1",
"globalthis": "1.0.3",
"graphql": "16.8.1",
"graphql-language-service-interface": "2.10.2",
"graphql-tag": "2.12.6",
"httpsnippet": "3.0.1",
"insomnia-importers": "3.6.0",
"io-ts": "2.2.20",
"js-yaml": "4.1.0",
"jsonpath-plus": "7.2.0",
"lodash-es": "4.17.21",
"lossless-json": "3.0.2",
"minisearch": "6.3.0",
"nprogress": "0.2.0",
"paho-mqtt": "1.1.0",
"path": "0.12.7",
"postman-collection": "4.3.0",
"process": "0.11.10",
"qs": "6.11.2",
"quicktype-core": "23.0.79",
"rxjs": "7.8.1",
"set-cookie-parser": "2.6.0",
"set-cookie-parser-es": "1.0.5",
"socket.io-client-v2": "npm:socket.io-client@2.4.0",
"socket.io-client-v3": "npm:socket.io-client@3.1.3",
"socket.io-client-v4": "npm:socket.io-client@4.4.1",
"socketio-wildcard": "2.0.0",
"splitpanes": "3.1.5",
"stream-browserify": "3.0.0",
"subscriptions-transport-ws": "0.11.0",
"tern": "0.24.3",
"timers": "0.1.1",
"tippy.js": "6.3.7",
"url": "0.11.3",
"util": "0.12.5",
"uuid": "9.0.1",
"verzod": "0.2.2",
"vue": "3.3.9",
"vue-i18n": "9.8.0",
"vue-pdf-embed": "1.2.1",
"vue-router": "4.2.5",
"vue-tippy": "6.3.1",
"vuedraggable-es": "^4.1.1",
"wonka": "^6.3.4",
"workbox-window": "^7.0.0",
"xml-formatter": "^3.6.0",
"yargs-parser": "^21.1.1",
"zod": "^3.22.4"
"vuedraggable-es": "4.1.1",
"wonka": "6.3.4",
"workbox-window": "7.0.0",
"xml-formatter": "3.6.0",
"yargs-parser": "21.1.1",
"zod": "3.22.4"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@esbuild-plugins/node-modules-polyfill": "^0.2.2",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
"@graphql-codegen/urql-introspection": "^3.0.0",
"@graphql-typed-document-node/core": "^3.2.0",
"@iconify-json/lucide": "^1.1.141",
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
"@relmify/jest-fp-ts": "^2.1.1",
"@rushstack/eslint-patch": "^1.6.0",
"@types/har-format": "^1.2.15",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/lossless-json": "^1.0.4",
"@types/nprogress": "^0.2.3",
"@types/paho-mqtt": "^1.0.10",
"@types/postman-collection": "^3.5.10",
"@types/splitpanes": "^2.2.6",
"@types/uuid": "^9.0.7",
"@types/yargs-parser": "^21.0.3",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/compiler-sfc": "^3.3.8",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/runtime-core": "^3.3.8",
"autoprefixer": "^10.4.14",
"cross-env": "^7.0.3",
"dotenv": "^16.3.1",
"eslint": "^8.54.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-vue": "^9.18.1",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5",
"openapi-types": "^12.1.3",
"postcss": "^8.4.23",
"prettier": "^3.1.0",
"prettier-plugin-tailwindcss": "^0.5.7",
"rollup-plugin-polyfill-node": "^0.13.0",
"sass": "^1.69.5",
"tailwindcss": "^3.3.2",
"typescript": "^5.3.2",
"unplugin-fonts": "^1.1.1",
"unplugin-icons": "^0.17.4",
"unplugin-vue-components": "^0.25.2",
"vite": "^4.5.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-fonts": "^0.7.0",
"vite-plugin-html-config": "^1.0.11",
"vite-plugin-inspect": "^0.7.42",
"vite-plugin-pages": "^0.31.0",
"vite-plugin-pages-sitemap": "^1.6.1",
"vite-plugin-pwa": "^0.17.0",
"vite-plugin-vue-layouts": "^0.8.0",
"vitest": "^0.34.6",
"vue-tsc": "^1.8.22"
"@esbuild-plugins/node-globals-polyfill": "0.2.3",
"@esbuild-plugins/node-modules-polyfill": "0.2.2",
"@graphql-codegen/add": "5.0.0",
"@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typed-document-node": "5.0.1",
"@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql-graphcache": "3.0.0",
"@graphql-codegen/urql-introspection": "3.0.0",
"@graphql-typed-document-node/core": "3.2.0",
"@iconify-json/lucide": "1.1.144",
"@intlify/vite-plugin-vue-i18n": "7.0.0",
"@relmify/jest-fp-ts": "2.1.1",
"@rushstack/eslint-patch": "1.6.0",
"@types/har-format": "1.2.15",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "4.17.12",
"@types/lossless-json": "1.0.4",
"@types/nprogress": "0.2.3",
"@types/paho-mqtt": "1.0.10",
"@types/postman-collection": "3.5.10",
"@types/splitpanes": "2.2.6",
"@types/uuid": "9.0.7",
"@types/yargs-parser": "21.0.3",
"@typescript-eslint/eslint-plugin": "7.3.1",
"@typescript-eslint/parser": "7.3.1",
"@vitejs/plugin-vue": "4.5.1",
"@vue/compiler-sfc": "3.3.10",
"@vue/eslint-config-typescript": "12.0.0",
"@vue/runtime-core": "3.3.10",
"autoprefixer": "10.4.16",
"cross-env": "7.0.3",
"dotenv": "16.3.1",
"eslint": "8.57.0",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-vue": "9.24.0",
"glob": "10.3.10",
"npm-run-all": "4.1.5",
"openapi-types": "12.1.3",
"postcss": "8.4.31",
"prettier": "3.1.0",
"prettier-plugin-tailwindcss": "0.5.7",
"rollup-plugin-polyfill-node": "0.13.0",
"sass": "1.69.5",
"tailwindcss": "3.3.5",
"typescript": "5.3.2",
"unplugin-fonts": "1.1.1",
"unplugin-icons": "0.17.4",
"unplugin-vue-components": "0.25.2",
"vite": "4.5.0",
"vite-plugin-checker": "0.6.2",
"vite-plugin-fonts": "0.7.0",
"vite-plugin-html-config": "1.0.11",
"vite-plugin-inspect": "0.7.42",
"vite-plugin-pages": "0.31.0",
"vite-plugin-pages-sitemap": "1.6.1",
"vite-plugin-pwa": "0.17.3",
"vite-plugin-vue-layouts": "0.8.0",
"vitest": "0.34.6",
"vue-tsc": "1.8.24"
}
}

View File

@@ -1,213 +1,216 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
import "@vue/runtime-core"
export {}
declare module '@vue/runtime-core' {
declare module "@vue/runtime-core" {
export interface GlobalComponents {
AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default']
AppBanner: typeof import('./components/app/Banner.vue')['default']
AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default']
AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default']
AppFooter: typeof import('./components/app/Footer.vue')['default']
AppGitHubStarButton: typeof import('./components/app/GitHubStarButton.vue')['default']
AppHeader: typeof import('./components/app/Header.vue')['default']
AppInspection: typeof import('./components/app/Inspection.vue')['default']
AppInterceptor: typeof import('./components/app/Interceptor.vue')['default']
AppLogo: typeof import('./components/app/Logo.vue')['default']
AppOptions: typeof import('./components/app/Options.vue')['default']
AppPaneLayout: typeof import('./components/app/PaneLayout.vue')['default']
AppShare: typeof import('./components/app/Share.vue')['default']
AppShortcuts: typeof import('./components/app/Shortcuts.vue')['default']
AppShortcutsEntry: typeof import('./components/app/ShortcutsEntry.vue')['default']
AppShortcutsPrompt: typeof import('./components/app/ShortcutsPrompt.vue')['default']
AppSidenav: typeof import('./components/app/Sidenav.vue')['default']
AppSpotlight: typeof import('./components/app/spotlight/index.vue')['default']
AppSpotlightEntry: typeof import('./components/app/spotlight/Entry.vue')['default']
AppSpotlightEntryGQLHistory: typeof import('./components/app/spotlight/entry/GQLHistory.vue')['default']
AppSpotlightEntryGQLRequest: typeof import('./components/app/spotlight/entry/GQLRequest.vue')['default']
AppSpotlightEntryIconSelected: typeof import('./components/app/spotlight/entry/IconSelected.vue')['default']
AppSpotlightEntryRESTHistory: typeof import('./components/app/spotlight/entry/RESTHistory.vue')['default']
AppSpotlightEntryRESTRequest: typeof import('./components/app/spotlight/entry/RESTRequest.vue')['default']
AppSupport: typeof import('./components/app/Support.vue')['default']
Collections: typeof import('./components/collections/index.vue')['default']
CollectionsAdd: typeof import('./components/collections/Add.vue')['default']
CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default']
CollectionsAddRequest: typeof import('./components/collections/AddRequest.vue')['default']
CollectionsCollection: typeof import('./components/collections/Collection.vue')['default']
CollectionsEdit: typeof import('./components/collections/Edit.vue')['default']
CollectionsEditFolder: typeof import('./components/collections/EditFolder.vue')['default']
CollectionsEditRequest: typeof import('./components/collections/EditRequest.vue')['default']
CollectionsGraphql: typeof import('./components/collections/graphql/index.vue')['default']
CollectionsGraphqlAdd: typeof import('./components/collections/graphql/Add.vue')['default']
CollectionsGraphqlAddFolder: typeof import('./components/collections/graphql/AddFolder.vue')['default']
CollectionsGraphqlAddRequest: typeof import('./components/collections/graphql/AddRequest.vue')['default']
CollectionsGraphqlCollection: typeof import('./components/collections/graphql/Collection.vue')['default']
CollectionsGraphqlEdit: typeof import('./components/collections/graphql/Edit.vue')['default']
CollectionsGraphqlEditFolder: typeof import('./components/collections/graphql/EditFolder.vue')['default']
CollectionsGraphqlEditRequest: typeof import('./components/collections/graphql/EditRequest.vue')['default']
CollectionsGraphqlFolder: typeof import('./components/collections/graphql/Folder.vue')['default']
CollectionsGraphqlImportExport: typeof import('./components/collections/graphql/ImportExport.vue')['default']
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
EnvironmentsMy: typeof import('./components/environments/my/index.vue')['default']
EnvironmentsMyDetails: typeof import('./components/environments/my/Details.vue')['default']
EnvironmentsMyEnvironment: typeof import('./components/environments/my/Environment.vue')['default']
EnvironmentsSelector: typeof import('./components/environments/Selector.vue')['default']
EnvironmentsTeams: typeof import('./components/environments/teams/index.vue')['default']
EnvironmentsTeamsDetails: typeof import('./components/environments/teams/Details.vue')['default']
EnvironmentsTeamsEnvironment: typeof import('./components/environments/teams/Environment.vue')['default']
FirebaseLogin: typeof import('./components/firebase/Login.vue')['default']
FirebaseLogout: typeof import('./components/firebase/Logout.vue')['default']
GraphqlAuthorization: typeof import('./components/graphql/Authorization.vue')['default']
GraphqlField: typeof import('./components/graphql/Field.vue')['default']
GraphqlHeaders: typeof import('./components/graphql/Headers.vue')['default']
GraphqlQuery: typeof import('./components/graphql/Query.vue')['default']
GraphqlRequest: typeof import('./components/graphql/Request.vue')['default']
GraphqlRequestOptions: typeof import('./components/graphql/RequestOptions.vue')['default']
GraphqlRequestTab: typeof import('./components/graphql/RequestTab.vue')['default']
GraphqlResponse: typeof import('./components/graphql/Response.vue')['default']
GraphqlSidebar: typeof import('./components/graphql/Sidebar.vue')['default']
GraphqlSubscriptionLog: typeof import('./components/graphql/SubscriptionLog.vue')['default']
GraphqlTabHead: typeof import('./components/graphql/TabHead.vue')['default']
GraphqlType: typeof import('./components/graphql/Type.vue')['default']
GraphqlTypeLink: typeof import('./components/graphql/TypeLink.vue')['default']
GraphqlVariable: typeof import('./components/graphql/Variable.vue')['default']
History: typeof import('./components/history/index.vue')['default']
HistoryGraphqlCard: typeof import('./components/history/graphql/Card.vue')['default']
HistoryRestCard: typeof import('./components/history/rest/Card.vue')['default']
HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary']
HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary']
HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor']
HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox']
HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal']
HoppSmartExpand: typeof import('@hoppscotch/ui')['HoppSmartExpand']
HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip']
HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput']
HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection']
HoppSmartItem: typeof import('@hoppscotch/ui')['HoppSmartItem']
HoppSmartLink: typeof import('@hoppscotch/ui')['HoppSmartLink']
HoppSmartModal: typeof import('@hoppscotch/ui')['HoppSmartModal']
HoppSmartPicture: typeof import('@hoppscotch/ui')['HoppSmartPicture']
HoppSmartPlaceholder: typeof import('@hoppscotch/ui')['HoppSmartPlaceholder']
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
HoppSmartTabs: typeof import('@hoppscotch/ui')['HoppSmartTabs']
HoppSmartToggle: typeof import('@hoppscotch/ui')['HoppSmartToggle']
HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree']
HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow']
HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows']
HttpAuthorization: typeof import('./components/http/Authorization.vue')['default']
HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default']
HttpAuthorizationBasic: typeof import('./components/http/authorization/Basic.vue')['default']
HttpBody: typeof import('./components/http/Body.vue')['default']
HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default']
HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default']
HttpHeaders: typeof import('./components/http/Headers.vue')['default']
HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default']
HttpOAuth2Authorization: typeof import('./components/http/OAuth2Authorization.vue')['default']
HttpParameters: typeof import('./components/http/Parameters.vue')['default']
HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default']
HttpRawBody: typeof import('./components/http/RawBody.vue')['default']
HttpReqChangeConfirmModal: typeof import('./components/http/ReqChangeConfirmModal.vue')['default']
HttpRequest: typeof import('./components/http/Request.vue')['default']
HttpRequestOptions: typeof import('./components/http/RequestOptions.vue')['default']
HttpRequestTab: typeof import('./components/http/RequestTab.vue')['default']
HttpResponse: typeof import('./components/http/Response.vue')['default']
HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default']
HttpSidebar: typeof import('./components/http/Sidebar.vue')['default']
HttpTabHead: typeof import('./components/http/TabHead.vue')['default']
HttpTestResult: typeof import('./components/http/TestResult.vue')['default']
HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default']
HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default']
HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default']
HttpTests: typeof import('./components/http/Tests.vue')['default']
HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default']
IconLucideActivity: typeof import('~icons/lucide/activity')['default']
IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default']
IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default']
IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default']
IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default']
IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default']
IconLucideGlobe: typeof import('~icons/lucide/globe')['default']
IconLucideHelpCircle: typeof import('~icons/lucide/help-circle')['default']
IconLucideInbox: typeof import('~icons/lucide/inbox')['default']
IconLucideInfo: typeof import('~icons/lucide/info')['default']
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
IconLucideListEnd: typeof import('~icons/lucide/list-end')['default']
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
IconLucideSearch: typeof import('~icons/lucide/search')['default']
IconLucideUsers: typeof import('~icons/lucide/users')['default']
IconLucideX: typeof import('~icons/lucide/x')['default']
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
LensesHeadersRendererEntry: typeof import('./components/lenses/HeadersRendererEntry.vue')['default']
LensesRenderersAudioLensRenderer: typeof import('./components/lenses/renderers/AudioLensRenderer.vue')['default']
LensesRenderersHTMLLensRenderer: typeof import('./components/lenses/renderers/HTMLLensRenderer.vue')['default']
LensesRenderersImageLensRenderer: typeof import('./components/lenses/renderers/ImageLensRenderer.vue')['default']
LensesRenderersJSONLensRenderer: typeof import('./components/lenses/renderers/JSONLensRenderer.vue')['default']
LensesRenderersPDFLensRenderer: typeof import('./components/lenses/renderers/PDFLensRenderer.vue')['default']
LensesRenderersRawLensRenderer: typeof import('./components/lenses/renderers/RawLensRenderer.vue')['default']
LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default']
LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default']
LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default']
ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
Share: typeof import('./components/share/index.vue')['default']
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
ShareModal: typeof import('./components/share/Modal.vue')['default']
ShareRequest: typeof import('./components/share/Request.vue')['default']
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default']
SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default']
SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
Teams: typeof import('./components/teams/index.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsEdit: typeof import('./components/teams/Edit.vue')['default']
TeamsInvite: typeof import('./components/teams/Invite.vue')['default']
TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
TeamsModal: typeof import('./components/teams/Modal.vue')['default']
TeamsTeam: typeof import('./components/teams/Team.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy']
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']
AppActionHandler: (typeof import("./components/app/ActionHandler.vue"))["default"]
AppBanner: (typeof import("./components/app/Banner.vue"))["default"]
AppContextMenu: (typeof import("./components/app/ContextMenu.vue"))["default"]
AppDeveloperOptions: (typeof import("./components/app/DeveloperOptions.vue"))["default"]
AppFooter: (typeof import("./components/app/Footer.vue"))["default"]
AppGitHubStarButton: (typeof import("./components/app/GitHubStarButton.vue"))["default"]
AppHeader: (typeof import("./components/app/Header.vue"))["default"]
AppInspection: (typeof import("./components/app/Inspection.vue"))["default"]
AppInterceptor: (typeof import("./components/app/Interceptor.vue"))["default"]
AppLogo: (typeof import("./components/app/Logo.vue"))["default"]
AppOptions: (typeof import("./components/app/Options.vue"))["default"]
AppPaneLayout: (typeof import("./components/app/PaneLayout.vue"))["default"]
AppShare: (typeof import("./components/app/Share.vue"))["default"]
AppShortcuts: (typeof import("./components/app/Shortcuts.vue"))["default"]
AppShortcutsEntry: (typeof import("./components/app/ShortcutsEntry.vue"))["default"]
AppShortcutsPrompt: (typeof import("./components/app/ShortcutsPrompt.vue"))["default"]
AppSidenav: (typeof import("./components/app/Sidenav.vue"))["default"]
AppSpotlight: (typeof import("./components/app/spotlight/index.vue"))["default"]
AppSpotlightEntry: (typeof import("./components/app/spotlight/Entry.vue"))["default"]
AppSpotlightEntryGQLHistory: (typeof import("./components/app/spotlight/entry/GQLHistory.vue"))["default"]
AppSpotlightEntryGQLRequest: (typeof import("./components/app/spotlight/entry/GQLRequest.vue"))["default"]
AppSpotlightEntryIconSelected: (typeof import("./components/app/spotlight/entry/IconSelected.vue"))["default"]
AppSpotlightEntryRESTHistory: (typeof import("./components/app/spotlight/entry/RESTHistory.vue"))["default"]
AppSpotlightEntryRESTRequest: (typeof import("./components/app/spotlight/entry/RESTRequest.vue"))["default"]
AppSupport: (typeof import("./components/app/Support.vue"))["default"]
Collections: (typeof import("./components/collections/index.vue"))["default"]
CollectionsAdd: (typeof import("./components/collections/Add.vue"))["default"]
CollectionsAddFolder: (typeof import("./components/collections/AddFolder.vue"))["default"]
CollectionsAddRequest: (typeof import("./components/collections/AddRequest.vue"))["default"]
CollectionsCollection: (typeof import("./components/collections/Collection.vue"))["default"]
CollectionsEdit: (typeof import("./components/collections/Edit.vue"))["default"]
CollectionsEditFolder: (typeof import("./components/collections/EditFolder.vue"))["default"]
CollectionsEditRequest: (typeof import("./components/collections/EditRequest.vue"))["default"]
CollectionsGraphql: (typeof import("./components/collections/graphql/index.vue"))["default"]
CollectionsGraphqlAdd: (typeof import("./components/collections/graphql/Add.vue"))["default"]
CollectionsGraphqlAddFolder: (typeof import("./components/collections/graphql/AddFolder.vue"))["default"]
CollectionsGraphqlAddRequest: (typeof import("./components/collections/graphql/AddRequest.vue"))["default"]
CollectionsGraphqlCollection: (typeof import("./components/collections/graphql/Collection.vue"))["default"]
CollectionsGraphqlEdit: (typeof import("./components/collections/graphql/Edit.vue"))["default"]
CollectionsGraphqlEditFolder: (typeof import("./components/collections/graphql/EditFolder.vue"))["default"]
CollectionsGraphqlEditRequest: (typeof import("./components/collections/graphql/EditRequest.vue"))["default"]
CollectionsGraphqlFolder: (typeof import("./components/collections/graphql/Folder.vue"))["default"]
CollectionsGraphqlImportExport: (typeof import("./components/collections/graphql/ImportExport.vue"))["default"]
CollectionsGraphqlRequest: (typeof import("./components/collections/graphql/Request.vue"))["default"]
CollectionsImportExport: (typeof import("./components/collections/ImportExport.vue"))["default"]
CollectionsMyCollections: (typeof import("./components/collections/MyCollections.vue"))["default"]
CollectionsProperties: (typeof import("./components/collections/Properties.vue"))["default"]
CollectionsRequest: (typeof import("./components/collections/Request.vue"))["default"]
CollectionsSaveRequest: (typeof import("./components/collections/SaveRequest.vue"))["default"]
CollectionsTeamCollections: (typeof import("./components/collections/TeamCollections.vue"))["default"]
CookiesAllModal: (typeof import("./components/cookies/AllModal.vue"))["default"]
CookiesEditCookie: (typeof import("./components/cookies/EditCookie.vue"))["default"]
Embeds: (typeof import("./components/embeds/index.vue"))["default"]
Environments: (typeof import("./components/environments/index.vue"))["default"]
EnvironmentsAdd: (typeof import("./components/environments/Add.vue"))["default"]
EnvironmentsImportExport: (typeof import("./components/environments/ImportExport.vue"))["default"]
EnvironmentsMy: (typeof import("./components/environments/my/index.vue"))["default"]
EnvironmentsMyDetails: (typeof import("./components/environments/my/Details.vue"))["default"]
EnvironmentsMyEnvironment: (typeof import("./components/environments/my/Environment.vue"))["default"]
EnvironmentsSelector: (typeof import("./components/environments/Selector.vue"))["default"]
EnvironmentsTeams: (typeof import("./components/environments/teams/index.vue"))["default"]
EnvironmentsTeamsDetails: (typeof import("./components/environments/teams/Details.vue"))["default"]
EnvironmentsTeamsEnvironment: (typeof import("./components/environments/teams/Environment.vue"))["default"]
FirebaseLogin: (typeof import("./components/firebase/Login.vue"))["default"]
FirebaseLogout: (typeof import("./components/firebase/Logout.vue"))["default"]
GraphqlAuthorization: (typeof import("./components/graphql/Authorization.vue"))["default"]
GraphqlField: (typeof import("./components/graphql/Field.vue"))["default"]
GraphqlHeaders: (typeof import("./components/graphql/Headers.vue"))["default"]
GraphqlQuery: (typeof import("./components/graphql/Query.vue"))["default"]
GraphqlRequest: (typeof import("./components/graphql/Request.vue"))["default"]
GraphqlRequestOptions: (typeof import("./components/graphql/RequestOptions.vue"))["default"]
GraphqlRequestTab: (typeof import("./components/graphql/RequestTab.vue"))["default"]
GraphqlResponse: (typeof import("./components/graphql/Response.vue"))["default"]
GraphqlSidebar: (typeof import("./components/graphql/Sidebar.vue"))["default"]
GraphqlSubscriptionLog: (typeof import("./components/graphql/SubscriptionLog.vue"))["default"]
GraphqlTabHead: (typeof import("./components/graphql/TabHead.vue"))["default"]
GraphqlType: (typeof import("./components/graphql/Type.vue"))["default"]
GraphqlTypeLink: (typeof import("./components/graphql/TypeLink.vue"))["default"]
GraphqlVariable: (typeof import("./components/graphql/Variable.vue"))["default"]
History: (typeof import("./components/history/index.vue"))["default"]
HistoryGraphqlCard: (typeof import("./components/history/graphql/Card.vue"))["default"]
HistoryRestCard: (typeof import("./components/history/rest/Card.vue"))["default"]
HoppButtonPrimary: (typeof import("@hoppscotch/ui"))["HoppButtonPrimary"]
HoppButtonSecondary: (typeof import("@hoppscotch/ui"))["HoppButtonSecondary"]
HoppSmartAnchor: (typeof import("@hoppscotch/ui"))["HoppSmartAnchor"]
HoppSmartCheckbox: (typeof import("@hoppscotch/ui"))["HoppSmartCheckbox"]
HoppSmartConfirmModal: (typeof import("@hoppscotch/ui"))["HoppSmartConfirmModal"]
HoppSmartExpand: (typeof import("@hoppscotch/ui"))["HoppSmartExpand"]
HoppSmartFileChip: (typeof import("@hoppscotch/ui"))["HoppSmartFileChip"]
HoppSmartInput: (typeof import("@hoppscotch/ui"))["HoppSmartInput"]
HoppSmartIntersection: (typeof import("@hoppscotch/ui"))["HoppSmartIntersection"]
HoppSmartItem: (typeof import("@hoppscotch/ui"))["HoppSmartItem"]
HoppSmartLink: (typeof import("@hoppscotch/ui"))["HoppSmartLink"]
HoppSmartModal: (typeof import("@hoppscotch/ui"))["HoppSmartModal"]
HoppSmartPicture: (typeof import("@hoppscotch/ui"))["HoppSmartPicture"]
HoppSmartPlaceholder: (typeof import("@hoppscotch/ui"))["HoppSmartPlaceholder"]
HoppSmartProgressRing: (typeof import("@hoppscotch/ui"))["HoppSmartProgressRing"]
HoppSmartRadio: (typeof import("@hoppscotch/ui"))["HoppSmartRadio"]
HoppSmartRadioGroup: (typeof import("@hoppscotch/ui"))["HoppSmartRadioGroup"]
HoppSmartSelectWrapper: (typeof import("@hoppscotch/ui"))["HoppSmartSelectWrapper"]
HoppSmartSlideOver: (typeof import("@hoppscotch/ui"))["HoppSmartSlideOver"]
HoppSmartSpinner: (typeof import("@hoppscotch/ui"))["HoppSmartSpinner"]
HoppSmartTab: (typeof import("@hoppscotch/ui"))["HoppSmartTab"]
HoppSmartTabs: (typeof import("@hoppscotch/ui"))["HoppSmartTabs"]
HoppSmartToggle: (typeof import("@hoppscotch/ui"))["HoppSmartToggle"]
HoppSmartTree: (typeof import("@hoppscotch/ui"))["HoppSmartTree"]
HoppSmartWindow: (typeof import("@hoppscotch/ui"))["HoppSmartWindow"]
HoppSmartWindows: (typeof import("@hoppscotch/ui"))["HoppSmartWindows"]
HttpAuthorization: (typeof import("./components/http/Authorization.vue"))["default"]
HttpAuthorizationApiKey: (typeof import("./components/http/authorization/ApiKey.vue"))["default"]
HttpAuthorizationBasic: (typeof import("./components/http/authorization/Basic.vue"))["default"]
HttpBody: (typeof import("./components/http/Body.vue"))["default"]
HttpBodyParameters: (typeof import("./components/http/BodyParameters.vue"))["default"]
HttpCodegenModal: (typeof import("./components/http/CodegenModal.vue"))["default"]
HttpHeaders: (typeof import("./components/http/Headers.vue"))["default"]
HttpImportCurl: (typeof import("./components/http/ImportCurl.vue"))["default"]
HttpOAuth2Authorization: (typeof import("./components/http/OAuth2Authorization.vue"))["default"]
HttpParameters: (typeof import("./components/http/Parameters.vue"))["default"]
HttpPreRequestScript: (typeof import("./components/http/PreRequestScript.vue"))["default"]
HttpRawBody: (typeof import("./components/http/RawBody.vue"))["default"]
HttpReqChangeConfirmModal: (typeof import("./components/http/ReqChangeConfirmModal.vue"))["default"]
HttpRequest: (typeof import("./components/http/Request.vue"))["default"]
HttpRequestOptions: (typeof import("./components/http/RequestOptions.vue"))["default"]
HttpRequestTab: (typeof import("./components/http/RequestTab.vue"))["default"]
HttpResponse: (typeof import("./components/http/Response.vue"))["default"]
HttpResponseMeta: (typeof import("./components/http/ResponseMeta.vue"))["default"]
HttpSidebar: (typeof import("./components/http/Sidebar.vue"))["default"]
HttpTabHead: (typeof import("./components/http/TabHead.vue"))["default"]
HttpTestResult: (typeof import("./components/http/TestResult.vue"))["default"]
HttpTestResultEntry: (typeof import("./components/http/TestResultEntry.vue"))["default"]
HttpTestResultEnv: (typeof import("./components/http/TestResultEnv.vue"))["default"]
HttpTestResultReport: (typeof import("./components/http/TestResultReport.vue"))["default"]
HttpTests: (typeof import("./components/http/Tests.vue"))["default"]
HttpURLEncodedParams: (typeof import("./components/http/URLEncodedParams.vue"))["default"]
IconLucideActivity: (typeof import("~icons/lucide/activity"))["default"]
IconLucideAlertTriangle: (typeof import("~icons/lucide/alert-triangle"))["default"]
IconLucideArrowLeft: (typeof import("~icons/lucide/arrow-left"))["default"]
IconLucideArrowUpRight: (typeof import("~icons/lucide/arrow-up-right"))["default"]
IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"]
IconLucideCheckCircle: (typeof import("~icons/lucide/check-circle"))["default"]
IconLucideChevronRight: (typeof import("~icons/lucide/chevron-right"))["default"]
IconLucideGlobe: (typeof import("~icons/lucide/globe"))["default"]
IconLucideHelpCircle: (typeof import("~icons/lucide/help-circle"))["default"]
IconLucideInbox: (typeof import("~icons/lucide/inbox"))["default"]
IconLucideInfo: (typeof import("~icons/lucide/info"))["default"]
IconLucideLayers: (typeof import("~icons/lucide/layers"))["default"]
IconLucideListEnd: (typeof import("~icons/lucide/list-end"))["default"]
IconLucideMinus: (typeof import("~icons/lucide/minus"))["default"]
IconLucideRss: (typeof import("~icons/lucide/rss"))["default"]
IconLucideSearch: (typeof import("~icons/lucide/search"))["default"]
IconLucideUsers: (typeof import("~icons/lucide/users"))["default"]
IconLucideX: (typeof import("~icons/lucide/x"))["default"]
ImportExportBase: (typeof import("./components/importExport/Base.vue"))["default"]
ImportExportImportExportList: (typeof import("./components/importExport/ImportExportList.vue"))["default"]
ImportExportImportExportSourcesList: (typeof import("./components/importExport/ImportExportSourcesList.vue"))["default"]
ImportExportImportExportStepsFileImport: (typeof import("./components/importExport/ImportExportSteps/FileImport.vue"))["default"]
ImportExportImportExportStepsMyCollectionImport: (typeof import("./components/importExport/ImportExportSteps/MyCollectionImport.vue"))["default"]
ImportExportImportExportStepsUrlImport: (typeof import("./components/importExport/ImportExportSteps/UrlImport.vue"))["default"]
InterceptorsErrorPlaceholder: (typeof import("./components/interceptors/ErrorPlaceholder.vue"))["default"]
InterceptorsExtensionSubtitle: (typeof import("./components/interceptors/ExtensionSubtitle.vue"))["default"]
LensesHeadersRenderer: (typeof import("./components/lenses/HeadersRenderer.vue"))["default"]
LensesHeadersRendererEntry: (typeof import("./components/lenses/HeadersRendererEntry.vue"))["default"]
LensesRenderersAudioLensRenderer: (typeof import("./components/lenses/renderers/AudioLensRenderer.vue"))["default"]
LensesRenderersHTMLLensRenderer: (typeof import("./components/lenses/renderers/HTMLLensRenderer.vue"))["default"]
LensesRenderersImageLensRenderer: (typeof import("./components/lenses/renderers/ImageLensRenderer.vue"))["default"]
LensesRenderersJSONLensRenderer: (typeof import("./components/lenses/renderers/JSONLensRenderer.vue"))["default"]
LensesRenderersPDFLensRenderer: (typeof import("./components/lenses/renderers/PDFLensRenderer.vue"))["default"]
LensesRenderersRawLensRenderer: (typeof import("./components/lenses/renderers/RawLensRenderer.vue"))["default"]
LensesRenderersVideoLensRenderer: (typeof import("./components/lenses/renderers/VideoLensRenderer.vue"))["default"]
LensesRenderersXMLLensRenderer: (typeof import("./components/lenses/renderers/XMLLensRenderer.vue"))["default"]
LensesResponseBodyRenderer: (typeof import("./components/lenses/ResponseBodyRenderer.vue"))["default"]
ProfileUserDelete: (typeof import("./components/profile/UserDelete.vue"))["default"]
RealtimeCommunication: (typeof import("./components/realtime/Communication.vue"))["default"]
RealtimeConnectionConfig: (typeof import("./components/realtime/ConnectionConfig.vue"))["default"]
RealtimeLog: (typeof import("./components/realtime/Log.vue"))["default"]
RealtimeLogEntry: (typeof import("./components/realtime/LogEntry.vue"))["default"]
RealtimeSubscription: (typeof import("./components/realtime/Subscription.vue"))["default"]
SettingsExtension: (typeof import("./components/settings/Extension.vue"))["default"]
SettingsProxy: (typeof import("./components/settings/Proxy.vue"))["default"]
Share: (typeof import("./components/share/index.vue"))["default"]
ShareCreateModal: (typeof import("./components/share/CreateModal.vue"))["default"]
ShareCustomizeModal: (typeof import("./components/share/CustomizeModal.vue"))["default"]
ShareModal: (typeof import("./components/share/Modal.vue"))["default"]
ShareRequest: (typeof import("./components/share/Request.vue"))["default"]
ShareTemplatesButton: (typeof import("./components/share/templates/Button.vue"))["default"]
ShareTemplatesEmbeds: (typeof import("./components/share/templates/Embeds.vue"))["default"]
ShareTemplatesLink: (typeof import("./components/share/templates/Link.vue"))["default"]
SmartAccentModePicker: (typeof import("./components/smart/AccentModePicker.vue"))["default"]
SmartChangeLanguage: (typeof import("./components/smart/ChangeLanguage.vue"))["default"]
SmartColorModePicker: (typeof import("./components/smart/ColorModePicker.vue"))["default"]
SmartEnvInput: (typeof import("./components/smart/EnvInput.vue"))["default"]
TabPrimary: (typeof import("./components/tab/Primary.vue"))["default"]
TabSecondary: (typeof import("./components/tab/Secondary.vue"))["default"]
Teams: (typeof import("./components/teams/index.vue"))["default"]
TeamsAdd: (typeof import("./components/teams/Add.vue"))["default"]
TeamsEdit: (typeof import("./components/teams/Edit.vue"))["default"]
TeamsInvite: (typeof import("./components/teams/Invite.vue"))["default"]
TeamsMemberStack: (typeof import("./components/teams/MemberStack.vue"))["default"]
TeamsModal: (typeof import("./components/teams/Modal.vue"))["default"]
TeamsTeam: (typeof import("./components/teams/Team.vue"))["default"]
Tippy: (typeof import("vue-tippy"))["Tippy"]
WorkspaceCurrent: (typeof import("./components/workspace/Current.vue"))["default"]
WorkspaceSelector: (typeof import("./components/workspace/Selector.vue"))["default"]
}
}

View File

@@ -1,8 +1,8 @@
<template>
<div
ref="contextMenuRef"
class="fixed translate-y-8 transform rounded border border-dividerDark bg-popover p-2 shadow-lg"
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 1000;`"
class="fixed transform -translate-x-10 -translate-y-8 rounded border border-dividerDark bg-popover p-2 shadow-lg"
:style="`top: ${position.top}px; left: ${position.left}px; z-index: 100;`"
>
<div v-if="contextMenuOptions" class="flex flex-col">
<div

View File

@@ -330,11 +330,11 @@ const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const workspace = workspaceService.currentWorkspace
const workspaceName = computed(() =>
workspace.value.type === "personal"
const workspaceName = computed(() => {
return workspace.value.type === "personal"
? t("workspace.personal")
: workspace.value.teamName
)
})
const refetchTeams = () => {
teamListAdapter.fetchList()

View File

@@ -0,0 +1,32 @@
<template>
<span class="flex flex-1 items-center space-x-2">
<template v-for="(title, index) in collectionTitles" :key="index">
<span class="block" :class="{ truncate: index !== 0 }">
{{ title }}
</span>
<icon-lucide-chevron-right class="flex flex-shrink-0" />
</template>
<span
v-if="request"
class="flex flex-shrink-0 truncate rounded-md border border-dividerDark px-1 text-tiny font-semibold"
:style="{ color: getMethodLabelColor(request.method) }"
>
{{ request.method.toUpperCase() }}
</span>
<span v-if="request" class="block">
{{ request.name }}
</span>
</span>
</template>
<script setup lang="ts">
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
defineProps<{
collectionTitles: string[]
request: {
name: string
method: string
}
}>()
</script>

View File

@@ -111,6 +111,7 @@ import { RequestSpotlightSearcherService } from "~/services/spotlight/searchers/
import { ResponseSpotlightSearcherService } from "~/services/spotlight/searchers/response.searcher"
import { SettingsSpotlightSearcherService } from "~/services/spotlight/searchers/settings.searcher"
import { TabSpotlightSearcherService } from "~/services/spotlight/searchers/tab.searcher"
import { TeamsSpotlightSearcherService } from "~/services/spotlight/searchers/teamRequest.searcher"
import { UserSpotlightSearcherService } from "~/services/spotlight/searchers/user.searcher"
import {
SwitchWorkspaceSpotlightSearcherService,
@@ -144,6 +145,7 @@ useService(SwitchEnvSpotlightSearcherService)
useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
useService(TeamsSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)

View File

@@ -8,7 +8,7 @@
>
<template #body>
<HoppSmartTabs
v-model="selectedOptionTab"
v-model="activeTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
@@ -16,7 +16,6 @@
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@change-tab="changeOptionTab"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -34,6 +33,7 @@
:is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties"
:source="source"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
@@ -64,28 +64,42 @@
</template>
<script setup lang="ts">
import { watch, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import {
GQLHeader,
HoppCollection,
HoppGQLAuth,
HoppRESTAuth,
HoppRESTHeaders,
} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { clone } from "lodash-es"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { ref, watch } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { PersistenceService } from "~/services/persistence"
const persistenceService = useService(PersistenceService)
const t = useI18n()
type EditingProperties = {
collection: HoppCollection | TeamCollection | null
export type EditingProperties = {
collection: Partial<HoppCollection> | null
isRootCollection: boolean
path: string
inheritedProperties: HoppInheritedProperty | undefined
inheritedProperties?: HoppInheritedProperty
}
type HoppCollectionAuth = HoppRESTAuth | HoppGQLAuth
type HoppCollectionHeaders = HoppRESTHeaders | GQLHeader[]
const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
editingProperties: EditingProperties | null
source: "REST" | "GraphQL"
modelValue: string
}>(),
{
show: false,
@@ -95,50 +109,68 @@ const props = withDefaults(
)
const emit = defineEmits<{
(e: "set-collection-properties", newCollection: any): void
(
e: "set-collection-properties",
newCollection: Omit<EditingProperties, "inheritedProperties">
): void
(e: "hide-modal"): void
(e: "update:modelValue"): void
}>()
const editableCollection = ref({
body: {
contentType: null,
body: null,
},
const editableCollection = ref<{
headers: HoppCollectionHeaders
auth: HoppCollectionAuth
}>({
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}) as any
})
const selectedOptionTab = ref("headers")
watch(
editableCollection,
(updatedEditableCollection) => {
if (props.show && props.editingProperties) {
const unsavedCollectionProperties: EditingProperties = {
collection: updatedEditableCollection,
isRootCollection: props.editingProperties?.isRootCollection ?? false,
path: props.editingProperties?.path,
inheritedProperties: props.editingProperties?.inheritedProperties,
}
persistenceService.setLocalConfig(
"unsaved_collection_properties",
JSON.stringify(unsavedCollectionProperties)
)
}
},
{
deep: true,
}
)
const changeOptionTab = (tab: RESTOptionTabs) => {
selectedOptionTab.value = tab
}
const activeTab = useVModel(props, "modelValue", emit)
watch(
() => props.show,
(show) => {
if (show && props.editingProperties?.collection) {
editableCollection.value.auth = clone(
props.editingProperties.collection.auth
props.editingProperties.collection.auth as HoppCollectionAuth
)
editableCollection.value.headers = clone(
props.editingProperties.collection.headers
props.editingProperties.collection.headers as HoppCollectionHeaders
)
} else {
editableCollection.value = {
body: {
contentType: null,
body: null,
},
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}
persistenceService.removeLocalConfig("unsaved_collection_properties")
}
}
)
@@ -146,7 +178,6 @@ watch(
const saveEditedCollection = () => {
if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value)
delete finalCollection.body
const collection = {
path: props.editingProperties.path,
collection: {
@@ -155,10 +186,12 @@ const saveEditedCollection = () => {
},
isRootCollection: props.editingProperties.isRootCollection,
}
emit("set-collection-properties", collection)
emit("set-collection-properties", collection as EditingProperties)
persistenceService.removeLocalConfig("unsaved_collection_properties")
}
const hideModal = () => {
persistenceService.removeLocalConfig("unsaved_collection_properties")
emit("hide-modal")
}
</script>

View File

@@ -9,7 +9,7 @@
"
>
<HoppButtonSecondary
v-if="hasNoTeamAccess"
v-if="hasNoTeamAccess || isShowingSearchResults"
v-tippy="{ theme: 'tooltip' }"
disabled
class="!rounded-none"
@@ -36,8 +36,9 @@
v-if="!saveRequest"
v-tippy="{ theme: 'tooltip' }"
:disabled="
collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined
(collectionsType.type === 'team-collections' &&
collectionsType.selectedTeam === undefined) ||
isShowingSearchResults
"
:icon="IconImport"
:title="t('modal.import_export')"
@@ -58,7 +59,7 @@
:collections-type="collectionsType.type"
:is-open="isOpen"
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:collection-move-loading="collectionMoveLoading"
:is-last-item="node.data.isLastItem"
:is-selected="
@@ -128,6 +129,14 @@
})
}
"
@click="
() => {
handleCollectionClick({
collectionID: node.id,
isOpen,
})
}
"
/>
<CollectionsCollection
v-if="node.data.type === 'folders'"
@@ -137,7 +146,7 @@
:collections-type="collectionsType.type"
:is-open="isOpen"
:export-loading="exportLoading"
:has-no-team-access="hasNoTeamAccess"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:collection-move-loading="collectionMoveLoading"
:is-last-item="node.data.isLastItem"
:is-selected="
@@ -209,6 +218,15 @@
})
}
"
@click="
() => {
handleCollectionClick({
// for the folders, we get a path, so we need to get the last part of the path which is the folder id
collectionID: node.id.split('/').pop() as string,
isOpen,
})
}
"
/>
<CollectionsRequest
v-if="node.data.type === 'requests'"
@@ -218,7 +236,7 @@
:collections-type="collectionsType.type"
:duplicate-loading="duplicateLoading"
:is-active="isActiveRequest(node.data.data.data.id)"
:has-no-team-access="hasNoTeamAccess"
:has-no-team-access="hasNoTeamAccess || isShowingSearchResults"
:request-move-loading="requestMoveLoading"
:is-last-item="node.data.isLastItem"
:is-selected="
@@ -283,7 +301,15 @@
</template>
<template #emptyNode="{ node }">
<HoppSmartPlaceholder
v-if="node === null"
v-if="filterText.length !== 0 && teamCollectionList.length === 0"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node === null"
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
@@ -394,6 +420,11 @@ const props = defineProps({
default: () => ({ type: "my-collections", selectedTeam: undefined }),
required: true,
},
filterText: {
type: String as PropType<string>,
default: "",
required: true,
},
teamCollectionList: {
type: Array as PropType<TeamCollection[]>,
default: () => [],
@@ -436,6 +467,8 @@ const props = defineProps({
},
})
const isShowingSearchResults = computed(() => props.filterText.length > 0)
const emit = defineEmits<{
(
event: "add-request",
@@ -543,6 +576,14 @@ const emit = defineEmits<{
}
}
): void
(
event: "collection-click",
payload: {
// if the collection is open or not in the tree
isOpen: boolean
collectionID: string
}
): void
(event: "select", payload: Picked | null): void
(event: "expand-team-collection", payload: string): void
(event: "display-modal-add"): void
@@ -555,6 +596,18 @@ const getPath = (path: string) => {
return pathArray.join("/")
}
const handleCollectionClick = (payload: {
collectionID: string
isOpen: boolean
}) => {
const { collectionID, isOpen } = payload
emit("collection-click", {
collectionID,
isOpen,
})
}
const teamCollectionsList = toRef(props, "teamCollectionList")
const hasNoTeamAccess = computed(

View File

@@ -146,8 +146,10 @@
@hide-modal="displayModalImportExport(false)"
/>
<CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
source="GraphQL"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
@@ -155,7 +157,7 @@
</template>
<script setup lang="ts">
import { nextTick, ref } from "vue"
import { nextTick, onMounted, ref } from "vue"
import { clone, cloneDeep } from "lodash-es"
import {
graphqlCollections$,
@@ -186,6 +188,10 @@ import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
import { useToast } from "~/composables/toast"
import { getRequestsByPath } from "~/helpers/collection/request"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { EditingProperties } from "../Properties.vue"
const t = useI18n()
const toast = useToast()
@@ -219,7 +225,7 @@ const editingRequest = ref<HoppGQLRequest | null>(null)
const editingRequestIndex = ref<number | null>(null)
const editingProperties = ref<{
collection: HoppCollection | null
collection: Partial<HoppCollection> | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
@@ -232,6 +238,53 @@ const editingProperties = ref<{
const filterText = ref("")
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<GQLOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "REST") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties = JSON.parse(
unsavedCollectionPropertiesString
)
const auth = unsavedCollectionProperties.collection?.auth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
const filteredCollections = computed(() => {
const collectionsClone = clone(collections.value)
@@ -557,7 +610,7 @@ const editProperties = ({
if (collectionIndex === null || collection === null) return
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties = {}
let inheritedProperties = undefined
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
@@ -568,7 +621,7 @@ const editProperties = ({
inheritedProperties = {
auth,
headers,
} as HoppInheritedProperty
}
}
editingProperties.value = {
@@ -582,11 +635,15 @@ const editProperties = ({
}
const setCollectionProperties = (newCollection: {
collection: HoppCollection
collection: Partial<HoppCollection> | null
path: string
isRootCollection: boolean
}) => {
const { collection, path, isRootCollection } = newCollection
if (!collection) {
return
}
if (isRootCollection) {
editGraphqlCollection(parseInt(path), collection)
} else {

View File

@@ -24,7 +24,6 @@
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>
</div>
<CollectionsMyCollections
@@ -58,8 +57,15 @@
<CollectionsTeamCollections
v-else
:collections-type="collectionsType"
:team-collection-list="teamCollectionList"
:team-loading-collections="teamLoadingCollections"
:team-collection-list="
filterTexts.length > 0 ? teamsSearchResults : teamCollectionList
"
:team-loading-collections="
filterTexts.length > 0
? collectionsBeingLoadedFromSearch
: teamLoadingCollections
"
:filter-text="filterTexts"
:export-loading="exportLoading"
:duplicate-loading="duplicateLoading"
:save-request="saveRequest"
@@ -87,6 +93,7 @@
@expand-team-collection="expandTeamCollection"
@display-modal-add="displayModalAdd(true)"
@display-modal-import-export="displayModalImportExport(true)"
@collection-click="handleCollectionClick"
/>
<div
class="py-15 hidden flex-1 flex-col items-center justify-center bg-primaryDark px-4 text-secondaryLight"
@@ -154,8 +161,10 @@
@hide-modal="displayTeamModalAdd(false)"
/>
<CollectionsProperties
v-model="collectionPropertiesModalActiveTab"
:show="showModalEditProperties"
:editing-properties="editingProperties"
source="REST"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
@@ -163,7 +172,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, PropType, ref, watch } from "vue"
import { computed, nextTick, onMounted, PropType, ref, watch } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked"
@@ -199,7 +208,7 @@ import {
HoppRESTRequest,
makeCollection,
} from "@hoppscotch/data"
import { cloneDeep, isEqual } from "lodash-es"
import { cloneDeep, debounce, isEqual } from "lodash-es"
import { GQLError } from "~/helpers/backend/GQLClient"
import {
createNewRootCollection,
@@ -240,6 +249,11 @@ import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { TeamSearchService } from "~/helpers/teams/TeamsSearch.service"
import { PersistenceService } from "~/services/persistence"
import { PersistedOAuthConfig } from "~/services/oauth/oauth.service"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { EditingProperties } from "./Properties.vue"
const t = useI18n()
const toast = useToast()
@@ -291,12 +305,7 @@ const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null)
const editingProperties = ref<{
collection: Omit<HoppCollection, "v"> | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
const editingProperties = ref<EditingProperties>({
collection: null,
isRootCollection: false,
path: "",
@@ -336,6 +345,96 @@ const teamLoadingCollections = useReadonlyStream(
[]
)
const {
cascadeParentCollectionForHeaderAuthForSearchResults,
searchTeams,
teamsSearchResults,
teamsSearchResultsLoading,
expandCollection,
expandingCollections,
} = useService(TeamSearchService)
watch(teamsSearchResults, (newSearchResults) => {
if (newSearchResults.length === 1 && filterTexts.value.length > 0) {
expandCollection(newSearchResults[0].id)
}
})
const debouncedSearch = debounce(searchTeams, 400)
const collectionsBeingLoadedFromSearch = computed(() => {
const collections = []
if (teamsSearchResultsLoading.value) {
collections.push("root")
}
collections.push(...expandingCollections.value)
return collections
})
watch(
filterTexts,
(newFilterText) => {
if (collectionsType.value.type === "team-collections") {
const selectedTeamID = collectionsType.value.selectedTeam?.id
selectedTeamID &&
debouncedSearch(newFilterText, selectedTeamID)?.catch(() => {})
}
},
{
immediate: true,
}
)
const persistenceService = useService(PersistenceService)
const collectionPropertiesModalActiveTab = ref<RESTOptionTabs>("headers")
onMounted(() => {
const localOAuthTempConfig =
persistenceService.getLocalConfig("oauth_temp_config")
if (!localOAuthTempConfig) {
return
}
const { context, source, token }: PersistedOAuthConfig =
JSON.parse(localOAuthTempConfig)
if (source === "GraphQL") {
return
}
if (context?.type === "collection-properties") {
// load the unsaved editing properties
const unsavedCollectionPropertiesString = persistenceService.getLocalConfig(
"unsaved_collection_properties"
)
if (unsavedCollectionPropertiesString) {
const unsavedCollectionProperties: EditingProperties = JSON.parse(
unsavedCollectionPropertiesString
)
const auth = unsavedCollectionProperties.collection?.auth
if (auth?.authType === "oauth-2") {
const grantTypeInfo = auth.grantTypeInfo
grantTypeInfo && (grantTypeInfo.token = token ?? "")
}
editingProperties.value = unsavedCollectionProperties
}
persistenceService.removeLocalConfig("oauth_temp_config")
collectionPropertiesModalActiveTab.value = "authorization"
showModalEditProperties.value = true
}
})
watch(
() => myTeams.value,
(newTeams) => {
@@ -364,7 +463,28 @@ const switchToMyCollections = () => {
teamCollectionAdapter.changeTeamID(null)
}
/**
* right now, for search results, we rely on collection click + isOpen to expand the collection
*/
const handleCollectionClick = (payload: {
collectionID: string
isOpen: boolean
}) => {
if (
filterTexts.value.length > 0 &&
teamsSearchResults.value.length &&
payload.isOpen
) {
expandCollection(payload.collectionID)
return
}
}
const expandTeamCollection = (collectionID: string) => {
if (filterTexts.value.length > 0 && teamsSearchResults.value) {
return
}
teamCollectionAdapter.expandCollection(collectionID)
}
@@ -739,7 +859,7 @@ const onAddRequest = (requestName: string) => {
saveContext: {
originLocation: "team-collection",
requestID: createRequestInCollection.id,
collectionID: createRequestInCollection.collection.id,
collectionID: path,
teamID: createRequestInCollection.collection.team.id,
},
inheritedProperties: {
@@ -1330,13 +1450,25 @@ const selectRequest = (selectedRequest: {
let possibleTab = null
if (collectionsType.value.type === "team-collections") {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
let inheritedProperties: HoppInheritedProperty | undefined = undefined
possibleTab = tabs.getTabRefWithSaveContext({
if (filterTexts.value.length > 0) {
const collectionID = folderPath.split("/").at(-1)
if (!collectionID) return
inheritedProperties =
cascadeParentCollectionForHeaderAuthForSearchResults(collectionID)
} else {
inheritedProperties =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
}
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
})
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
} else {
@@ -1348,10 +1480,7 @@ const selectRequest = (selectedRequest: {
requestID: requestIndex,
collectionID: folderPath,
},
inheritedProperties: {
auth,
headers,
},
inheritedProperties: inheritedProperties,
})
}
} else {
@@ -2021,7 +2150,7 @@ const editProperties = (payload: {
{
parentID: "",
parentName: "",
inheritedHeaders: [],
inheritedHeader: {},
},
],
} as HoppInheritedProperty
@@ -2039,7 +2168,7 @@ const editProperties = (payload: {
}
editingProperties.value = {
collection,
collection: collection as Partial<HoppCollection>,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
@@ -2083,7 +2212,7 @@ const editProperties = (payload: {
}
editingProperties.value = {
collection: coll,
collection: coll as unknown as Partial<HoppCollection>,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
@@ -2094,11 +2223,12 @@ const editProperties = (payload: {
}
const setCollectionProperties = (newCollection: {
collection: HoppCollection
path: string
collection: Partial<HoppCollection> | null
isRootCollection: boolean
path: string
}) => {
const { collection, path, isRootCollection } = newCollection
if (!collection) return
if (collectionsType.value.type === "my-collections") {
if (isRootCollection) {
@@ -2148,8 +2278,7 @@ const setCollectionProperties = (newCollection: {
auth,
headers,
},
"rest",
"team"
"rest"
)
}, 200)
}

View File

@@ -32,9 +32,13 @@
{{ tab.document.request.method }}
</span>
<div
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
class="flex items-center flex-1 flex-shrink-0 min-w-0 truncate rounded-r"
>
{{ tab.document.request.endpoint }}
<SmartEnvInput
v-model="tab.document.request.endpoint"
:readonly="true"
:envs="tabRequestVariables"
/>
</div>
</div>
<div class="flex mt-2 space-x-2 sm:mt-0">
@@ -69,6 +73,7 @@
v-model="tab.document.request"
v-model:option-tab="selectedOptionTab"
:properties="properties"
:envs="tabRequestVariables"
/>
<HttpResponse
v-if="tab.document.response"
@@ -117,6 +122,15 @@ const sharedRequestURL = computed(() => {
return `${shortcodeBaseURL}/r/${props.sharedRequestID}`
})
const tabRequestVariables = computed(() => {
return tab.value.document.request.requestVariables.map(({ key, value }) => ({
key,
value,
secret: false,
sourceEnv: "RequestVariable",
}))
})
const { subscribeToStream } = useStreamSubscriber()
const newSendRequest = async () => {

View File

@@ -165,7 +165,7 @@ import { environmentsStore } from "~/newstore/environments"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { uniqueId } from "lodash-es"
import { uniqueID } from "~/helpers/utils/uniqueID"
type EnvironmentVariable = {
id: number
@@ -277,7 +277,7 @@ const workingEnv = computed(() => {
} as Environment
} else if (props.action === "new") {
return {
id: uniqueId(),
id: uniqueID(),
name: "",
variables: props.envVars(),
}
@@ -331,7 +331,7 @@ watch(
: "variables"
if (props.editingEnvironmentIndex !== "Global") {
editingID.value = workingEnv.value?.id ?? uniqueId()
editingID.value = workingEnv.value?.id || uniqueID()
}
vars.value = pipe(
workingEnv.value?.variables ?? [],
@@ -416,14 +416,12 @@ const saveEnvironment = () => {
const variables = pipe(
filteredVariables,
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
)
const environmentUpdated: Environment = {
v: 1,
id: uniqueId(),
id: uniqueID(),
name: editingName.value,
variables,
}

View File

@@ -360,7 +360,7 @@ const saveEnvironment = async () => {
return
}
const filterdVariables = pipe(
const filteredVariables = pipe(
vars.value,
A.filterMap(
flow(
@@ -371,17 +371,15 @@ const saveEnvironment = async () => {
)
const secretVariables = pipe(
filterdVariables,
filteredVariables,
A.filterMapWithIndex((i, e) =>
e.secret ? O.some({ key: e.key, value: e.value, varIndex: i }) : O.none
)
)
const variables = pipe(
filterdVariables,
A.map((e) =>
e.secret ? { key: e.key, secret: e.secret, value: undefined } : e
)
filteredVariables,
A.map((e) => (e.secret ? { key: e.key, secret: e.secret } : e))
)
const environmentUpdated: Environment = {

View File

@@ -23,10 +23,10 @@
@click="provider.action"
/>
<hr v-if="additonalLoginItems.length > 0" />
<hr v-if="additionalLoginItems.length > 0" />
<HoppSmartItem
v-for="loginItem in additonalLoginItems"
v-for="loginItem in additionalLoginItems"
:key="loginItem.id"
:icon="loginItem.icon"
:label="loginItem.text(t)"
@@ -170,7 +170,7 @@ type AuthProviderItem = {
}
let allowedAuthProviders: AuthProviderItem[] = []
let additonalLoginItems: LoginItemDef[] = []
const additionalLoginItems: LoginItemDef[] = []
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
await item.onClick()
@@ -199,10 +199,33 @@ onMounted(async () => {
allowedAuthProviders = enabledAuthProviders
// setup the additional login items
additonalLoginItems =
platform.auth.additionalLoginItems?.filter((item) =>
res.right.includes(item.id)
) ?? []
platform.auth.additionalLoginItems?.forEach((item) => {
if (res.right.includes(item.id)) {
additionalLoginItems.push(item)
}
// since the BE send the OIDC auth providers as OIDC:providerName,
// we need to split the string and use the providerName as the text
if (item.id === "OIDC") {
res.right
.filter((provider) => provider.startsWith("OIDC"))
.forEach((provider) => {
const OIDCName = provider.split(":")[1]
const loginItemText = OIDCName
? () =>
t("auth.continue_with_auth_provider", {
provider: OIDCName,
})
: item.text
const OIDCLoginItem = {
...item,
text: loginItemText,
}
additionalLoginItems.push(OIDCLoginItem)
})
}
})
isLoadingAllowedAuthProviders.value = false
})
@@ -311,6 +334,14 @@ const authProvidersAvailable: AuthProviderItem[] = [
action: signInWithGithub,
isLoading: signingInWithGitHub,
},
// the authprovider either send github or github:enterprise and both are handled by the same route
{
id: "GITHUB:ENTERPRISE",
icon: IconGithub,
label: t("auth.continue_with_github_enterprise"),
action: signInWithGithub,
isLoading: signingInWithGitHub,
},
{
id: "GOOGLE",
icon: IconGoogle,

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'"
@click="
() => {
auth.authType = 'oauth-2'
selectOAuth2AuthType()
hide()
}
"
@@ -189,12 +189,12 @@
<div v-if="auth.authType === 'oauth-2'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.token"
v-model="auth.grantTypeInfo.token"
:environment-highlights="false"
placeholder="Token"
/>
</div>
<HttpOAuth2Authorization v-model="auth" />
<HttpOAuth2Authorization v-model="auth" source="GraphQL" />
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
@@ -220,19 +220,22 @@
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { pluckRef } from "@composables/ref"
import { useColorMode } from "@composables/theming"
import { HoppGQLAuth, HoppGQLAuthOAuth2 } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed, onMounted, ref } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import IconCircle from "~icons/lucide/circle"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconExternalLink from "~icons/lucide/external-link"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconTrash2 from "~icons/lucide/trash-2"
import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppGQLAuth } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { onMounted } from "vue"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
const t = useI18n()
@@ -280,6 +283,30 @@ const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppGQLAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppGQLAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = {
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -579,12 +579,18 @@ const getComputedAuthHeaders = (
})
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
(request.auth.authType === "oauth-2" && request.auth.addTo === "HEADERS")
) {
const requestAuth = request.auth
const isOAuth2 = requestAuth.authType === "oauth-2"
const token = isOAuth2 ? requestAuth.grantTypeInfo.token : requestAuth.token
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${request.auth.token}`,
value: `Bearer ${token}`,
})
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth

View File

@@ -82,7 +82,7 @@
:active="authName === 'OAuth 2.0'"
@click="
() => {
auth.authType = 'oauth-2'
selectOAuth2AuthType()
hide()
}
"
@@ -150,7 +150,7 @@
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">
<div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" />
<HttpAuthorizationBasic v-model="auth" :envs="envs" />
</div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
@@ -169,17 +169,35 @@
</div>
<div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
<SmartEnvInput
v-model="auth.token"
placeholder="Token"
:auto-complete-env="true"
:envs="envs"
/>
</div>
</div>
<div v-if="auth.authType === 'oauth-2'">
<div v-if="auth.authType === 'oauth-2'" class="w-full">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
<!-- Ensure a new object is assigned here to avoid reactivity issues -->
<SmartEnvInput
:model-value="auth.grantTypeInfo.token"
placeholder="Token"
:envs="envs"
@update:model-value="
auth.grantTypeInfo = { ...auth.grantTypeInfo, token: $event }
"
/>
</div>
<HttpOAuth2Authorization v-model="auth" />
<HttpOAuth2Authorization
v-model="auth"
:is-collection-property="isCollectionProperty"
:envs="envs"
:source="source"
/>
</div>
<div v-if="auth.authType === 'api-key'">
<HttpAuthorizationApiKey v-model="auth" />
<HttpAuthorizationApiKey v-model="auth" :envs="envs" />
</div>
</div>
<div
@@ -208,24 +226,36 @@ import IconExternalLink from "~icons/lucide/external-link"
import IconCircleDot from "~icons/lucide/circle-dot"
import IconCircle from "~icons/lucide/circle"
import { computed, ref } from "vue"
import { HoppRESTAuth } from "@hoppscotch/data"
import { HoppRESTAuth, HoppRESTAuthOAuth2 } from "@hoppscotch/data"
import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
import { getDefaultAuthCodeOauthFlowParams } from "~/services/oauth/flows/authCode"
const t = useI18n()
const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const props = withDefaults(
defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
source?: "REST" | "GraphQL"
}>(),
{
source: "REST",
envs: undefined,
inheritedProperties: undefined,
}
)
const emit = defineEmits<{
(e: "update:modelValue", value: HoppRESTAuth): void
@@ -261,6 +291,30 @@ const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const selectOAuth2AuthType = () => {
const defaultGrantTypeInfo: HoppRESTAuthOAuth2["grantTypeInfo"] = {
...getDefaultAuthCodeOauthFlowParams(),
grantType: "AUTHORIZATION_CODE",
token: "",
}
// @ts-expect-error - the existing grantTypeInfo might be in the auth object, typescript doesnt know that
const existingGrantTypeInfo = auth.value.grantTypeInfo as
| HoppRESTAuthOAuth2["grantTypeInfo"]
| undefined
const grantTypeInfo = existingGrantTypeInfo
? existingGrantTypeInfo
: defaultGrantTypeInfo
auth.value = {
...auth.value,
authType: "oauth-2",
addTo: "HEADERS",
grantTypeInfo: grantTypeInfo,
}
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -100,10 +100,12 @@
<HttpBodyParameters
v-if="body.contentType === 'multipart/form-data'"
v-model="body"
:envs="envs"
/>
<HttpURLEncodedParams
v-else-if="body.contentType === 'application/x-www-form-urlencoded'"
v-model="body"
:envs="envs"
/>
<HttpRawBody v-else-if="body.contentType !== null" v-model="body" />
<HoppSmartPlaceholder
@@ -141,6 +143,7 @@ import IconExternalLink from "~icons/lucide/external-link"
import IconInfo from "~icons/lucide/info"
import IconRefreshCW from "~icons/lucide/refresh-cw"
import { RESTOptionTabs } from "./RequestOptions.vue"
import { AggregateEnvironment } from "~/newstore/environments"
const colorMode = useColorMode()
const t = useI18n()
@@ -148,6 +151,7 @@ const t = useI18n()
const props = defineProps<{
body: HoppRESTReqBody
headers: HoppRESTHeader[]
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -65,6 +65,8 @@
<SmartEnvInput
v-model="entry.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateBodyParam(index, {
key: $event,
@@ -87,6 +89,8 @@
<SmartEnvInput
v-model="entry.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateBodyParam(index, {
key: entry.key,
@@ -190,11 +194,13 @@ import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { AggregateEnvironment } from "~/newstore/environments"
type Body = HoppRESTReqBody & { contentType: "multipart/form-data" }
const props = defineProps<{
modelValue: Body
envs: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -188,11 +188,25 @@ const copyCodeIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
const requestCode = computed(() => {
const aggregateEnvs = getAggregateEnvs()
const requestVariables = request.value.requestVariables.map(
(requestVariable) => {
if (requestVariable.active)
return {
key: requestVariable.key,
value: requestVariable.value,
secret: false,
}
return {}
}
)
const env: Environment = {
v: 1,
id: "env",
name: "Env",
variables: aggregateEnvs,
variables: [
...(requestVariables as Environment["variables"]),
...aggregateEnvs,
],
}
const effectiveRequest = getEffectiveRESTRequest(request.value, env)
@@ -212,6 +226,12 @@ const requestCode = computed(() => {
active: true,
})),
endpoint: effectiveRequest.effectiveFinalURL,
requestVariables: effectiveRequest.effectiveFinalRequestVariables.map(
(requestVariable) => ({
...requestVariable,
active: true,
})
),
})
)

View File

@@ -26,7 +26,7 @@
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkMode"
v-if="bulkHeaders"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
@@ -92,6 +92,8 @@
:auto-complete-source="commonHeaders"
:env-index="index"
:inspection-results="getInspectorResult(headerKeyResults, index)"
:auto-complete-env="true"
:envs="envs"
@change="
updateHeader(index, {
id: header.id,
@@ -108,6 +110,8 @@
getInspectorResult(headerValueResults, index)
"
:env-index="index"
:auto-complete-env="true"
:envs="envs"
@change="
updateHeader(index, {
id: header.id,
@@ -303,6 +307,7 @@ import { useColorMode } from "@composables/theming"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
import {
HoppRESTAuth,
HoppRESTHeader,
HoppRESTRequest,
parseRawKeyValueEntriesE,
@@ -329,7 +334,11 @@ import {
getComputedHeaders,
getComputedAuthHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import {
AggregateEnvironment,
aggregateEnvs$,
getAggregateEnvs,
} from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
@@ -356,9 +365,15 @@ const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit
const props = defineProps<{
modelValue: HoppRESTRequest
modelValue:
| HoppRESTRequest
| {
headers: HoppRESTHeader[]
auth: HoppRESTAuth
}
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -98,6 +98,7 @@ import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { EditorView } from "@codemirror/view"
const t = useI18n()
@@ -124,6 +125,7 @@ useCodemirror(
linter: null,
completer: null,
environmentHighlights: false,
onInit: (view: EditorView) => view.focus(),
})
)

View File

@@ -87,6 +87,8 @@
:inspection-results="
getInspectorResult(parameterKeyResults, index)
"
:auto-complete-env="true"
:envs="envs"
@change="
updateParam(index, {
id: param.id,
@@ -102,6 +104,8 @@
:inspection-results="
getInspectorResult(parameterValueResults, index)
"
:auto-complete-env="true"
:envs="envs"
@change="
updateParam(index, {
id: param.id,
@@ -209,6 +213,7 @@ import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { AggregateEnvironment } from "~/newstore/environments"
const colorMode = useColorMode()
@@ -242,6 +247,7 @@ useCodemirror(
const props = defineProps<{
modelValue: HoppRESTParam[]
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -56,6 +56,7 @@
v-model="tab.document.request.endpoint"
:placeholder="`${t('request.url')}`"
:auto-complete-source="userHistories"
:auto-complete-env="true"
:inspection-results="tabResults"
@paste="onPasteUrl($event)"
@enter="newSendRequest"

View File

@@ -5,48 +5,51 @@
render-inactive-tabs
>
<HoppSmartTab
v-if="properties ? properties.includes('params') : true"
v-if="properties?.includes('params') ?? true"
:id="'params'"
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
:info="`${newActiveParamsCount}`"
>
<HttpParameters v-model="request.params" />
<HttpParameters v-model="request.params" :envs="envs" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('bodyParams') : true"
v-if="properties?.includes('bodyParams') ?? true"
:id="'bodyParams'"
:label="`${t('tab.body')}`"
>
<HttpBody
v-model:headers="request.headers"
v-model:body="request.body"
:envs="envs"
@change-tab="changeOptionTab"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('headers') : true"
v-if="properties?.includes('headers') ?? true"
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
:info="`${newActiveHeadersCount}`"
>
<HttpHeaders
v-model="request"
:inherited-properties="inheritedProperties"
:envs="envs"
@change-tab="changeOptionTab"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('authorization') : true"
v-if="properties?.includes('authorization') ?? true"
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
:envs="envs"
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true"
v-if="properties?.includes('preRequestScript') ?? true"
:id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`"
:indicator="
@@ -58,7 +61,7 @@
<HttpPreRequestScript v-model="request.preRequestScript" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('tests') : true"
v-if="properties?.includes('tests') ?? true"
:id="'tests'"
:label="`${t('tab.tests')}`"
:indicator="
@@ -67,6 +70,15 @@
>
<HttpTests v-model="request.testScript" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties?.includes('requestVariables') ?? true"
:id="'requestVariables'"
:label="`${t('tab.variables')}`"
:info="`${newActiveRequestVariablesCount}`"
:align-last="true"
>
<HttpRequestVariables v-model="request.requestVariables" />
</HoppSmartTab>
</HoppSmartTabs>
</template>
@@ -77,6 +89,7 @@ import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { AggregateEnvironment } from "~/newstore/environments"
const VALID_OPTION_TABS = [
"params",
@@ -85,6 +98,7 @@ const VALID_OPTION_TABS = [
"authorization",
"preRequestScript",
"tests",
"requestVariables",
] as const
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
@@ -98,6 +112,7 @@ const props = withDefaults(
optionTab: RESTOptionTabs
properties?: string[]
inheritedProperties?: HoppInheritedProperty
envs?: AggregateEnvironment[]
}>(),
{
optionTab: "params",
@@ -116,22 +131,27 @@ const changeOptionTab = (e: RESTOptionTabs) => {
selectedOptionTab.value = e
}
const newActiveParamsCount$ = computed(() => {
const e = request.value.params.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
const newActiveParamsCount = computed(() => {
const count = request.value.params.filter(
(x) => x.active && (x.key || x.value)
).length
if (e === 0) return null
return `${e}`
return count ? count : null
})
const newActiveHeadersCount$ = computed(() => {
const e = request.value.headers.filter(
(x) => x.active && (x.key !== "" || x.value !== "")
const newActiveHeadersCount = computed(() => {
const count = request.value.headers.filter(
(x) => x.active && (x.key || x.value)
).length
if (e === 0) return null
return `${e}`
return count ? count : null
})
const newActiveRequestVariablesCount = computed(() => {
const count = request.value.requestVariables.filter(
(x) => x.active && (x.key || x.value)
).length
return count ? count : null
})
defineActionHandler("request.open-tab", ({ tab }) => {

View File

@@ -0,0 +1,412 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("request.request_variables") }}
</label>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.clear_all')"
:icon="IconTrash2"
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkVariables"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
:icon="IconWrapText"
@click.prevent="
toggleNestedSetting('WRAP_LINES', 'httpRequestVariables')
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('state.bulk_mode')"
:icon="IconEdit"
:class="{ '!text-accent': bulkMode }"
@click="bulkMode = !bulkMode"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('add.new')"
:icon="IconPlus"
:disabled="bulkMode"
@click="addVariable"
/>
</div>
</div>
<div v-if="bulkMode" class="h-full relative">
<div ref="bulkEditor" class="absolute inset-0"></div>
</div>
<div v-else>
<draggable
v-model="workingRequestVariables"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: variable, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<HoppButtonSecondary
v-tippy="{
theme: 'tooltip',
delay: [500, 20],
content:
index !== workingRequestVariables?.length - 1
? t('action.drag_to_reorder')
: null,
}"
:icon="IconGripVertical"
class="opacity-0"
:class="{
'draggable-handle cursor-grab group-hover:opacity-100':
index !== workingRequestVariables?.length - 1,
}"
tabindex="-1"
/>
<SmartEnvInput
v-model="variable.key"
:placeholder="`${t('count.variable', { count: index + 1 })}`"
@change="
updateVariable(index, {
id: variable.id,
key: $event,
value: variable.value,
active: variable.active,
})
"
/>
<SmartEnvInput
v-model="variable.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
@change="
updateVariable(index, {
id: variable.id,
key: variable.key,
value: $event,
active: variable.active,
})
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="
variable.hasOwnProperty('active')
? variable.active
? t('action.turn_off')
: t('action.turn_on')
: t('action.turn_off')
"
:icon="
variable.hasOwnProperty('active')
? variable.active
? IconCheckCircle
: IconCircle
: IconCheckCircle
"
color="green"
@click="
updateVariable(index, {
id: variable.id,
key: variable.key,
value: variable.value,
active: variable.hasOwnProperty('active')
? !variable.active
: false,
})
"
/>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:title="t('action.remove')"
:icon="IconTrash"
color="red"
@click="deleteVariable(index)"
/>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingRequestVariables.length === 0"
:src="`/images/states/${colorMode.value}/add_files.svg`"
:alt="`${t('empty.request_variables')}`"
:text="t('empty.request_variables')"
>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>
</template>
<script lang="ts" setup>
import { throwError } from "@functional/error"
import {
HoppRESTRequestVariable,
RawKeyValueEntry,
parseRawKeyValueEntriesE,
rawKeyValueEntriesToString,
} from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import { flow, pipe } from "fp-ts/function"
import { cloneDeep, isEqual } from "lodash-es"
import { reactive, ref, watch } from "vue"
import draggable from "vuedraggable-es"
import { useCodemirror } from "~/composables/codemirror"
import { useI18n } from "~/composables/i18n"
import { useNestedSetting } from "~/composables/settings"
import { useColorMode } from "~/composables/theming"
import { useToast } from "~/composables/toast"
import linter from "~/helpers/editor/linting/rawKeyValue"
import { objRemoveKey } from "~/helpers/functional/object"
import { toggleNestedSetting } from "~/newstore/settings"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconCircle from "~icons/lucide/circle"
import IconEdit from "~icons/lucide/edit"
import IconGripVertical from "~icons/lucide/grip-vertical"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconPlus from "~icons/lucide/plus"
import IconTrash from "~icons/lucide/trash"
import IconTrash2 from "~icons/lucide/trash-2"
import IconWrapText from "~icons/lucide/wrap-text"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
const bulkMode = ref(false)
const bulkEditor = ref<any | null>(null)
const bulkVariables = ref("")
const WRAP_LINES = useNestedSetting("WRAP_LINES", "httpRequestVariables")
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
const props = defineProps<{
modelValue: HoppRESTRequestVariable[]
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: Array<HoppRESTRequestVariable>): void
}>()
// The functional requestVariable list (the requestVariable actually applied to the session)
const requestVariables = useVModel(props, "modelValue", emit)
useCodemirror(
bulkEditor,
bulkVariables,
reactive({
extendedEditorConfig: {
mode: "text/x-yaml",
placeholder: `${t("state.bulk_mode_placeholder")}`,
lineWrapping: WRAP_LINES,
},
linter,
completer: null,
environmentHighlights: true,
})
)
const idTicker = ref(0)
const workingRequestVariables = ref<
Array<HoppRESTRequestVariable & { id: number }>
>([
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
])
// Sync logic between params and working/bulk params
watch(
requestVariables,
(newRequestVariableList) => {
// Sync should overwrite working params
const filteredWorkingRequestVariables: HoppRESTRequestVariable[] = pipe(
workingRequestVariables.value,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
const filteredBulkRequestVariables = pipe(
parseRawKeyValueEntriesE(bulkVariables.value),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(newRequestVariableList, filteredWorkingRequestVariables)) {
workingRequestVariables.value = pipe(
newRequestVariableList,
A.map((x) => ({ id: idTicker.value++, ...x }))
)
}
if (!isEqual(newRequestVariableList, filteredBulkRequestVariables)) {
bulkVariables.value = rawKeyValueEntriesToString(newRequestVariableList)
}
},
{ immediate: true }
)
watch(workingRequestVariables, (newWorkingRequestVariables) => {
const fixedRequestVariables = pipe(
newWorkingRequestVariables,
A.filterMap(
flow(
O.fromPredicate((e) => e.key !== ""),
O.map(objRemoveKey("id"))
)
)
)
if (!isEqual(requestVariables.value, fixedRequestVariables)) {
requestVariables.value = cloneDeep(fixedRequestVariables)
}
})
watch(bulkVariables, (newBulkParams) => {
const filteredBulkRequestVariables = pipe(
parseRawKeyValueEntriesE(newBulkParams),
E.map(
flow(
RA.filter((e) => e.key !== ""),
RA.toArray
)
),
E.getOrElse(() => [] as RawKeyValueEntry[])
)
if (!isEqual(requestVariables.value, filteredBulkRequestVariables)) {
requestVariables.value = filteredBulkRequestVariables
}
})
// Rule: Working Request variable always have last element is always an empty param
watch(workingRequestVariables, (variableList) => {
if (
variableList.length > 0 &&
variableList[variableList.length - 1].key !== ""
) {
workingRequestVariables.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
})
const addVariable = () => {
workingRequestVariables.value.push({
id: idTicker.value++,
key: "",
value: "",
active: true,
})
}
const updateVariable = (index: number, variable: any & { id: number }) => {
workingRequestVariables.value = workingRequestVariables.value.map((h, i) =>
i === index ? variable : h
)
}
const deleteVariable = (index: number) => {
const requestVariablesBeforeDeletion = cloneDeep(
workingRequestVariables.value
)
if (
!(
requestVariablesBeforeDeletion.length > 0 &&
index === requestVariablesBeforeDeletion.length - 1
)
) {
if (deletionToast.value) {
deletionToast.value.goAway(0)
deletionToast.value = null
}
deletionToast.value = toast.success(`${t("state.deleted")}`, {
action: [
{
text: `${t("action.undo")}`,
onClick: (_, toastObject) => {
workingRequestVariables.value = requestVariablesBeforeDeletion
toastObject.goAway(0)
deletionToast.value = null
},
},
],
onComplete: () => {
deletionToast.value = null
},
})
}
workingRequestVariables.value = pipe(
workingRequestVariables.value,
A.deleteAt(index),
O.getOrElseW(() =>
throwError("Working Request Variable Deletion Out of Bounds")
)
)
}
const clearContent = () => {
// set params list to the initial state
workingRequestVariables.value = [
{
id: idTicker.value++,
key: "",
value: "",
active: true,
},
]
bulkVariables.value = ""
}
</script>

View File

@@ -21,7 +21,7 @@
@click="clearContent()"
/>
<HoppButtonSecondary
v-if="bulkMode"
v-if="bulkUrlEncodedParams"
v-tippy="{ theme: 'tooltip' }"
:title="t('state.linewrap')"
:class="{ '!text-accent': WRAP_LINES }"
@@ -84,6 +84,8 @@
<SmartEnvInput
v-model="param.key"
:placeholder="`${t('count.parameter', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateUrlEncodedParam(index, {
id: param.id,
@@ -96,6 +98,8 @@
<SmartEnvInput
v-model="param.value"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:auto-complete-env="true"
:envs="envs"
@change="
updateUrlEncodedParam(index, {
id: param.id,
@@ -200,6 +204,7 @@ import { throwError } from "~/helpers/functional/error"
import { useVModel } from "@vueuse/core"
import { useNestedSetting } from "~/composables/settings"
import { toggleNestedSetting } from "~/newstore/settings"
import { AggregateEnvironment } from "~/newstore/environments"
type Body = HoppRESTReqBody & {
contentType: "application/x-www-form-urlencoded"
@@ -207,6 +212,7 @@ type Body = HoppRESTReqBody & {
const props = defineProps<{
modelValue: Body
envs: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -1,9 +1,19 @@
<template>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.key" placeholder="Key" />
<SmartEnvInput
v-model="auth.key"
:auto-complete-env="true"
placeholder="Key"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.value" placeholder="Value" />
<SmartEnvInput
v-model="auth.value"
:auto-complete-env="true"
placeholder="Value"
:envs="envs"
/>
</div>
<div class="flex items-center border-b border-dividerLight">
<span class="flex items-center">
@@ -65,11 +75,13 @@ import { useI18n } from "@composables/i18n"
import { HoppRESTAuthAPIKey } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { ref } from "vue"
import { AggregateEnvironment } from "~/newstore/environments"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthAPIKey
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -3,12 +3,16 @@
<SmartEnvInput
v-model="auth.username"
:placeholder="t('authorization.username')"
:auto-complete-env="true"
:envs="envs"
/>
</div>
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
v-model="auth.password"
:placeholder="t('authorization.password')"
:auto-complete-env="true"
:envs="envs"
/>
</div>
</template>
@@ -17,11 +21,13 @@
import { useI18n } from "@composables/i18n"
import { HoppRESTAuthBasic } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { AggregateEnvironment } from "~/newstore/environments"
const t = useI18n()
const props = defineProps<{
modelValue: HoppRESTAuthBasic
envs?: AggregateEnvironment[]
}>()
const emit = defineEmits<{

View File

@@ -21,28 +21,24 @@
</span>
</div>
<div class="flex flex-col space-y-2 py-4">
<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>
<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
/>
<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
/>
</div>
<div class="space-y-4 py-4">
<div class="flex items-center">

View File

@@ -290,7 +290,12 @@ const widgets: Widget[] = [
},
]
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs

View File

@@ -56,7 +56,12 @@ import { useI18n } from "~/composables/i18n"
const t = useI18n()
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs

View File

@@ -173,6 +173,11 @@ const embedOptions = ref<EmbedOption>({
label: t("tab.authorization"),
enabled: false,
},
{
value: "requestVariables",
label: t("tab.variables"),
enabled: false,
},
],
theme: "system",
})
@@ -223,7 +228,12 @@ const currentUser = useReadonlyStream(
const step = ref(1)
type EmbedTabs = "params" | "bodyParams" | "headers" | "authorization"
type EmbedTabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: EmbedTabs
@@ -263,6 +273,10 @@ const loading = computed(
)
onLoggedIn(() => {
if (adapter.isInitialized()) {
return
}
try {
// wait for a bit to let the auth token to be set
// because in some race conditions, the token is not set this fixes that
@@ -369,6 +383,11 @@ const displayCustomizeRequestModal = (
label: t("tab.authorization"),
enabled: false,
},
{
value: "requestVariables",
label: t("tab.variables"),
enabled: false,
},
],
theme: "system",
}

View File

@@ -28,7 +28,7 @@
</button>
</div>
<div
class="flex"
class="flex overflow-x-scroll"
:class="[{ 'border-b pt-2 ': !noActiveTab }, embedColours]"
>
<span
@@ -57,7 +57,12 @@ import { computed } from "vue"
import { useI18n } from "~/composables/i18n"
type Tabs = "params" | "bodyParams" | "headers" | "authorization"
type Tabs =
| "params"
| "bodyParams"
| "headers"
| "authorization"
| "requestVariables"
type EmbedOption = {
selectedTab: Tabs

View File

@@ -79,29 +79,27 @@ import { history, historyKeymap } from "@codemirror/commands"
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
import { HoppReactiveEnvPlugin } from "~/helpers/editor/extensions/HoppEnvironment"
import { useReadonlyStream } from "@composables/stream"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
} from "~/newstore/environments"
import { AggregateEnvironment, aggregateEnvs$ } from "~/newstore/environments"
import { platform } from "~/platform"
import { onClickOutside, useDebounceFn } from "@vueuse/core"
import { InspectorResult } from "~/services/inspection"
import { invokeAction } from "~/helpers/actions"
import { Environment } from "@hoppscotch/data"
import { useI18n } from "~/composables/i18n"
import IconEye from "~icons/lucide/eye"
import IconEyeoff from "~icons/lucide/eye-off"
import { CompletionContext, autocompletion } from "@codemirror/autocomplete"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { syntaxTree } from "@codemirror/language"
const t = useI18n()
type Env = Environment["variables"][number] & { source: string }
const props = withDefaults(
defineProps<{
modelValue?: string
placeholder?: string
styles?: string
envs?: Env[] | null
envs?: AggregateEnvironment[] | null
focus?: boolean
selectTextOnMount?: boolean
environmentHighlights?: boolean
@@ -110,6 +108,7 @@ const props = withDefaults(
inspectionResults?: InspectorResult[] | undefined
contextMenuEnabled?: boolean
secret?: boolean
autoCompleteEnv?: boolean
}>(),
{
modelValue: "",
@@ -124,6 +123,7 @@ const props = withDefaults(
inspectionResults: undefined,
contextMenuEnabled: true,
secret: false,
autoCompleteEnvSource: false,
}
)
@@ -353,29 +353,62 @@ watch(
let clipboardEv: ClipboardEvent | null = null
let pastedValue: string | null = null
const aggregateEnvs = useReadonlyStream(aggregateEnvsWithSecrets$, []) as Ref<
const aggregateEnvs = useReadonlyStream(aggregateEnvs$, []) as Ref<
AggregateEnvironment[]
>
const tabs = useService(RESTTabService)
const envVars = computed(() => {
return props.envs
? props.envs.map((x) => {
if (x.secret) {
return {
key: x.key,
sourceEnv: "source" in x ? x.source : null,
value: "********",
}
}
return {
key: x.key,
value: x.value,
sourceEnv: "source" in x ? x.source : null,
}
})
: aggregateEnvs.value
if (props.envs) {
return props.envs.map((x) => {
const { key, secret } = x
const value = secret ? "********" : x.value
const sourceEnv = "sourceEnv" in x ? x.sourceEnv : null
return {
key,
value,
sourceEnv,
secret,
}
})
}
return [
...tabs.currentActiveTab.value.document.request.requestVariables.map(
({ active, key, value }) =>
active
? {
key,
value,
sourceEnv: "RequestVariable",
secret: false,
}
: ({} as AggregateEnvironment)
),
...aggregateEnvs.value,
]
})
function envAutoCompletion(context: CompletionContext) {
const options = (envVars.value ?? [])
.map((env) => ({
label: env?.key ? `<<${env.key}>>` : "",
info: env?.value ?? "",
apply: env?.key ? `<<${env.key}>>` : "",
}))
.filter((x) => x)
const nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1)
const textBefore = context.state.sliceDoc(nodeBefore.from, context.pos)
const tagBefore = /<<\w*$/.exec(textBefore)
if (!tagBefore && !context.explicit) return null
return {
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
options: options,
validFor: /^(<<\w*)?$/,
}
}
const envTooltipPlugin = new HoppReactiveEnvPlugin(envVars, view)
function handleTextSelection() {
@@ -384,7 +417,9 @@ function handleTextSelection() {
const { from, to } = selection
if (from === to) return
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
const coords = view.value?.coordsAtPos(from)
const top = coords?.top ?? 0
const left = coords?.left ?? 0
if (text) {
invokeAction("contextmenu.open", {
position: {
@@ -406,16 +441,17 @@ function handleTextSelection() {
}
}
const initView = (el: any) => {
// Debounce to prevent double click from selecting the word
const debounceFn = useDebounceFn(() => {
// Debounce to prevent double click from selecting the word
const debouncedTextSelection = (time: number) =>
useDebounceFn(() => {
handleTextSelection()
}, 140)
}, time)
const initView = (el: any) => {
// Only add event listeners if context menu is enabled in the component
if (props.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
el.addEventListener("mouseup", debouncedTextSelection(140))
el.addEventListener("keyup", debouncedTextSelection(140))
}
const extensions: Extension = getExtensions(props.readonly || isSecret.value)
@@ -465,10 +501,17 @@ const getExtensions = (readonly: boolean): Extension => {
},
scroll(event) {
if (event.target && props.contextMenuEnabled) {
handleTextSelection()
// Debounce to make the performance better
debouncedTextSelection(30)()
}
},
}),
props.autoCompleteEnv
? autocompletion({
activateOnTyping: true,
override: [envAutoCompletion],
})
: [],
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {

View File

@@ -20,7 +20,7 @@
: ''
"
>
<div class="p-4">
<div class="p-4 truncate">
<label
class="font-semibold text-secondaryDark"
:class="{ 'cursor-pointer': compact && team.myRole === 'OWNER' }"
@@ -131,6 +131,7 @@
<HoppSmartConfirmModal
:show="confirmRemove"
:title="t('confirm.remove_team')"
:loading-state="loading"
@hide-modal="confirmRemove = false"
@resolve="deleteTeam()"
/>
@@ -161,6 +162,8 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
import IconUserX from "~icons/lucide/user-x"
import IconUserPlus from "~icons/lucide/user-plus"
import IconTrash2 from "~icons/lucide/trash-2"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
const t = useI18n()
@@ -173,6 +176,7 @@ const props = defineProps<{
const emit = defineEmits<{
(e: "edit-team"): void
(e: "invite-team"): void
(e: "refetch-teams"): void
}>()
const toast = useToast()
@@ -180,7 +184,12 @@ const toast = useToast()
const confirmRemove = ref(false)
const confirmExit = ref(false)
const loading = ref(false)
const workspaceService = useService(WorkspaceService)
const deleteTeam = () => {
loading.value = true
pipe(
backendDeleteTeam(props.teamID),
TE.match(
@@ -188,9 +197,25 @@ const deleteTeam = () => {
// TODO: Better errors ? We know the possible errors now
toast.error(`${t("error.something_went_wrong")}`)
console.error(err)
loading.value = false
confirmRemove.value = false
},
() => {
toast.success(`${t("team.deleted")}`)
loading.value = false
emit("refetch-teams")
const currentWorkspace = workspaceService.currentWorkspace.value
// If the current workspace is the deleted workspace, change the workspace to personal
if (
currentWorkspace.type === "team" &&
currentWorkspace.teamID === props.teamID
) {
workspaceService.changeWorkspace({ type: "personal" })
}
confirmRemove.value = false
}
)
)() // Tasks (and TEs) are lazy, so call the function returned

View File

@@ -4,6 +4,7 @@
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
<div v-if="loading" class="flex flex-col items-center justify-center">
@@ -16,13 +17,6 @@
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<template #body>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div
v-else-if="!loading"
@@ -39,6 +33,7 @@
:compact="modal"
@edit-team="editTeam(team, team.id)"
@invite-team="inviteTeam(team, team.id)"
@refetch-teams="refetchTeams"
/>
</div>
<div v-if="!loading && adapterError" class="flex flex-col items-center">
@@ -76,6 +71,7 @@ import { useReadonlyStream } from "@composables/stream"
import { useColorMode } from "@composables/theming"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import IconPlus from "~icons/lucide/plus"
const t = useI18n()

View File

@@ -27,13 +27,10 @@ const workspaceService = useService(WorkspaceService)
const workspace = workspaceService.currentWorkspace
const currentWorkspace = computed(() => {
if (props.isOnlyPersonal) {
return `${t("workspace.personal")}`
if (props.isOnlyPersonal || workspace.value.type === "personal") {
return t("workspace.personal")
}
if (workspace.value.type === "team") {
return teamWorkspaceName.value
}
return `${t("workspace.personal")}`
return teamWorkspaceName.value
})
const teamWorkspaceName = computed(() => {

View File

@@ -3,7 +3,7 @@
<div class="flex flex-col">
<div class="flex flex-col">
<HoppSmartItem
label="My Workspace"
:label="t('workspace.personal')"
:icon="IconUser"
:info-icon="workspace.type === 'personal' ? IconDone : undefined"
:active-info-icon="workspace.type === 'personal'"
@@ -36,7 +36,7 @@
class="sticky top-0 z-10 mb-2 flex items-center justify-between bg-popover py-2 pl-2"
>
<div class="flex items-center px-2 font-semibold text-secondaryLight">
{{ t("team.title") }}
{{ t("workspace.other_workspaces") }}
</div>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"

View File

@@ -39,7 +39,7 @@ export function onLoggedIn(exec: (user: HoppUser) => void) {
* the auth system.
*
* NOTE: Unlike `onLoggedIn` for which the callback will be called once on mount with the current state,
* here the callback will only be called on authentication event occurances.
* here the callback will only be called on authentication event occurrences.
* You might want to check the auth state from an `onMounted` hook or something
* if you want to access the initial state
*

View File

@@ -68,6 +68,9 @@ type CodeMirrorOptions = {
// callback on editor update
onUpdate?: (view: ViewUpdate) => void
// callback on view initialization
onInit?: (view: EditorView) => void
}
const hoppCompleterExt = (completer: Completer): Extension => {
@@ -208,7 +211,9 @@ export function useCodemirror(
el: Ref<any | null>,
value: Ref<string | undefined>,
options: CodeMirrorOptions
): { cursor: Ref<{ line: number; ch: number }> } {
): {
cursor: Ref<{ line: number; ch: number }>
} {
const { subscribeToStream } = useStreamSubscriber()
// Set default value for contextMenuEnabled if not provided
@@ -242,8 +247,10 @@ export function useCodemirror(
const { from, to } = selection
if (from === to) return
const text = view.value?.state.doc.sliceString(from, to)
const { top, left } = view.value?.coordsAtPos(from)
if (text) {
const coords = view.value?.coordsAtPos(from)
const top = coords?.top ?? 0
const left = coords?.left ?? 0
if (text?.trim()) {
invokeAction("contextmenu.open", {
position: {
top,
@@ -263,6 +270,12 @@ export function useCodemirror(
}
}
// Debounce to prevent double click from selecting the word
const debouncedTextSelection = (time: number) =>
useDebounceFn(() => {
handleTextSelection()
}, time)
const initView = (el: any) => {
if (el) platform.ui?.onCodemirrorInstanceMount?.(el)
@@ -274,15 +287,10 @@ export function useCodemirror(
ViewPlugin.fromClass(
class {
update(update: ViewUpdate) {
// Debounce to prevent double click from selecting the word
const debounceFn = useDebounceFn(() => {
handleTextSelection()
}, 140)
// Only add event listeners if context menu is enabled in the editor
if (options.contextMenuEnabled) {
el.addEventListener("mouseup", debounceFn)
el.addEventListener("keyup", debounceFn)
el.addEventListener("mouseup", debouncedTextSelection(140))
el.addEventListener("keyup", debouncedTextSelection(140))
}
if (options.onUpdate) {
@@ -324,7 +332,8 @@ export function useCodemirror(
EditorView.domEventHandlers({
scroll(event) {
if (event.target && options.contextMenuEnabled) {
handleTextSelection()
// Debounce to make the performance better
debouncedTextSelection(30)()
}
},
}),
@@ -379,6 +388,8 @@ export function useCodemirror(
extensions,
}),
})
options.onInit?.(view.value)
}
onMounted(() => {

View File

@@ -1,4 +1,4 @@
import { customRef, onBeforeUnmount, Ref, watch } from "vue"
import { customRef, onBeforeUnmount, ref, Ref, UnwrapRef, watch } from "vue"
export function pluckRef<T, K extends keyof T>(ref: Ref<T>, key: K): Ref<T[K]> {
return customRef((track, trigger) => {
@@ -31,3 +31,16 @@ export function pluckMultipleFromRef<T, K extends Array<keyof T>>(
): { [key in K[number]]: Ref<T[key]> } {
return Object.fromEntries(keys.map((x) => [x, pluckRef(sourceRef, x)])) as any
}
export const refWithCallbackOnChange = <T>(
initialValue: T,
callback: (value: UnwrapRef<T>) => void
) => {
const targetRef = ref(initialValue)
watch(targetRef, (value) => {
callback(value)
})
return targetRef
}

View File

@@ -1,293 +0,0 @@
import {
FormDataKeyValue,
HoppRESTAuth,
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
RESTReqSchemaVersion,
ValidContentTypes,
} from "@hoppscotch/data"
import { BehaviorSubject, combineLatest, map } from "rxjs"
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
export class RESTRequest {
public v$ = new BehaviorSubject<typeof RESTReqSchemaVersion>(
RESTReqSchemaVersion
)
public name$ = new BehaviorSubject("Untitled")
public endpoint$ = new BehaviorSubject("https://echo.hoppscotch.io/")
public params$ = new BehaviorSubject<HoppRESTParam[]>([])
public headers$ = new BehaviorSubject<HoppRESTHeader[]>([])
public method$ = new BehaviorSubject("GET")
public auth$ = new BehaviorSubject<HoppRESTAuth>({
authType: "none",
authActive: true,
})
public preRequestScript$ = new BehaviorSubject("")
public testScript$ = new BehaviorSubject("")
public body$ = new BehaviorSubject<HoppRESTReqBody>({
contentType: null,
body: null,
})
public response$ = new BehaviorSubject<HoppRESTResponse | null>(null)
get request$() {
// any of above changes construct requests
return combineLatest([
this.v$,
this.name$,
this.endpoint$,
this.params$,
this.headers$,
this.method$,
this.auth$,
this.preRequestScript$,
this.testScript$,
this.body$,
]).pipe(
map(
([
v,
name,
endpoint,
params,
headers,
method,
auth,
preRequestScript,
testScript,
body,
]) => ({
v,
name,
endpoint,
params,
headers,
method,
auth,
preRequestScript,
testScript,
body,
})
)
)
}
get contentType$() {
return this.body$.pipe(map((body) => body.contentType))
}
get bodyContent$() {
return this.body$.pipe(map((body) => body.body))
}
get headersCount$() {
return this.headers$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
)
}
get paramsCount$() {
return this.params$.pipe(
map(
(params) =>
params.filter((x) => x.active && (x.key !== "" || x.value !== ""))
.length
)
)
}
setName(name: string) {
this.name$.next(name)
}
setEndpoint(newURL: string) {
this.endpoint$.next(newURL)
}
setMethod(newMethod: string) {
this.method$.next(newMethod)
}
setParams(entries: HoppRESTParam[]) {
this.params$.next(entries)
}
addParam(newParam: HoppRESTParam) {
const newParams = this.params$.value.concat(newParam)
this.params$.next(newParams)
}
updateParam(index: number, updatedParam: HoppRESTParam) {
const newParams = this.params$.value.map((param, i) =>
i === index ? updatedParam : param
)
this.params$.next(newParams)
}
deleteParam(index: number) {
const newParams = this.params$.value.filter((_, i) => i !== index)
this.params$.next(newParams)
}
deleteAllParams() {
this.params$.next([])
}
setHeaders(entries: HoppRESTHeader[]) {
this.headers$.next(entries)
}
addHeader(newHeader: HoppRESTHeader) {
const newHeaders = this.headers$.value.concat(newHeader)
this.headers$.next(newHeaders)
}
updateHeader(index: number, updatedHeader: HoppRESTHeader) {
const newHeaders = this.headers$.value.map((header, i) =>
i === index ? updatedHeader : header
)
this.headers$.next(newHeaders)
}
deleteHeader(index: number) {
const newHeaders = this.headers$.value.filter((_, i) => i !== index)
this.headers$.next(newHeaders)
}
deleteAllHeaders() {
this.headers$.next([])
}
setContentType(newContentType: ValidContentTypes | null) {
// TODO: persist body evenafter switching content typees
this.body$.next(applyBodyTransition(this.body$.value, newContentType))
}
setBody(newBody: string | FormDataKeyValue[] | null) {
const body = { ...this.body$.value }
body.body = newBody
this.body$.next({ ...body })
}
addFormDataEntry(entry: FormDataKeyValue) {
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [...this.body$.value.body, entry],
}
this.body$.next(body)
}
deleteFormDataEntry(index: number) {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [...this.body$.value.body.filter((_, i) => i !== index)],
}
this.body$.next(body)
}
updateFormDataEntry(index: number, entry: FormDataKeyValue) {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [
...this.body$.value.body.map((oldEntry, i) =>
i === index ? entry : oldEntry
),
],
}
this.body$.next(body)
}
deleteAllFormDataEntries() {
// Only perform update if the current content-type is formdata
if (this.body$.value.contentType !== "multipart/form-data") return {}
const body: HoppRESTReqBody = {
contentType: "multipart/form-data",
body: [],
}
this.body$.next(body)
}
setRequestBody(newBody: HoppRESTReqBody) {
this.body$.next(newBody)
}
setAuth(newAuth: HoppRESTAuth) {
this.auth$.next(newAuth)
}
setPreRequestScript(newScript: string) {
this.preRequestScript$.next(newScript)
}
setTestScript(newScript: string) {
this.testScript$.next(newScript)
}
updateResponse(response: HoppRESTResponse | null) {
this.response$.next(response)
}
setRequest(request: HoppRESTRequest) {
this.v$.next(RESTReqSchemaVersion)
this.name$.next(request.name)
this.endpoint$.next(request.endpoint)
this.params$.next(request.params)
this.headers$.next(request.headers)
this.method$.next(request.method)
this.auth$.next(request.auth)
this.preRequestScript$.next(request.preRequestScript)
this.testScript$.next(request.testScript)
this.body$.next(request.body)
}
getRequest() {
return {
v: this.v$.value,
name: this.name$.value,
endpoint: this.endpoint$.value,
params: this.params$.value,
headers: this.headers$.value,
method: this.method$.value,
auth: this.auth$.value,
preRequestScript: this.preRequestScript$.value,
testScript: this.testScript$.value,
body: this.body$.value,
}
}
resetRequest() {
this.v$.next(RESTReqSchemaVersion)
this.name$.next("")
this.endpoint$.next("")
this.params$.next([])
this.headers$.next([])
this.method$.next("GET")
this.auth$.next({
authType: "none",
authActive: false,
})
this.preRequestScript$.next("")
this.testScript$.next("")
this.body$.next({
contentType: null,
body: null,
})
}
}

View File

@@ -1,4 +1,8 @@
import { Environment } from "@hoppscotch/data"
import {
Environment,
HoppRESTHeaders,
HoppRESTRequestVariable,
} from "@hoppscotch/data"
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import * as A from "fp-ts/Array"
@@ -65,10 +69,17 @@ const getTestableBody = (
return x
}
const combineEnvVariables = (envs: {
global: Environment["variables"]
selected: Environment["variables"]
}) => [...envs.selected, ...envs.global]
const combineEnvVariables = (variables: {
environments: {
selected: Environment["variables"]
global: Environment["variables"]
}
requestVariables: Environment["variables"]
}) => [
...variables.requestVariables,
...variables.environments.selected,
...variables.environments.global,
]
export const executedResponses$ = new Subject<
HoppRESTResponse & { type: "success" | "fail " }
@@ -122,6 +133,38 @@ const updateEnvironmentsWithSecret = (
return updatedEnv
}
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
const filterNonEmptyEnvironmentVariables = (
envs: Environment["variables"]
): Environment["variables"] => {
const envsMap = new Map<string, Environment["variables"][number]>()
envs.forEach((env) => {
if (env.secret) {
envsMap.set(env.key, env)
} else if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (
existingEnv &&
"value" in existingEnv &&
existingEnv.value === "" &&
env.value !== ""
) {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
export function runRESTRequest$(
tab: Ref<HoppTab<HoppRESTDocument>>
): [
@@ -175,15 +218,40 @@ export function runRESTRequest$(
requestHeaders = [...tab.value.document.request.headers]
}
const finalRequestVariables =
tab.value.document.request.requestVariables.map(
(v: HoppRESTRequestVariable) => {
if (v.active) {
return {
key: v.key,
value: v.value,
secret: false,
}
}
return []
}
)
const finalRequest = {
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
headers: requestHeaders,
headers: requestHeaders as HoppRESTHeaders,
}
const finalEnvs = {
requestVariables: finalRequestVariables as Environment["variables"],
environments: envs.right,
}
const finalEnvsWithNonEmptyValues = filterNonEmptyEnvironmentVariables(
combineEnvVariables(finalEnvs)
)
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
id: "env-id",
v: 1,
name: "Env",
variables: combineEnvVariables(envs.right),
variables: finalEnvsWithNonEmptyValues,
})
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)

View File

@@ -109,7 +109,6 @@ export function updateSaveContextForAffectedRequests(
}
}
}
/**
* Used to check the new folder path is close to the save context folder path or not
* @param folderPathCurrent The path saved as the inherited path in the inherited properties
@@ -123,120 +122,109 @@ function folderPathCloseToSaveContext(
saveContextPath: string
) {
if (!folderPathCurrent) return newFolderPath
const folderPathCurrentArray = folderPathCurrent.split("/")
const newFolderPathArray = newFolderPath.split("/")
const saveContextFolderPathArray = saveContextPath.split("/")
let folderPathCurrentMatch = 0
const folderPathCurrentMatch = folderPathCurrentArray.filter(
(folder, i) => folder === saveContextFolderPathArray[i]
).length
for (let i = 0; i < folderPathCurrentArray.length; i++) {
if (folderPathCurrentArray[i] === saveContextFolderPathArray[i]) {
folderPathCurrentMatch++
const newFolderPathMatch = newFolderPathArray.filter(
(folder, i) => folder === saveContextFolderPathArray[i]
).length
return folderPathCurrentMatch > newFolderPathMatch
? folderPathCurrent
: newFolderPath
}
function removeDuplicatesAndKeepLast(arr: HoppInheritedProperty["headers"]) {
const keyMap: { [key: string]: number[] } = {} // Map to store array of indices for each key
// Populate keyMap with the indices of each key
arr.forEach((item, index) => {
const key = item.inheritedHeader.key
if (!(key in keyMap)) {
keyMap[key] = []
}
keyMap[key].push(index)
})
// Create a new array containing only the last occurrence of each key
const result = []
for (const key in keyMap) {
if (Object.prototype.hasOwnProperty.call(keyMap, key)) {
const lastIndex = keyMap[key][keyMap[key].length - 1]
result.push(arr[lastIndex])
}
}
let newFolderPathMatch = 0
for (let i = 0; i < newFolderPathArray.length; i++) {
if (newFolderPathArray[i] === saveContextFolderPathArray[i]) {
newFolderPathMatch++
}
}
if (folderPathCurrentMatch > newFolderPathMatch) {
return folderPathCurrent
}
return newFolderPath
// Sort the result array based on the parentID
result.sort((a, b) => a.parentID.localeCompare(b.parentID))
return result
}
export function updateInheritedPropertiesForAffectedRequests(
path: string,
inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql",
workspace: "personal" | "team" = "personal"
type: "rest" | "graphql"
) {
const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
let tabs
if (workspace === "personal") {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(path)
)
})
} else {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.collectionID?.startsWith(path)
)
})
}
const effectedTabs = tabService.getTabsRefTo((tab) => {
const saveContext = tab.document.saveContext
const tabsEffectedByAuth = tabs.filter((tab) => {
if (workspace === "personal") {
return (
tab.value.document.saveContext?.originLocation === "user-collection" &&
tab.value.document.saveContext.folderPath.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.folderPath
)
)
}
const saveContextPath =
saveContext?.originLocation === "team-collection"
? saveContext.collectionID
: saveContext?.folderPath
return (
tab.value.document.saveContext?.originLocation === "team-collection" &&
tab.value.document.saveContext.collectionID?.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.collectionID
)
)
return saveContextPath?.startsWith(path) ?? false
})
const tabsEffectedByHeaders = tabs.filter((tab) => {
return (
tab.value.document.inheritedProperties &&
tab.value.document.inheritedProperties.headers.some(
effectedTabs.map((tab) => {
const inheritedParentID =
tab.value.document.inheritedProperties?.auth.parentID
const contextPath =
tab.value.document.saveContext?.originLocation === "team-collection"
? tab.value.document.saveContext.collectionID
: tab.value.document.saveContext?.folderPath
const effectedPath = folderPathCloseToSaveContext(
inheritedParentID,
path,
contextPath ?? ""
)
if (effectedPath === path) {
if (tab.value.document.inheritedProperties) {
tab.value.document.inheritedProperties.auth = inheritedProperties.auth
}
}
if (tab.value.document.inheritedProperties?.headers) {
// filter out the headers with the parentID not as the path
const headers = tab.value.document.inheritedProperties.headers.filter(
(header) => header.parentID !== path
)
// filter out the headers with the parentID as the path in the inheritedProperties
const inheritedHeaders = inheritedProperties.headers.filter(
(header) => header.parentID === path
)
)
})
for (const tab of tabsEffectedByAuth) {
tab.value.document.inheritedProperties = inheritedProperties
}
// merge the headers with the parentID as the path
const mergedHeaders = removeDuplicatesAndKeepLast([
...new Set([...inheritedHeaders, ...headers]),
])
for (const tab of tabsEffectedByHeaders) {
const headers = tab.value.document.inheritedProperties?.headers.map(
(header) => {
if (header.parentID === path) {
return {
...header,
inheritedHeader: inheritedProperties.headers.find(
(inheritedHeader) =>
inheritedHeader.inheritedHeader?.key ===
header.inheritedHeader?.key
)?.inheritedHeader,
}
}
return header
}
)
tab.value.document.inheritedProperties = {
...tab.value.document.inheritedProperties,
headers,
tab.value.document.inheritedProperties.headers = mergedHeaders
}
}
})
}
function resetSaveContextForAffectedRequests(folderPath: string) {

View File

@@ -66,12 +66,20 @@ export function getRequestsByPath(
let currentCollection = collections[pathArray[0]]
if (pathArray.length === 1) {
return currentCollection.requests
const latestVersionedRequests = currentCollection.requests.filter(
(req): req is HoppRESTRequest => req.v === "3"
)
return latestVersionedRequests
}
for (let i = 1; i < pathArray.length; i++) {
const folder = currentCollection.folders[pathArray[i]]
if (folder) currentCollection = folder
}
return currentCollection.requests
const latestVersionedRequests = currentCollection.requests.filter(
(req): req is HoppRESTRequest => req.v === "3"
)
return latestVersionedRequests
}

View File

@@ -38,6 +38,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -141,6 +142,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -158,6 +160,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -181,6 +184,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -209,6 +213,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -239,6 +244,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -268,6 +274,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -291,6 +298,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -312,6 +320,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -335,6 +344,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -366,6 +376,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -430,6 +441,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -458,6 +470,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -515,6 +528,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -560,6 +574,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -607,6 +622,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -621,6 +637,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -645,6 +662,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -662,6 +680,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -685,6 +704,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -702,6 +722,7 @@ const samples = [
params: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -725,6 +746,7 @@ const samples = [
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -747,6 +769,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -767,6 +790,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -788,6 +812,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -808,6 +833,7 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
@@ -839,6 +865,39 @@ const samples = [
headers: [],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
{
command: `curl --location 'https://api.example.net/id/1164/requests' \
--header 'Accept: application/vnd.test-data.v2.1+json' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'data={"type":"test","typeId":"101"}' \
--data-urlencode 'data2={"type":"test2","typeId":"123"}'`,
response: makeRESTRequest({
method: "POST",
name: "Untitled",
endpoint: "https://api.example.net/id/1164/requests",
auth: {
authType: "inherit",
authActive: true,
},
body: {
contentType: "application/x-www-form-urlencoded",
body: `data: {"type":"test","typeId":"101"}
data2: {"type":"test2","typeId":"123"}`,
},
params: [],
headers: [
{
active: true,
key: "Accept",
value: "application/vnd.test-data.v2.1+json",
},
],
preRequestScript: "",
testScript: "",
requestVariables: [],
}),
},
]

View File

@@ -33,7 +33,27 @@ export const parseCurlCommand = (curlCommand: string) => {
// const compressed = !!parsedArguments.compressed
curlCommand = preProcessCurlCommand(curlCommand)
const parsedArguments = parser(curlCommand)
const args: parser.Arguments = parser(curlCommand)
const parsedArguments = pipe(
args,
O.fromPredicate(
(args) =>
objHasProperty("dataUrlencode", "string")(args) ||
objHasProperty("dataUrlencode", "object")(args)
),
O.map((args) => {
const urlEncodedData: string[] = Array.isArray(args.dataUrlencode)
? args.dataUrlencode
: [args.dataUrlencode]
const data = A.map(decodeURIComponent)(urlEncodedData)
return { ...args, d: data }
}),
O.getOrElse(() => args)
)
const headerObject = getHeaders(parsedArguments)
const { headers } = headerObject
@@ -181,5 +201,6 @@ export const parseCurlCommand = (curlCommand: string) => {
testScript: defaultRESTReq.testScript,
auth,
body: finalBody,
requestVariables: defaultRESTReq.requestVariables,
})
}

View File

@@ -13,7 +13,7 @@ import { StreamSubscriberFunc } from "@composables/stream"
import {
AggregateEnvironment,
aggregateEnvsWithSecrets$,
getAggregateEnvsWithSecrets,
getAggregateEnvs,
getCurrentEnvironment,
getSelectedEnvironmentType,
} from "~/newstore/environments"
@@ -23,15 +23,45 @@ import IconUsers from "~icons/lucide/users?raw"
import IconEdit from "~icons/lucide/edit?raw"
import { SecretEnvironmentService } from "~/services/secret-environment.service"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
const HOPP_ENVIRONMENT_REGEX = /(<<[a-zA-Z0-9-_]+>>)/g
const HOPP_ENV_HIGHLIGHT =
"cursor-help transition rounded px-1 focus:outline-none mx-0.5 env-highlight"
const HOPP_ENV_HIGHLIGHT_FOUND = "env-found"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "env-not-found"
const HOPP_REQUEST_VARIABLE_HIGHLIGHT = "request-variable-highlight"
const HOPP_ENVIRONMENT_HIGHLIGHT = "environment-variable-highlight"
const HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT = "global-variable-highlight"
const HOPP_ENV_HIGHLIGHT_NOT_FOUND = "environment-not-found-highlight"
const secretEnvironmentService = getService(SecretEnvironmentService)
const restTabs = getService(RESTTabService)
/**
* Transforms the environment list to a list with unique keys with value
* @param envs The environment list to be transformed
* @returns The transformed environment list with keys with value
*/
const filterNonEmptyEnvironmentVariables = (
envs: AggregateEnvironment[]
): AggregateEnvironment[] => {
const envsMap = new Map<string, AggregateEnvironment>()
envs.forEach((env) => {
if (envsMap.has(env.key)) {
const existingEnv = envsMap.get(env.key)
if (existingEnv?.value === "" && env.value !== "") {
envsMap.set(env.key, env)
}
} else {
envsMap.set(env.key, env)
}
})
return Array.from(envsMap.values())
}
const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
hoverTooltip(
@@ -67,7 +97,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const parsedEnvKey = text.slice(start - from, end - from)
const tooltipEnv = aggregateEnvs.find((env) => env.key === parsedEnvKey)
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
const tooltipEnv = envsWithNoEmptyValues.find(
(env) => env.key === parsedEnvKey
)
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
@@ -95,7 +130,12 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
const result = parseTemplateStringE(envValue, aggregateEnvs)
const finalEnv = E.isLeft(result) ? "error" : result.right
let finalEnv = E.isLeft(result) ? "error" : result.right
// If the request variable has an secret variable
// parseTemplateStringE is passed the secret value which has value undefined
// So, we need to check if the result is undefined and then set the finalEnv to ******
if (finalEnv === "undefined") finalEnv = "******"
const selectedEnvType = getSelectedEnvironmentType()
@@ -123,11 +163,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
invokeActionType = "modals.my.environment.edit"
}
invokeAction(invokeActionType, {
envName: tooltipEnv?.sourceEnv !== "Global" ? envName : "Global",
variableName: parsedEnvKey,
isSecret: tooltipEnv?.secret,
})
if (tooltipEnv?.sourceEnv === "RequestVariable") {
restTabs.currentActiveTab.value.document.optionTabPreference =
"requestVariables"
} else {
invokeAction(invokeActionType, {
envName: tooltipEnv?.sourceEnv === "Global" ? "Global" : envName,
variableName: parsedEnvKey,
isSecret: tooltipEnv?.secret,
})
}
})
editIcon.innerHTML = `<span class="inline-flex items-center justify-center my-1">${IconEdit}</span>`
tooltip.appendChild(editIcon)
@@ -165,11 +210,16 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
)
function checkEnv(env: string, aggregateEnvs: AggregateEnvironment[]) {
const className = aggregateEnvs.find(
let className = HOPP_ENV_HIGHLIGHT_NOT_FOUND
const envSource = aggregateEnvs.find(
(k: { key: string }) => k.key === env.slice(2, -2)
)
? HOPP_ENV_HIGHLIGHT_FOUND
: HOPP_ENV_HIGHLIGHT_NOT_FOUND
)?.sourceEnv
if (envSource === "RequestVariable")
className = HOPP_REQUEST_VARIABLE_HIGHLIGHT
else if (envSource === "Global") className = HOPP_GLOBAL_ENVIRONMENT_HIGHLIGHT
else if (envSource !== undefined) className = HOPP_ENVIRONMENT_HIGHLIGHT
return Decoration.mark({
class: `${HOPP_ENV_HIGHLIGHT} ${className}`,
@@ -185,7 +235,10 @@ const getMatchDecorator = (aggregateEnvs: AggregateEnvironment[]) =>
export const environmentHighlightStyle = (
aggregateEnvs: AggregateEnvironment[]
) => {
const decorator = getMatchDecorator(aggregateEnvs)
const envsWithNoEmptyValues =
filterNonEmptyEnvironmentVariables(aggregateEnvs)
const decorator = getMatchDecorator(envsWithNoEmptyValues)
return ViewPlugin.define(
(view) => ({
@@ -209,10 +262,45 @@ export class HoppEnvironmentPlugin {
subscribeToStream: StreamSubscriberFunc,
private editorView: Ref<EditorView | undefined>
) {
this.envs = getAggregateEnvsWithSecrets()
const aggregateEnvs = getAggregateEnvs()
const currentTab = restTabs.currentActiveTab.value
watch(
currentTab.document.request,
(reqVariables) => {
this.envs = [
...reqVariables.requestVariables.map(({ key, value }) => ({
key,
value,
sourceEnv: "RequestVariable",
secret: false,
})),
...aggregateEnvs,
]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
environmentHighlightStyle(this.envs),
]),
})
},
{ immediate: true, deep: true }
)
subscribeToStream(aggregateEnvsWithSecrets$, (envs) => {
this.envs = envs
this.envs = [
...currentTab.document.request.requestVariables.map(
({ key, value }) => ({
key,
value,
sourceEnv: "RequestVariable",
secret: false,
})
),
...envs,
]
this.editorView.value?.dispatch({
effects: this.compartment.reconfigure([
cursorTooltipField(this.envs),
@@ -251,7 +339,7 @@ export class HoppReactiveEnvPlugin {
]),
})
},
{ immediate: true }
{ immediate: true, deep: true }
)
}

View File

@@ -1,10 +1,10 @@
/**
* Converts an array of key-value tuples (for e.g ["key", "value"]), into a record.
* (for eg. output -> { "key": "value" })
* NOTE: This function will discard duplicate key occurances and only keep the last occurance. If you do not want that behaviour,
* NOTE: This function will discard duplicate key occurrences and only keep the last occurrence. If you do not want that behaviour,
* use `tupleWithSamesKeysToRecord`.
* @param tuples Array of tuples ([key, value])
* @returns A record with value corresponding to the last occurance of that key
* @returns A record with value corresponding to the last occurrence of that key
*/
export const tupleToRecord = <
KeyType extends string | number | symbol,

View File

@@ -269,8 +269,16 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
const username = auth.username
const password = auth.password
finalHeaders.Authorization = `Basic ${btoa(`${username}:${password}`)}`
} else if (auth.authType === "bearer" || auth.authType === "oauth-2") {
} else if (auth.authType === "bearer") {
finalHeaders.Authorization = `Bearer ${auth.token}`
} else if (auth.authType === "oauth-2") {
const { addTo } = auth
if (addTo === "HEADERS") {
finalHeaders.Authorization = `Bearer ${auth.grantTypeInfo.token}`
} else if (addTo === "QUERY_PARAMS") {
params["access_token"] = auth.grantTypeInfo.token
}
} else if (auth.authType === "api-key") {
const { key, value, addTo } = auth
if (addTo === "Headers") {

View File

@@ -1,14 +1,19 @@
import { pipe, flow } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import {
HoppCollection,
HoppRESTRequest,
getDefaultGQLRequest,
getDefaultRESTRequest,
translateToNewRESTCollection,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import * as A from "fp-ts/Array"
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es"
import * as TE from "fp-ts/TaskEither"
import { flow, pipe } from "fp-ts/function"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { HoppGQLRequest, translateToNewGQLCollection } from "@hoppscotch/data"
import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
export const hoppRESTImporter = (content: string[]) =>
pipe(
@@ -26,26 +31,30 @@ export const hoppRESTImporter = (content: string[]) =>
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
/**
* checks if a value is a plain object
*/
const isPlainObject = (value: any): value is object => _isPlainObject(value)
/**
* checks if a collection matches the schema for a hoppscotch collection.
* here 2 is the latest version of the schema.
*/
const isValidCollection = (collection: unknown): collection is HoppCollection =>
isPlainObject(collection) && "v" in collection && collection.v === 2
/**
* checks if a collection is a valid hoppscotch collection.
* else translate it into one.
*/
const validateCollection = (collection: unknown) => {
if (isValidCollection(collection)) {
return O.some(collection)
const collectionSchemaParsedResult = HoppCollection.safeParse(collection)
if (collectionSchemaParsedResult.type === "ok") {
const requests = collectionSchemaParsedResult.value.requests.map(
(request) => {
const requestSchemaParsedResult = HoppRESTRequest.safeParse(request)
return requestSchemaParsedResult.type === "ok"
? requestSchemaParsedResult.value
: getDefaultRESTRequest()
}
)
return O.some({
...collectionSchemaParsedResult.value,
requests,
})
}
return O.some(translateToNewRESTCollection(collection))
}
@@ -75,8 +84,24 @@ export const hoppGQLImporter = (content: string) =>
* @returns the collection if it is valid, else a translated version of the collection
*/
export const validateGQLCollection = (collection: unknown) => {
if (isValidCollection(collection)) {
return O.some(collection)
const collectionSchemaParsedResult = HoppCollection.safeParse(collection)
if (collectionSchemaParsedResult.type === "ok") {
const requests = collectionSchemaParsedResult.value.requests.map(
(request) => {
const requestSchemaParsedResult = HoppGQLRequest.safeParse(request)
return requestSchemaParsedResult.type === "ok"
? requestSchemaParsedResult.value
: getDefaultGQLRequest()
}
)
return O.some({
...collectionSchemaParsedResult.value,
requests,
})
}
return O.some(translateToNewGQLCollection(collection))
}

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