Compare commits
28 Commits
pr/danithe
...
refactor/p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13393e9346 | ||
|
|
685d8105e4 | ||
|
|
07c841796a | ||
|
|
f5db54484c | ||
|
|
8deb6471b9 | ||
|
|
73b3ff8e41 | ||
|
|
016a18d3b2 | ||
|
|
ba31cdabea | ||
|
|
51510566bc | ||
|
|
cabee0ecc8 | ||
|
|
2c2b39a236 | ||
|
|
78450c9316 | ||
|
|
b18fd90b64 | ||
|
|
0188a8d7db | ||
|
|
6c63a8dc28 | ||
|
|
17d6ae15a5 | ||
|
|
40f72278a9 | ||
|
|
f717704731 | ||
|
|
185c225297 | ||
|
|
2694731c36 | ||
|
|
ae89af9978 | ||
|
|
87d617012f | ||
|
|
2420b3fa42 | ||
|
|
175a991ec4 | ||
|
|
0301649aff | ||
|
|
544b045300 | ||
|
|
65884293be | ||
|
|
3cb4861bac |
@@ -5,5 +5,5 @@
|
||||
"features": {
|
||||
"ghcr.io/NicoVIII/devcontainer-features/pnpm:1": {}
|
||||
},
|
||||
"postCreateCommand": "mv .env.example .env && pnpm i"
|
||||
"postCreateCommand": "cp .env.example .env && pnpm i"
|
||||
}
|
||||
|
||||
3
.github/workflows/release-push-docker.yml
vendored
3
.github/workflows/release-push-docker.yml
vendored
@@ -18,6 +18,9 @@ jobs:
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
|
||||
2
.github/workflows/ui.yml
vendored
2
.github/workflows/ui.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
# Deploy the ui site with netlify-cli
|
||||
- name: Deploy to Netlify (ui)
|
||||
run: npx netlify-cli deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||
run: npx netlify-cli@15.11.0 deploy --dir=packages/hoppscotch-ui/.histoire/dist --prod
|
||||
env:
|
||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_UI_SITE_ID }}
|
||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
|
||||
10
package.json
10
package.json
@@ -22,16 +22,14 @@
|
||||
"workspaces": [
|
||||
"./packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^16.2.3",
|
||||
"@commitlint/config-conventional": "^16.2.1",
|
||||
"@types/node": "^17.0.24",
|
||||
"@types/node": "17.0.27",
|
||||
"cross-env": "^7.0.3",
|
||||
"http-server": "^14.1.1"
|
||||
"http-server": "^14.1.1",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "12.4.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"packageExtensions": {
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
"devDependencies": {
|
||||
"@lezer/generator": "^1.5.0",
|
||||
"mocha": "^9.2.2",
|
||||
"rollup": "^2.70.2",
|
||||
"rollup-plugin-dts": "^4.2.1",
|
||||
"rollup-plugin-ts": "^2.0.7",
|
||||
"typescript": "^4.6.3"
|
||||
"rollup": "^3.29.3",
|
||||
"rollup-plugin-dts": "^6.0.2",
|
||||
"rollup-plugin-ts": "^3.4.5",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hoppscotch-backend",
|
||||
"version": "2023.8.1",
|
||||
"version": "2023.8.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -24,18 +24,17 @@
|
||||
"do-test": "pnpm run test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs-modules/mailer": "^1.8.1",
|
||||
"@nestjs/apollo": "^10.1.6",
|
||||
"@nestjs/common": "^9.2.1",
|
||||
"@nestjs/core": "^9.2.1",
|
||||
"@nestjs/graphql": "^10.1.6",
|
||||
"@nestjs/jwt": "^10.0.1",
|
||||
"@nestjs/passport": "^9.0.0",
|
||||
"@nestjs/platform-express": "^9.2.1",
|
||||
"@nestjs/throttler": "^4.0.0",
|
||||
"@apollo/server": "^4.9.4",
|
||||
"@nestjs-modules/mailer": "^1.9.1",
|
||||
"@nestjs/apollo": "^12.0.9",
|
||||
"@nestjs/common": "^10.2.6",
|
||||
"@nestjs/core": "^10.2.6",
|
||||
"@nestjs/graphql": "^12.0.9",
|
||||
"@nestjs/jwt": "^10.1.1",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-express": "^10.2.6",
|
||||
"@nestjs/throttler": "^5.0.0",
|
||||
"@prisma/client": "^4.16.2",
|
||||
"apollo-server-express": "^3.11.1",
|
||||
"apollo-server-plugin-base": "^3.7.1",
|
||||
"argon2": "^0.30.3",
|
||||
"bcrypt": "^5.1.0",
|
||||
"cookie": "^0.5.0",
|
||||
@@ -43,9 +42,9 @@
|
||||
"express": "^4.17.1",
|
||||
"express-session": "^1.17.3",
|
||||
"fp-ts": "^2.13.1",
|
||||
"graphql": "^15.5.0",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-query-complexity": "^0.12.0",
|
||||
"graphql-redis-subscriptions": "^2.5.0",
|
||||
"graphql-redis-subscriptions": "^2.6.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"handlebars": "^4.7.7",
|
||||
"io-ts": "^2.2.16",
|
||||
@@ -63,9 +62,9 @@
|
||||
"rxjs": "^7.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^9.1.5",
|
||||
"@nestjs/schematics": "^9.0.3",
|
||||
"@nestjs/testing": "^9.2.1",
|
||||
"@nestjs/cli": "^10.1.18",
|
||||
"@nestjs/schematics": "^10.0.2",
|
||||
"@nestjs/testing": "^10.2.6",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@types/argon2": "^0.15.0",
|
||||
"@types/bcrypt": "^5.0.0",
|
||||
|
||||
@@ -27,12 +27,7 @@ import { AppController } from './app.controller';
|
||||
buildSchemaOptions: {
|
||||
numberScalarMode: 'integer',
|
||||
},
|
||||
cors: {
|
||||
origin: process.env.WHITELISTED_ORIGINS.split(','),
|
||||
credentials: true,
|
||||
},
|
||||
playground: process.env.PRODUCTION !== 'true',
|
||||
debug: process.env.PRODUCTION !== 'true',
|
||||
autoSchemaFile: true,
|
||||
installSubscriptionHandlers: true,
|
||||
subscriptions: {
|
||||
@@ -62,10 +57,12 @@ import { AppController } from './app.controller';
|
||||
}),
|
||||
driver: ApolloDriver,
|
||||
}),
|
||||
ThrottlerModule.forRoot({
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
}),
|
||||
ThrottlerModule.forRoot([
|
||||
{
|
||||
ttl: +process.env.RATE_LIMIT_TTL,
|
||||
limit: +process.env.RATE_LIMIT_MAX,
|
||||
},
|
||||
]),
|
||||
UserModule,
|
||||
AuthModule,
|
||||
AdminModule,
|
||||
|
||||
@@ -93,9 +93,7 @@ export async function emitGQLSchemaFile() {
|
||||
numberScalarMode: 'integer',
|
||||
});
|
||||
|
||||
const schemaString = printSchema(schema, {
|
||||
commentDescriptions: true,
|
||||
});
|
||||
const schemaString = printSchema(schema);
|
||||
|
||||
logger.log(`Writing schema to GQL_SCHEMA_EMIT_LOCATION (${destination})`);
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
|
||||
protected getTracker(req: Record<string, any>): string {
|
||||
protected async getTracker(req: Record<string, any>): Promise<string> {
|
||||
return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
|
||||
// learn more: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For#directives
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { GraphQLSchemaHost } from '@nestjs/graphql';
|
||||
import {
|
||||
ApolloServerPlugin,
|
||||
BaseContext,
|
||||
GraphQLRequestListener,
|
||||
} from 'apollo-server-plugin-base';
|
||||
} from '@apollo/server';
|
||||
import { Plugin } from '@nestjs/apollo';
|
||||
import { GraphQLError } from 'graphql';
|
||||
import {
|
||||
@@ -17,7 +18,7 @@ const COMPLEXITY_LIMIT = 50;
|
||||
export class GQLComplexityPlugin implements ApolloServerPlugin {
|
||||
constructor(private gqlSchemaHost: GraphQLSchemaHost) {}
|
||||
|
||||
async requestDidStart(): Promise<GraphQLRequestListener> {
|
||||
async requestDidStart(): Promise<GraphQLRequestListener<BaseContext>> {
|
||||
const { schema } = this.gqlSchemaHost;
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Team, TeamCollection as DBTeamCollection } from '@prisma/client';
|
||||
import { mock, mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import { mockDeep, mockReset } from 'jest-mock-extended';
|
||||
import {
|
||||
TEAM_COLL_DEST_SAME,
|
||||
TEAM_COLL_INVALID_JSON,
|
||||
@@ -17,9 +17,6 @@ import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { PubSubService } from 'src/pubsub/pubsub.service';
|
||||
import { AuthUser } from 'src/types/AuthUser';
|
||||
import { TeamCollectionService } from './team-collection.service';
|
||||
import { TeamCollection } from './team-collection.model';
|
||||
import { TeamCollectionModule } from './team-collection.module';
|
||||
import * as E from 'fp-ts/Either';
|
||||
|
||||
const mockPrisma = mockDeep<PrismaService>();
|
||||
const mockPubSub = mockDeep<PubSubService>();
|
||||
|
||||
@@ -301,7 +301,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
|
||||
describe('createDuplicateEnvironment', () => {
|
||||
test('should successfully duplicate an existing team environment', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
@@ -322,7 +322,9 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
test('should throw TEAM_ENVIRONMMENT_NOT_FOUND if provided id is invalid', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockRejectedValue('NotFoundError');
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockRejectedValue(
|
||||
'NotFoundError',
|
||||
);
|
||||
|
||||
const result = await teamEnvironmentsService.createDuplicateEnvironment(
|
||||
teamEnvironment.id,
|
||||
@@ -332,7 +334,7 @@ describe('TeamEnvironmentsService', () => {
|
||||
});
|
||||
|
||||
test('should send pubsub message to "team_environment/<teamID>/created" if team environment is updated successfully', async () => {
|
||||
mockPrisma.teamEnvironment.findFirst.mockResolvedValueOnce(
|
||||
mockPrisma.teamEnvironment.findFirstOrThrow.mockResolvedValueOnce(
|
||||
teamEnvironment,
|
||||
);
|
||||
|
||||
|
||||
@@ -183,11 +183,10 @@ export class TeamEnvironmentsService {
|
||||
*/
|
||||
async createDuplicateEnvironment(id: string) {
|
||||
try {
|
||||
const environment = await this.prisma.teamEnvironment.findFirst({
|
||||
const environment = await this.prisma.teamEnvironment.findFirstOrThrow({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
rejectOnNotFound: true,
|
||||
});
|
||||
|
||||
const result = await this.prisma.teamEnvironment.create({
|
||||
|
||||
@@ -142,13 +142,15 @@ describe('UserHistoryService', () => {
|
||||
});
|
||||
describe('createUserHistory', () => {
|
||||
test('Should resolve right and create a REST request to users history and return a `UserHistory` object', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -158,7 +160,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -172,13 +174,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualRight(userHistory);
|
||||
});
|
||||
test('Should resolve right and create a GQL request to users history and return a `UserHistory` object', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -188,7 +192,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -212,13 +216,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualLeft(USER_HISTORY_INVALID_REQ_TYPE);
|
||||
});
|
||||
test('Should create a GQL request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -228,7 +234,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.GQL,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -245,13 +251,15 @@ describe('UserHistoryService', () => {
|
||||
);
|
||||
});
|
||||
test('Should create a REST request to users history and publish a created subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.create.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -261,7 +269,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
};
|
||||
|
||||
@@ -323,13 +331,15 @@ describe('UserHistoryService', () => {
|
||||
).toEqualLeft(USER_HISTORY_NOT_FOUND);
|
||||
});
|
||||
test('Should star/unstar a request in the history and publish a updated subscription', async () => {
|
||||
const executedOn = new Date();
|
||||
|
||||
mockPrisma.userHistory.findFirst.mockResolvedValueOnce({
|
||||
userUid: 'abc',
|
||||
id: '1',
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: false,
|
||||
});
|
||||
|
||||
@@ -339,7 +349,7 @@ describe('UserHistoryService', () => {
|
||||
request: [{}],
|
||||
responseMetadata: [{}],
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: true,
|
||||
});
|
||||
|
||||
@@ -349,7 +359,7 @@ describe('UserHistoryService', () => {
|
||||
request: JSON.stringify([{}]),
|
||||
responseMetadata: JSON.stringify([{}]),
|
||||
reqType: ReqType.REST,
|
||||
executedOn: new Date(),
|
||||
executedOn,
|
||||
isStarred: true,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hoppscotch/cli",
|
||||
"version": "0.3.2",
|
||||
"version": "0.3.3",
|
||||
"description": "A CLI to run Hoppscotch test scripts in CI environments.",
|
||||
"homepage": "https://hoppscotch.io",
|
||||
"main": "dist/index.js",
|
||||
@@ -40,9 +40,6 @@
|
||||
"@hoppscotch/js-sandbox": "workspace:^",
|
||||
"@relmify/jest-fp-ts": "^2.0.2",
|
||||
"@swc/core": "^1.2.181",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/chalk": "^2.2.0",
|
||||
"@types/commander": "^2.12.2",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/lodash": "^4.14.181",
|
||||
"@types/qs": "^6.9.7",
|
||||
@@ -58,6 +55,7 @@
|
||||
"qs": "^6.10.3",
|
||||
"ts-jest": "^27.1.4",
|
||||
"tsup": "^5.12.7",
|
||||
"typescript": "^4.6.4"
|
||||
"typescript": "^4.6.4",
|
||||
"zod": "^3.22.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,9 @@ import { isHoppCLIError } from "../utils/checks";
|
||||
export const test = (path: string, options: TestCmdOptions) => async () => {
|
||||
try {
|
||||
const delay = options.delay ? parseDelayOption(options.delay) : 0
|
||||
const envName = options.envName
|
||||
const envs = options.env ? await parseEnvsData(options.env, envName) : <HoppEnvs>{ global: [], selected: [] }
|
||||
const envs = options.env ? await parseEnvsData(options.env) : <HoppEnvs>{ global: [], selected: [] }
|
||||
const collections = await parseCollectionData(path)
|
||||
|
||||
|
||||
const report = await collectionsRunner({collections, envs, delay})
|
||||
const hasSucceeded = collectionsRunnerResult(report)
|
||||
collectionsRunnerExit(hasSucceeded)
|
||||
|
||||
@@ -48,12 +48,14 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
ERROR_MSG = `Unavailable command: ${error.command}`;
|
||||
break;
|
||||
case "MALFORMED_ENV_FILE":
|
||||
ERROR_MSG = `The environment file is not of the correct format.`;
|
||||
break;
|
||||
case "BULK_ENV_FILE":
|
||||
ERROR_MSG = `CLI doesn't support bulk environments export.`;
|
||||
break;
|
||||
case "MALFORMED_COLLECTION":
|
||||
ERROR_MSG = `${error.path}\n${parseErrorData(error.data)}`;
|
||||
break;
|
||||
case "ENVIRONMENT_NAME_NOT_FOUND":
|
||||
ERROR_MSG = `\n${parseErrorData(error.data)}`;
|
||||
break;
|
||||
case "NO_FILE_PATH":
|
||||
ERROR_MSG = `Please provide a hoppscotch-collection file path.`;
|
||||
break;
|
||||
@@ -85,4 +87,4 @@ export const handleError = <T extends HoppErrorCode>(error: HoppError<T>) => {
|
||||
if (!S.isEmpty(ERROR_MSG)) {
|
||||
console.error(ERROR_CODE, ERROR_MSG);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -50,7 +50,6 @@ program
|
||||
"path to a hoppscotch collection.json file for CI testing"
|
||||
)
|
||||
.option("-e, --env <file_path>", "path to an environment variables json file")
|
||||
.option("-eN, --envName <environment_name>","Specific Name of the environment")
|
||||
.option(
|
||||
"-d, --delay <delay_in_ms>",
|
||||
"delay in milliseconds(ms) between consecutive requests within a collection"
|
||||
|
||||
@@ -33,5 +33,4 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
effectiveFinalHeaders: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalParams: { key: string; value: string; active: boolean }[];
|
||||
effectiveFinalBody: FormData | string | null;
|
||||
effectiveFinalMaskedURL: string;
|
||||
}
|
||||
|
||||
@@ -1,50 +1,45 @@
|
||||
import { error } from "../../types/errors";
|
||||
import { HoppEnvs, HoppEnvPair } from "../../types/request";
|
||||
import {
|
||||
HoppEnvs,
|
||||
HoppEnvPair,
|
||||
HoppEnvKeyPairObject,
|
||||
HoppEnvExportObject,
|
||||
HoppBulkEnvExportObject,
|
||||
} from "../../types/request";
|
||||
import { readJsonFile } from "../../utils/mutators";
|
||||
|
||||
/**
|
||||
* Parses env json file for given path and validates the parsed env json object.
|
||||
* @param path Path of env.json file to be parsed.
|
||||
* @param envName Name of the environment that should be used. If undefined first environment is used.
|
||||
* @returns For successful parsing we get HoppEnvs object.
|
||||
*/
|
||||
export async function parseEnvsData(path: string, envName: string | undefined) {
|
||||
const contents = await readJsonFile(path)
|
||||
if(!(contents && typeof contents === "object" && Array.isArray(contents))) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: null })
|
||||
export async function parseEnvsData(path: string) {
|
||||
const contents = await readJsonFile(path);
|
||||
const envPairs: Array<HoppEnvPair> = [];
|
||||
const HoppEnvKeyPairResult = HoppEnvKeyPairObject.safeParse(contents);
|
||||
const HoppEnvExportObjectResult = HoppEnvExportObject.safeParse(contents);
|
||||
const HoppBulkEnvExportObjectResult =
|
||||
HoppBulkEnvExportObject.safeParse(contents);
|
||||
|
||||
// CLI doesnt support bulk environments export.
|
||||
// Hence we check for this case and throw an error if it matches the format.
|
||||
if (HoppBulkEnvExportObjectResult.success) {
|
||||
throw error({ code: "BULK_ENV_FILE", path, data: error });
|
||||
}
|
||||
|
||||
const envPairs: Array<HoppEnvPair> = []
|
||||
|
||||
const contentEntries = Object.entries(contents)
|
||||
let environmentFound = false;
|
||||
|
||||
for(const [key, obj] of contentEntries) {
|
||||
if(!(typeof obj === "object" && "name" in obj && "variables" in obj)) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: obj } })
|
||||
}
|
||||
if(envName && envName !== obj.name) {
|
||||
continue
|
||||
}
|
||||
environmentFound = true;
|
||||
for(const variable of obj.variables) {
|
||||
if(
|
||||
!(
|
||||
typeof variable === "object" &&
|
||||
"key" in variable &&
|
||||
"value" in variable &&
|
||||
"secret" in variable
|
||||
)
|
||||
) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: { value: variable } });
|
||||
}
|
||||
const { key, value, secret } = variable;
|
||||
envPairs.push({ key: key, value: value, secret: secret });
|
||||
}
|
||||
break
|
||||
// Checks if the environment file is of the correct format.
|
||||
// If it doesnt match either of them, we throw an error.
|
||||
if (!(HoppEnvKeyPairResult.success || HoppEnvExportObjectResult.success)) {
|
||||
throw error({ code: "MALFORMED_ENV_FILE", path, data: error });
|
||||
}
|
||||
if(envName && !environmentFound) {
|
||||
throw error({ code: "ENVIRONMENT_NAME_NOT_FOUND", data: envName });
|
||||
|
||||
if (HoppEnvKeyPairResult.success) {
|
||||
for (const [key, value] of Object.entries(HoppEnvKeyPairResult.data)) {
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
} else if (HoppEnvExportObjectResult.success) {
|
||||
const { key, value } = HoppEnvExportObjectResult.data.variables[0];
|
||||
envPairs.push({ key, value });
|
||||
}
|
||||
return <HoppEnvs>{ global: [], selected: envPairs }
|
||||
|
||||
return <HoppEnvs>{ global: [], selected: envPairs };
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export type TestCmdOptions = {
|
||||
env: string | undefined;
|
||||
delay: string | undefined;
|
||||
envName: string | undefined;
|
||||
};
|
||||
|
||||
export type HOPP_ENV_FILE_EXT = "json";
|
||||
|
||||
@@ -15,7 +15,6 @@ type HoppErrors = {
|
||||
FILE_NOT_FOUND: HoppErrorPath;
|
||||
UNKNOWN_COMMAND: HoppErrorCmd;
|
||||
MALFORMED_COLLECTION: HoppErrorPath & HoppErrorData;
|
||||
ENVIRONMENT_NAME_NOT_FOUND: HoppErrorData;
|
||||
NO_FILE_PATH: {};
|
||||
PRE_REQUEST_SCRIPT_ERROR: HoppErrorData;
|
||||
PARSING_ERROR: HoppErrorData;
|
||||
@@ -25,6 +24,7 @@ type HoppErrors = {
|
||||
REQUEST_ERROR: HoppErrorData;
|
||||
INVALID_ARGUMENT: HoppErrorData;
|
||||
MALFORMED_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
BULK_ENV_FILE: HoppErrorPath & HoppErrorData;
|
||||
INVALID_FILE_TYPE: HoppErrorData;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,30 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
|
||||
import { TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "./errors";
|
||||
import { z } from "zod";
|
||||
|
||||
export type FormDataEntry = {
|
||||
key: string;
|
||||
value: string | Blob;
|
||||
};
|
||||
|
||||
export type HoppEnvPair = { key: string; value: string; secret: boolean };
|
||||
export type HoppEnvPair = { key: string; value: string };
|
||||
|
||||
export const HoppEnvKeyPairObject = z.record(z.string(), z.string());
|
||||
|
||||
// Shape of the single environment export object that is exported from the app.
|
||||
export const HoppEnvExportObject = z.object({
|
||||
name: z.string(),
|
||||
variables: z.array(
|
||||
z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// Shape of the bulk environment export object that is exported from the app.
|
||||
export const HoppBulkEnvExportObject = z.array(HoppEnvExportObject);
|
||||
|
||||
export type HoppEnvs = {
|
||||
global: HoppEnvPair[];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { bold } from "chalk";
|
||||
import { groupEnd, group, log } from "console";
|
||||
import { handleError } from "../handlers/error";
|
||||
import { Method } from "axios";
|
||||
import { RequestConfig } from "../interfaces/request";
|
||||
import { RequestRunnerResponse, TestReport } from "../interfaces/response";
|
||||
import { HoppCLIError } from "../types/errors";
|
||||
import {
|
||||
@@ -172,12 +172,11 @@ export const printFailedTestsReport = (
|
||||
export const printRequestRunner = {
|
||||
/**
|
||||
* Request-runner starting message.
|
||||
* @param requestMethod Provides request's method
|
||||
* @param maskedURL Provides the URL with secrets masked with asterisks
|
||||
* @param requestConfig Provides request's method and url.
|
||||
*/
|
||||
start: (requestMethod: Method | undefined, maskedURL: string) => {
|
||||
const METHOD = BG_INFO(` ${requestMethod} `);
|
||||
const ENDPOINT = maskedURL;
|
||||
start: (requestConfig: RequestConfig) => {
|
||||
const METHOD = BG_INFO(` ${requestConfig.method} `);
|
||||
const ENDPOINT = requestConfig.url;
|
||||
|
||||
process.stdout.write(`${METHOD} ${ENDPOINT}`);
|
||||
},
|
||||
|
||||
@@ -50,9 +50,9 @@ export const preRequestScriptRunner = (
|
||||
isHoppCLIError(reason)
|
||||
? reason
|
||||
: error({
|
||||
code: "PRE_REQUEST_SCRIPT_ERROR",
|
||||
data: reason,
|
||||
})
|
||||
code: "PRE_REQUEST_SCRIPT_ERROR",
|
||||
data: reason,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
@@ -151,12 +151,6 @@ export function getEffectiveRESTRequest(
|
||||
request.endpoint,
|
||||
envVariables
|
||||
);
|
||||
|
||||
const maskedEnvVariables = setAllEnvironmentValuesToAsterisk(envVariables)
|
||||
const _effectiveFinalMaskedURL = parseTemplateStringE(
|
||||
request.endpoint,
|
||||
maskedEnvVariables)
|
||||
|
||||
if (E.isLeft(_effectiveFinalURL)) {
|
||||
return E.left(
|
||||
error({
|
||||
@@ -166,7 +160,6 @@ export function getEffectiveRESTRequest(
|
||||
);
|
||||
}
|
||||
const effectiveFinalURL = _effectiveFinalURL.right;
|
||||
const effectiveFinalMaskedURL = E.isLeft(_effectiveFinalMaskedURL) ? request.endpoint : _effectiveFinalMaskedURL.right;
|
||||
|
||||
return E.right({
|
||||
...request,
|
||||
@@ -174,7 +167,6 @@ export function getEffectiveRESTRequest(
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams,
|
||||
effectiveFinalBody,
|
||||
effectiveFinalMaskedURL,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -250,15 +242,15 @@ function getFinalBodyFromRequest(
|
||||
arrayFlatMap((x) =>
|
||||
x.isFile
|
||||
? x.value.map((v) => ({
|
||||
key: parseTemplateString(x.key, envVariables),
|
||||
value: v as string | Blob,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: parseTemplateString(x.key, envVariables),
|
||||
value: parseTemplateString(x.value, envVariables),
|
||||
},
|
||||
]
|
||||
value: v as string | Blob,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
key: parseTemplateString(x.key, envVariables),
|
||||
value: parseTemplateString(x.value, envVariables),
|
||||
},
|
||||
]
|
||||
),
|
||||
toFormData,
|
||||
E.right
|
||||
@@ -295,22 +287,3 @@ export const getPreRequestMetrics = (
|
||||
hasPreReqErrors ? { failed: 1, passed: 0 } : { failed: 0, passed: 1 },
|
||||
(scripts) => <PreRequestMetrics>{ scripts, duration }
|
||||
);
|
||||
|
||||
/**
|
||||
* Mask all environment values with asterisks
|
||||
* @param variables Environment variable array
|
||||
* @returns Environment variable array with masked values
|
||||
*/
|
||||
const setAllEnvironmentValuesToAsterisk = (
|
||||
variables: Environment["variables"]
|
||||
): Environment["variables"] => {
|
||||
const envVariables: Environment["variables"] = [];
|
||||
for (const variable of variables) {
|
||||
let value = variable.value
|
||||
if (variable.secret) {
|
||||
value = "******"
|
||||
}
|
||||
envVariables.push({ key: variable.key, secret: variable.secret, value: value })
|
||||
}
|
||||
return envVariables
|
||||
}
|
||||
|
||||
@@ -220,7 +220,6 @@ export const processRequest =
|
||||
effectiveFinalHeaders: [],
|
||||
effectiveFinalParams: [],
|
||||
effectiveFinalURL: "",
|
||||
effectiveFinalMaskedURL:"",
|
||||
};
|
||||
|
||||
// Executing pre-request-script
|
||||
@@ -238,8 +237,8 @@ export const processRequest =
|
||||
|
||||
// Creating request-config for request-runner.
|
||||
const requestConfig = createRequest(effectiveRequest);
|
||||
|
||||
printRequestRunner.start(requestConfig.method, effectiveRequest.effectiveFinalMaskedURL);
|
||||
|
||||
printRequestRunner.start(requestConfig);
|
||||
|
||||
// Default value for request-runner's response.
|
||||
let _requestRunnerRes: RequestRunnerResponse = {
|
||||
|
||||
@@ -137,10 +137,10 @@ a {
|
||||
|
||||
.cm-tooltip {
|
||||
.tippy-box {
|
||||
@apply shadow-none;
|
||||
@apply shadow-none #{!important};
|
||||
@apply fixed;
|
||||
@apply inline-flex;
|
||||
@apply -mt-8;
|
||||
@apply -mt-7.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,7 @@
|
||||
"turn_off": "Ausschalten",
|
||||
"turn_on": "Einschalten",
|
||||
"undo": "Rückgängig machen",
|
||||
"yes": "Ja",
|
||||
"secret": "Als Secret speichern"
|
||||
"yes": "Ja"
|
||||
},
|
||||
"add": {
|
||||
"new": "Neue hinzufügen",
|
||||
@@ -881,4 +880,4 @@
|
||||
"team": "Team Workspace",
|
||||
"title": "Workspaces"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,8 +48,7 @@
|
||||
"turn_off": "Turn off",
|
||||
"turn_on": "Turn on",
|
||||
"undo": "Undo",
|
||||
"yes": "Yes",
|
||||
"secret": "Save as secret"
|
||||
"yes": "Yes"
|
||||
},
|
||||
"add": {
|
||||
"new": "Add new",
|
||||
@@ -113,6 +112,7 @@
|
||||
},
|
||||
"authorization": {
|
||||
"generate_token": "Generate Token",
|
||||
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
|
||||
"include_in_url": "Include in URL",
|
||||
"learn": "Learn how",
|
||||
"pass_key_by": "Pass by",
|
||||
@@ -125,6 +125,7 @@
|
||||
"created": "Collection created",
|
||||
"different_parent": "Cannot reorder collection with different parent",
|
||||
"edit": "Edit Collection",
|
||||
"import_or_create": "Import or create a collection",
|
||||
"invalid_name": "Please provide a name for the collection",
|
||||
"invalid_root_move": "Collection already in the root",
|
||||
"moved": "Moved Successfully",
|
||||
@@ -210,6 +211,7 @@
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"import_or_create": "Import or create a environment",
|
||||
"invalid_name": "Please provide a name for the environment",
|
||||
"list": "Environment variables",
|
||||
"my_environments": "My Environments",
|
||||
@@ -250,7 +252,9 @@
|
||||
"json_prettify_invalid_body": "Couldn't prettify an invalid body, solve json syntax errors and try again",
|
||||
"network_error": "There seems to be a network error. Please try again.",
|
||||
"network_fail": "Could not send request",
|
||||
"no_collections_to_export": "No collections to export. Please create a collection to get started.",
|
||||
"no_duration": "No duration",
|
||||
"no_environments_to_export": "No environments to export. Please create an environment to get started.",
|
||||
"no_results_found": "No matches found",
|
||||
"page_not_found": "This page could not be found",
|
||||
"proxy_error": "Proxy error",
|
||||
@@ -457,6 +461,7 @@
|
||||
"enter_curl": "Enter cURL command",
|
||||
"generate_code": "Generate code",
|
||||
"generated_code": "Generated code",
|
||||
"go_to_authorization_tab": "Go to Authorization",
|
||||
"header_list": "Header List",
|
||||
"invalid_name": "Please provide a name for the request",
|
||||
"method": "Method",
|
||||
@@ -744,9 +749,11 @@
|
||||
"disconnected_from": "Disconnected from {name}",
|
||||
"docs_generated": "Documentation generated",
|
||||
"download_started": "Download started",
|
||||
"download_failed": "Download failed",
|
||||
"enabled": "Enabled",
|
||||
"file_imported": "File imported",
|
||||
"finished_in": "Finished in {duration} ms",
|
||||
"hide": "Hide",
|
||||
"history_deleted": "History deleted",
|
||||
"linewrap": "Wrap lines",
|
||||
"loading": "Loading...",
|
||||
@@ -757,6 +764,7 @@
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"reconnection_error": "Failed to reconnect",
|
||||
"show":"Show",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
@@ -838,7 +846,7 @@
|
||||
"new": "New Team",
|
||||
"new_created": "New team created",
|
||||
"new_name": "My New Team",
|
||||
"no_access": "You do not have edit access to these collections",
|
||||
"no_access": "You do not have edit access to this team",
|
||||
"no_invite_found": "Invitation not found. Contact your team owner.",
|
||||
"no_request_found": "Request not found.",
|
||||
"not_found": "Team not found. Contact your team owner.",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"choose_file": "選擇一個檔案",
|
||||
"clear": "清除",
|
||||
"clear_all": "全部清除",
|
||||
"clear_history": "Clear all History",
|
||||
"clear_history": "清除所有歷史記錄",
|
||||
"close": "關閉",
|
||||
"connect": "連線",
|
||||
"connecting": "正在連接",
|
||||
@@ -79,8 +79,8 @@
|
||||
"search": "搜尋",
|
||||
"share": "分享",
|
||||
"shortcuts": "快捷方式",
|
||||
"social_description": "Follow us on social media to stay updated with the latest news, updates and releases.",
|
||||
"social_links": "Social links",
|
||||
"social_description": "在社交媒體上追蹤我們即可在第一時間得知新聞、更新、以及新版本的消息。",
|
||||
"social_links": "社群連結",
|
||||
"spotlight": "聚光燈",
|
||||
"status": "狀態",
|
||||
"status_description": "檢查網站狀態",
|
||||
@@ -135,15 +135,15 @@
|
||||
"renamed": "集合已重新命名",
|
||||
"request_in_use": "請求正在使用中",
|
||||
"save_as": "另存為",
|
||||
"save_to_collection": "Save to Collection",
|
||||
"save_to_collection": "儲存到集合",
|
||||
"select": "選擇一個集合",
|
||||
"select_location": "選擇位置",
|
||||
"select_team": "選擇一個團隊",
|
||||
"team_collections": "團隊集合"
|
||||
},
|
||||
"confirm": {
|
||||
"close_unsaved_tab": "Are you sure you want to close this tab?",
|
||||
"close_unsaved_tabs": "Are you sure you want to close all tabs? {count} unsaved tabs will be lost.",
|
||||
"close_unsaved_tab": "您確定要關閉此分頁嗎?",
|
||||
"close_unsaved_tabs": "您確定要關閉所有分頁嗎?{count} 個未儲存的分頁將會遺失。",
|
||||
"exit_team": "您確定要離開此團隊嗎?",
|
||||
"logout": "您確定要登出嗎?",
|
||||
"remove_collection": "您確定要永久刪除該集合嗎?",
|
||||
@@ -158,9 +158,9 @@
|
||||
"sync": "您想從雲端恢復您的工作區嗎?這將丟棄您的本地進度。"
|
||||
},
|
||||
"context_menu": {
|
||||
"add_parameters": "Add to parameters",
|
||||
"open_request_in_new_tab": "Open request in new tab",
|
||||
"set_environment_variable": "Set as variable"
|
||||
"add_parameters": "新增至參數",
|
||||
"open_request_in_new_tab": "在新分頁開啟請求",
|
||||
"set_environment_variable": "設為變數"
|
||||
},
|
||||
"count": {
|
||||
"header": "請求標頭 {count}",
|
||||
@@ -204,31 +204,31 @@
|
||||
"create_new": "建立新環境",
|
||||
"created": "已建立環境",
|
||||
"deleted": "刪除環境",
|
||||
"duplicated": "Environment duplicated",
|
||||
"duplicated": "已複製環境",
|
||||
"edit": "編輯環境",
|
||||
"empty_variables": "No variables",
|
||||
"global": "Global",
|
||||
"global_variables": "Global variables",
|
||||
"empty_variables": "無變數",
|
||||
"global": "全域",
|
||||
"global_variables": "全域變數",
|
||||
"invalid_name": "請提供有效的環境名稱",
|
||||
"list": "Environment variables",
|
||||
"list": "環境變數",
|
||||
"my_environments": "我的環境",
|
||||
"name": "Name",
|
||||
"name": "名稱",
|
||||
"nested_overflow": "巢狀環境變數不得大於 10 層",
|
||||
"new": "建立環境",
|
||||
"no_active_environment": "No active environment",
|
||||
"no_active_environment": "無使用中的環境",
|
||||
"no_environment": "無環境",
|
||||
"no_environment_description": "未選取任何環境。請選擇要對以下變數進行的動作。",
|
||||
"quick_peek": "Environment Quick Peek",
|
||||
"replace_with_variable": "Replace with variable",
|
||||
"scope": "Scope",
|
||||
"quick_peek": "快速預覽環境",
|
||||
"replace_with_variable": "以變數替代",
|
||||
"scope": "範圍",
|
||||
"select": "選擇環境",
|
||||
"set": "Set environment",
|
||||
"set_as_environment": "Set as environment",
|
||||
"set": "設定環境",
|
||||
"set_as_environment": "設為環境",
|
||||
"team_environments": "團隊環境",
|
||||
"title": "環境",
|
||||
"updated": "更新環境",
|
||||
"value": "Value",
|
||||
"variable": "Variable",
|
||||
"value": "數值",
|
||||
"variable": "變數",
|
||||
"variable_list": "變數列表"
|
||||
},
|
||||
"error": {
|
||||
@@ -252,7 +252,7 @@
|
||||
"no_duration": "無持續時間",
|
||||
"no_results_found": "找不到結果",
|
||||
"page_not_found": "找不到此頁面",
|
||||
"proxy_error": "Proxy error",
|
||||
"proxy_error": "Proxy 錯誤",
|
||||
"script_fail": "無法執行預請求指令碼",
|
||||
"something_went_wrong": "發生了一些錯誤",
|
||||
"test_script_fail": "無法執行測試指令碼"
|
||||
@@ -278,13 +278,13 @@
|
||||
"renamed": "資料夾已重新命名"
|
||||
},
|
||||
"graphql": {
|
||||
"connection_switch_confirm": "Do you want to connect with the latest GraphQL endpoint?",
|
||||
"connection_switch_new_url": "Switching to a tab will disconnected you from the active GraphQL connection. New connection URL is",
|
||||
"connection_switch_url": "You're connected to a GraphQL endpoint the connection URL is",
|
||||
"connection_switch_confirm": "您要使用最新的 GraphQL 端點連線嗎?",
|
||||
"connection_switch_new_url": "切換至分頁將斷開使用中的 GraphQL 連線。新的連線網址為 ",
|
||||
"connection_switch_url": "您已連接至 GraphQL 端點。連線網址為 ",
|
||||
"mutations": "變體",
|
||||
"schema": "綱要",
|
||||
"subscriptions": "訂閱",
|
||||
"switch_connection": "Switch connection"
|
||||
"switch_connection": "切換連線"
|
||||
},
|
||||
"group": {
|
||||
"time": "時間",
|
||||
@@ -339,27 +339,27 @@
|
||||
"title": "匯入"
|
||||
},
|
||||
"inspections": {
|
||||
"description": "Inspect possible errors",
|
||||
"description": "檢查潛在錯誤",
|
||||
"environment": {
|
||||
"add_environment": "Add to Environment",
|
||||
"not_found": "Environment variable “{environment}” not found."
|
||||
"add_environment": "新增至環境",
|
||||
"not_found": "找不到環境變數 “{environment}”。"
|
||||
},
|
||||
"header": {
|
||||
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
|
||||
"cookie": "瀏覽器不允許 Hoppscotch 設定 Cookie 標頭。在我們推出 Hoppscotch 桌面版前,請先使用 Authorization 標頭。"
|
||||
},
|
||||
"response": {
|
||||
"401_error": "Please check your authentication credentials.",
|
||||
"404_error": "Please check your request URL and method type.",
|
||||
"cors_error": "Please check your Cross-Origin Resource Sharing configuration.",
|
||||
"default_error": "Please check your request.",
|
||||
"network_error": "Please check your network connection."
|
||||
"401_error": "請檢查您的授權認證。",
|
||||
"404_error": "請檢查您的請求網址和方式類型。",
|
||||
"cors_error": "請檢查您的跨來源資源共用設定。",
|
||||
"default_error": "請檢查您的請求。",
|
||||
"network_error": "請檢查您的網路連線。"
|
||||
},
|
||||
"title": "Inspector",
|
||||
"title": "檢查工具",
|
||||
"url": {
|
||||
"extension_not_installed": "Extension not installed.",
|
||||
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
|
||||
"extention_enable_action": "Enable Browser Extension",
|
||||
"extention_not_enabled": "Extension not enabled."
|
||||
"extension_not_installed": "未安裝擴充套件。",
|
||||
"extension_unknown_origin": "請確認您是否已將 API 端點的來源加入 Hoppscotch 擴充套件的清單。",
|
||||
"extention_enable_action": "啟用瀏覽器擴充套件",
|
||||
"extention_not_enabled": "未啟用擴充套件。"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -472,7 +472,7 @@
|
||||
"payload": "負載",
|
||||
"query": "查詢",
|
||||
"raw_body": "原始請求本體",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重新命名請求",
|
||||
"renamed": "請求已重新命名",
|
||||
"run": "執行",
|
||||
"save": "儲存",
|
||||
@@ -510,7 +510,7 @@
|
||||
"accent_color": "強調色",
|
||||
"account": "帳號",
|
||||
"account_deleted": "已刪除您的帳號",
|
||||
"account_description": "自定義您的帳號設定。",
|
||||
"account_description": "自訂您的帳號設定。",
|
||||
"account_email_description": "您的主要電子郵件地址。",
|
||||
"account_name_description": "這是您的顯示名稱。",
|
||||
"background": "背景",
|
||||
@@ -542,7 +542,7 @@
|
||||
"read_the": "閱讀",
|
||||
"reset_default": "重置為預設",
|
||||
"short_codes": "快捷碼",
|
||||
"short_codes_description": "我們為您打造的快捷碼。",
|
||||
"short_codes_description": "您建立的快捷碼。",
|
||||
"sidebar_on_left": "左側邊欄",
|
||||
"sync": "同步",
|
||||
"sync_collections": "集合",
|
||||
@@ -551,9 +551,9 @@
|
||||
"sync_history": "歷史",
|
||||
"system_mode": "系統",
|
||||
"telemetry": "遙測服務",
|
||||
"telemetry_helps_us": "遙測服務幫助我們進行個性化操作,為您提供最佳體驗。",
|
||||
"telemetry_helps_us": "遙測服務能夠幫助我們進行個人化操作,為您提供最佳體驗。",
|
||||
"theme": "主題",
|
||||
"theme_description": "自定義您的應用程式主題。",
|
||||
"theme_description": "自訂您的應用程式主題。",
|
||||
"use_experimental_url_bar": "使用帶有環境醒目標示的實驗性網址欄",
|
||||
"user": "使用者",
|
||||
"verified_email": "已確認電子郵件地址",
|
||||
@@ -592,26 +592,26 @@
|
||||
"title": "導航"
|
||||
},
|
||||
"others": {
|
||||
"prettify": "Prettify Editor's Content",
|
||||
"title": "Others"
|
||||
"prettify": "美化編輯器的內容",
|
||||
"title": "其他"
|
||||
},
|
||||
"request": {
|
||||
"copy_request_link": "複製請求連結",
|
||||
"delete_method": "選擇 DELETE 方法",
|
||||
"get_method": "選擇 GET 方法",
|
||||
"head_method": "選擇 HEAD 方法",
|
||||
"import_curl": "Import cURL",
|
||||
"import_curl": "匯入 cURL",
|
||||
"method": "方法",
|
||||
"next_method": "選擇下一個方法",
|
||||
"post_method": "選擇 POST 方法",
|
||||
"previous_method": "選擇上一個方法",
|
||||
"put_method": "選擇 PUT 方法",
|
||||
"rename": "Rename Request",
|
||||
"rename": "重新命名請求",
|
||||
"reset_request": "重置請求",
|
||||
"save_request": "Save Request",
|
||||
"save_request": "儲存請求",
|
||||
"save_to_collections": "儲存到集合",
|
||||
"send_request": "傳送請求",
|
||||
"show_code": "Generate code snippet",
|
||||
"show_code": "產生程式碼片段",
|
||||
"title": "請求"
|
||||
},
|
||||
"response": {
|
||||
@@ -642,82 +642,82 @@
|
||||
"url": "網址"
|
||||
},
|
||||
"spotlight": {
|
||||
"change_language": "Change Language",
|
||||
"change_language": "變更語言",
|
||||
"environments": {
|
||||
"delete": "Delete current environment",
|
||||
"duplicate": "Duplicate current environment",
|
||||
"duplicate_global": "Duplicate global environment",
|
||||
"edit": "Edit current environment",
|
||||
"edit_global": "Edit global environment",
|
||||
"new": "Create new environment",
|
||||
"new_variable": "Create a new environment variable",
|
||||
"title": "Environments"
|
||||
"delete": "刪除目前環境",
|
||||
"duplicate": "複製目前環境",
|
||||
"duplicate_global": "複製全域環境",
|
||||
"edit": "編輯目前環境",
|
||||
"edit_global": "編輯全域環境",
|
||||
"new": "建立新環境",
|
||||
"new_variable": "建立新環境變數",
|
||||
"title": "環境"
|
||||
},
|
||||
"general": {
|
||||
"chat": "Chat with support",
|
||||
"help_menu": "Help and support",
|
||||
"open_docs": "Read Documentation",
|
||||
"open_github": "Open GitHub repository",
|
||||
"open_keybindings": "Keyboard shortcuts",
|
||||
"social": "Social",
|
||||
"title": "General"
|
||||
"chat": "與客服對話",
|
||||
"help_menu": "幫助與支援",
|
||||
"open_docs": "閱讀說明文件",
|
||||
"open_github": "開啟 GitHub 儲存庫",
|
||||
"open_keybindings": "鍵盤快捷鍵",
|
||||
"social": "社交",
|
||||
"title": "一般"
|
||||
},
|
||||
"graphql": {
|
||||
"connect": "Connect to server",
|
||||
"disconnect": "Disconnect from server"
|
||||
"connect": "連接至伺服器",
|
||||
"disconnect": "斷開與伺服器的連線"
|
||||
},
|
||||
"miscellaneous": {
|
||||
"invite": "Invite your friends to Hoppscotch",
|
||||
"title": "Miscellaneous"
|
||||
"invite": "邀請您的朋友使用 Hoppscotch",
|
||||
"title": "雜項"
|
||||
},
|
||||
"request": {
|
||||
"save_as_new": "Save as new request",
|
||||
"select_method": "Select method",
|
||||
"switch_to": "Switch to",
|
||||
"tab_authorization": "Authorization tab",
|
||||
"tab_body": "Body tab",
|
||||
"tab_headers": "Headers tab",
|
||||
"tab_parameters": "Parameters tab",
|
||||
"tab_pre_request_script": "Pre-request script tab",
|
||||
"tab_query": "Query tab",
|
||||
"tab_tests": "Tests tab",
|
||||
"tab_variables": "Variables tab"
|
||||
"save_as_new": "儲存為新請求",
|
||||
"select_method": "選擇方法",
|
||||
"switch_to": "切換至",
|
||||
"tab_authorization": "授權分頁",
|
||||
"tab_body": "本體分頁",
|
||||
"tab_headers": "標頭分頁",
|
||||
"tab_parameters": "參數分頁",
|
||||
"tab_pre_request_script": "預請求腳本分頁",
|
||||
"tab_query": "查詢分頁",
|
||||
"tab_tests": "測試分頁",
|
||||
"tab_variables": "變數分頁"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response",
|
||||
"download": "Download response as file",
|
||||
"title": "Response"
|
||||
"copy": "複製回應",
|
||||
"download": "下載回應",
|
||||
"title": "回應"
|
||||
},
|
||||
"section": {
|
||||
"interceptor": "Interceptor",
|
||||
"interface": "Interface",
|
||||
"theme": "Theme",
|
||||
"user": "User"
|
||||
"interceptor": "攔截器",
|
||||
"interface": "介面",
|
||||
"theme": "主題",
|
||||
"user": "使用者"
|
||||
},
|
||||
"settings": {
|
||||
"change_interceptor": "Change Interceptor",
|
||||
"change_language": "Change Language",
|
||||
"change_interceptor": "變更攔截器",
|
||||
"change_language": "變更語言",
|
||||
"theme": {
|
||||
"black": "Black",
|
||||
"dark": "Dark",
|
||||
"light": "Light",
|
||||
"system": "System preference"
|
||||
"black": "黑色",
|
||||
"dark": "暗色",
|
||||
"light": "亮色",
|
||||
"system": "跟隨系統"
|
||||
}
|
||||
},
|
||||
"tab": {
|
||||
"close_current": "Close current tab",
|
||||
"close_others": "Close all other tabs",
|
||||
"duplicate": "Duplicate current tab",
|
||||
"new_tab": "Open a new tab",
|
||||
"title": "Tabs"
|
||||
"close_current": "關閉目前分頁",
|
||||
"close_others": "關閉所有其他分頁",
|
||||
"duplicate": "複製目前分頁",
|
||||
"new_tab": "開啟新分頁",
|
||||
"title": "分頁"
|
||||
},
|
||||
"workspace": {
|
||||
"delete": "Delete current team",
|
||||
"edit": "Edit current team",
|
||||
"invite": "Invite people to team",
|
||||
"new": "Create new team",
|
||||
"switch_to_personal": "Switch to your personal workspace",
|
||||
"title": "Teams"
|
||||
"delete": "刪除目前團隊",
|
||||
"edit": "編輯目前團隊",
|
||||
"invite": "邀請他人加入團隊",
|
||||
"new": "建立新團隊",
|
||||
"switch_to_personal": "切換至您的個人工作區",
|
||||
"title": "團隊"
|
||||
}
|
||||
},
|
||||
"sse": {
|
||||
@@ -777,11 +777,11 @@
|
||||
"tab": {
|
||||
"authorization": "授權",
|
||||
"body": "請求本體",
|
||||
"close": "Close Tab",
|
||||
"close_others": "Close other Tabs",
|
||||
"close": "關閉分頁",
|
||||
"close_others": "關閉其他分頁",
|
||||
"collections": "集合",
|
||||
"documentation": "幫助文件",
|
||||
"duplicate": "Duplicate Tab",
|
||||
"duplicate": "複製分頁",
|
||||
"environments": "環境",
|
||||
"headers": "請求標頭",
|
||||
"history": "歷史記錄",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@hoppscotch/common",
|
||||
"private": true,
|
||||
"version": "2023.8.1",
|
||||
"version": "2023.8.2",
|
||||
"scripts": {
|
||||
"dev": "pnpm exec npm-run-all -p -l dev:*",
|
||||
"test": "vitest --run",
|
||||
|
||||
@@ -96,6 +96,7 @@ declare module 'vue' {
|
||||
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']
|
||||
@@ -142,6 +143,7 @@ declare module 'vue' {
|
||||
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']
|
||||
@@ -151,8 +153,10 @@ declare module 'vue' {
|
||||
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']
|
||||
IconLucideVerified: typeof import('~icons/lucide/verified')['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']
|
||||
|
||||
@@ -2,53 +2,16 @@
|
||||
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
|
||||
<AppShare :show="showShare" @hide-modal="showShare = false" />
|
||||
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam()"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
const showShortcuts = ref(false)
|
||||
const showShare = ref(false)
|
||||
const showLogin = ref(false)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const teamID = ref<string | null>(null)
|
||||
|
||||
const deleteTeam = () => {
|
||||
if (!teamID.value) return
|
||||
pipe(
|
||||
backendDeleteTeam(teamID.value),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// TODO: Better errors ? We know the possible errors now
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(err)
|
||||
},
|
||||
() => {
|
||||
invokeAction("workspace.switch.personal")
|
||||
toast.success(`${t("team.deleted")}`)
|
||||
}
|
||||
)
|
||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||
}
|
||||
|
||||
defineActionHandler("flyouts.keybinds.toggle", () => {
|
||||
showShortcuts.value = !showShortcuts.value
|
||||
})
|
||||
@@ -60,9 +23,4 @@ defineActionHandler("modals.share.toggle", () => {
|
||||
defineActionHandler("modals.login.toggle", () => {
|
||||
showLogin.value = !showLogin.value
|
||||
})
|
||||
|
||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||
teamID.value = teamId
|
||||
confirmRemove.value = true
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -231,29 +231,39 @@
|
||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||
@refetch-teams="refetchTeams"
|
||||
/>
|
||||
|
||||
<HoppSmartConfirmModal
|
||||
:show="confirmRemove"
|
||||
:title="t('confirm.remove_team')"
|
||||
@hide-modal="confirmRemove = false"
|
||||
@resolve="deleteTeam"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||
import { pwaDefferedPrompt, installPWA } from "@modules/pwa"
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { defineActionHandler, invokeAction } from "@helpers/actions"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { installPWA, pwaDefferedPrompt } from "@modules/pwa"
|
||||
import { breakpointsTailwind, useBreakpoints, useNetwork } from "@vueuse/core"
|
||||
import { computed, reactive, ref, watch } from "vue"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { GetMyTeamsQuery, TeamMemberRole } from "~/helpers/backend/graphql"
|
||||
import { getPlatformSpecialKey } from "~/helpers/platformutils"
|
||||
import { platform } from "~/platform"
|
||||
import IconDownload from "~icons/lucide/download"
|
||||
import IconLifeBuoy from "~icons/lucide/life-buoy"
|
||||
import IconSettings from "~icons/lucide/settings"
|
||||
import IconUploadCloud from "~icons/lucide/upload-cloud"
|
||||
import IconUser from "~icons/lucide/user"
|
||||
import IconUserPlus from "~icons/lucide/user-plus"
|
||||
import IconUsers from "~icons/lucide/users"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { deleteTeam as backendDeleteTeam } from "~/helpers/backend/mutations/Team"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -278,6 +288,9 @@ const currentUser = useReadonlyStream(
|
||||
platform.auth.getProbableUser()
|
||||
)
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
const teamID = ref<string | null>(null)
|
||||
|
||||
const selectedTeam = ref<GetMyTeamsQuery["myTeams"][number] | undefined>()
|
||||
|
||||
// TeamList-Adapter
|
||||
@@ -377,6 +390,24 @@ const handleTeamEdit = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteTeam = () => {
|
||||
if (!teamID.value) return
|
||||
pipe(
|
||||
backendDeleteTeam(teamID.value),
|
||||
TE.match(
|
||||
(err) => {
|
||||
// TODO: Better errors ? We know the possible errors now
|
||||
toast.error(`${t("error.something_went_wrong")}`)
|
||||
console.error(err)
|
||||
},
|
||||
() => {
|
||||
invokeAction("workspace.switch.personal")
|
||||
toast.success(`${t("team.deleted")}`)
|
||||
}
|
||||
)
|
||||
)() // Tasks (and TEs) are lazy, so call the function returned
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
const profile = ref<any | null>(null)
|
||||
@@ -405,6 +436,12 @@ defineActionHandler(
|
||||
computed(() => !currentUser.value)
|
||||
)
|
||||
|
||||
defineActionHandler("modals.team.delete", ({ teamId }) => {
|
||||
if (selectedTeam.value?.myRole !== TeamMemberRole.Owner) return noPermission()
|
||||
teamID.value = teamId
|
||||
confirmRemove.value = true
|
||||
})
|
||||
|
||||
const noPermission = () => {
|
||||
toast.error(`${t("profile.no_permission")}`)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
@@ -60,11 +61,12 @@ const emit = defineEmits<{
|
||||
|
||||
const editingName = ref("")
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = currentActiveTab.value.document.request.name
|
||||
editingName.value = tabs.currentActiveTab.value.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<HoppButtonSecondary
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
@@ -257,12 +257,27 @@
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-secondaryLight text-center">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex gap-4 flex-col items-stretch">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
@@ -288,8 +303,7 @@
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
:text="t('empty.folder')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
@@ -297,9 +311,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
@@ -312,7 +326,8 @@ import { useColorMode } from "@composables/theming"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
export type Collection = {
|
||||
type: "collections"
|
||||
@@ -520,7 +535,8 @@ const isSelected = ({
|
||||
}
|
||||
}
|
||||
|
||||
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||
const tabs = useService(RESTTabService)
|
||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||
|
||||
const isActiveRequest = (folderPath: string, requestIndex: number) => {
|
||||
return pipe(
|
||||
|
||||
@@ -82,12 +82,16 @@ import {
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab as activeRESTTab } from "~/helpers/rest/tab"
|
||||
import { currentActiveTab as activeGQLTab } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const RESTTabs = useService(RESTTabService)
|
||||
const GQLTabs = useService(GQLTabService)
|
||||
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
type CollectionType =
|
||||
@@ -123,13 +127,13 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const gqlRequestName = computedWithControl(
|
||||
() => activeGQLTab.value,
|
||||
() => activeGQLTab.value.document.request.name
|
||||
() => GQLTabs.currentActiveTab.value,
|
||||
() => GQLTabs.currentActiveTab.value.document.request.name
|
||||
)
|
||||
|
||||
const restRequestName = computedWithControl(
|
||||
() => activeRESTTab.value,
|
||||
() => activeRESTTab.value.document.request.name
|
||||
() => RESTTabs.currentActiveTab.value,
|
||||
() => RESTTabs.currentActiveTab.value.document.request.name
|
||||
)
|
||||
|
||||
const reqName = computed(() => {
|
||||
@@ -145,12 +149,14 @@ const reqName = computed(() => {
|
||||
const requestName = ref(reqName.value)
|
||||
|
||||
watch(
|
||||
() => [activeRESTTab.value, activeGQLTab.value],
|
||||
() => [RESTTabs.currentActiveTab.value, GQLTabs.currentActiveTab.value],
|
||||
() => {
|
||||
if (props.mode === "rest") {
|
||||
requestName.value = activeRESTTab.value?.document.request.name ?? ""
|
||||
requestName.value =
|
||||
RESTTabs.currentActiveTab.value?.document.request.name ?? ""
|
||||
} else {
|
||||
requestName.value = activeGQLTab.value?.document.request.name ?? ""
|
||||
requestName.value =
|
||||
GQLTabs.currentActiveTab.value?.document.request.name ?? ""
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -210,8 +216,8 @@ const saveRequestAs = async () => {
|
||||
|
||||
const requestUpdated =
|
||||
props.mode === "rest"
|
||||
? cloneDeep(activeRESTTab.value.document.request)
|
||||
: cloneDeep(activeGQLTab.value.document.request)
|
||||
? cloneDeep(RESTTabs.currentActiveTab.value.document.request)
|
||||
: cloneDeep(GQLTabs.currentActiveTab.value.document.request)
|
||||
|
||||
requestUpdated.name = requestName.value
|
||||
|
||||
@@ -224,7 +230,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
RESTTabs.currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -251,7 +257,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
RESTTabs.currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -279,7 +285,7 @@ const saveRequestAs = async () => {
|
||||
requestUpdated
|
||||
)
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
RESTTabs.currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -439,7 +445,7 @@ const updateTeamCollectionOrFolder = (
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
|
||||
activeRESTTab.value.document = {
|
||||
RESTTabs.currentActiveTab.value.document = {
|
||||
request: requestUpdated,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -460,7 +466,7 @@ const updateTeamCollectionOrFolder = (
|
||||
const requestSaved = () => {
|
||||
toast.success(`${t("request.added")}`)
|
||||
nextTick(() => {
|
||||
activeRESTTab.value.document.isDirty = false
|
||||
RESTTabs.currentActiveTab.value.document.isDirty = false
|
||||
})
|
||||
hideModal()
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@
|
||||
class="!rounded-none"
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
:label="t('add.new')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
:label="t('add.new')"
|
||||
class="!rounded-none"
|
||||
@click="emit('display-modal-add')"
|
||||
/>
|
||||
@@ -39,7 +39,7 @@
|
||||
collectionsType.type === 'team-collections' &&
|
||||
collectionsType.selectedTeam === undefined
|
||||
"
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
@click="emit('display-modal-import-export')"
|
||||
/>
|
||||
@@ -261,55 +261,68 @@
|
||||
/>
|
||||
</template>
|
||||
<template #emptyNode="{ node }">
|
||||
<div v-if="node === null">
|
||||
<div @drop="(e) => e.stopPropagation()">
|
||||
<HoppSmartPlaceholder
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="hasNoTeamAccess"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
<HoppSmartPlaceholder
|
||||
v-if="node === null"
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
@drop.stop
|
||||
>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-secondaryLight text-center">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex gap-4 flex-col items-stretch">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="
|
||||
hasNoTeamAccess ? null : emit('display-modal-import-export')
|
||||
"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:icon="IconPlus"
|
||||
:label="t('action.new')"
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="emit('display-modal-add')"
|
||||
:disabled="hasNoTeamAccess"
|
||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'collections'"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
@drop.stop
|
||||
>
|
||||
<HoppSmartPlaceholder
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
<div
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="
|
||||
node.data.type === 'collections' &&
|
||||
emit('add-folder', {
|
||||
path: node.id,
|
||||
folder: node.data.data.data,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-else-if="node.data.type === 'folders'"
|
||||
@drop="(e) => e.stopPropagation()"
|
||||
>
|
||||
<HoppSmartPlaceholder
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
:text="t('empty.folder')"
|
||||
>
|
||||
</HoppSmartPlaceholder>
|
||||
</div>
|
||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||
:alt="`${t('empty.folder')}`"
|
||||
:text="t('empty.folder')"
|
||||
@drop.stop
|
||||
/>
|
||||
</template>
|
||||
</HoppSmartTree>
|
||||
</div>
|
||||
@@ -317,9 +330,9 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import { computed, PropType, Ref, toRef } from "vue"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
@@ -335,10 +348,12 @@ import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { Picked } from "~/helpers/types/HoppPicked.js"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||
|
||||
@@ -536,7 +551,7 @@ const isSelected = ({
|
||||
}
|
||||
}
|
||||
|
||||
const active = computed(() => currentActiveTab.value.document.saveContext)
|
||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||
|
||||
const isActiveRequest = (requestID: string) => {
|
||||
return pipe(
|
||||
|
||||
@@ -36,11 +36,14 @@
|
||||
import { ref, watch } from "vue"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
folderPath?: string
|
||||
@@ -63,7 +66,7 @@ watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
if (show) {
|
||||
editingName.value = currentActiveTab.value?.document.request.name
|
||||
editingName.value = tabs.currentActiveTab.value?.document.request.name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -220,7 +220,8 @@ import {
|
||||
moveGraphqlRequest,
|
||||
} from "~/newstore/collections"
|
||||
import { Picked } from "~/helpers/types/HoppPicked"
|
||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
@@ -235,6 +236,8 @@ const colorMode = useColorMode()
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
// TODO: improve types plz
|
||||
const emit = defineEmits<{
|
||||
(e: "select", i: Picked | null): void
|
||||
@@ -295,7 +298,7 @@ const removeCollection = () => {
|
||||
emit("select", null)
|
||||
}
|
||||
|
||||
const possibleTabs = getTabsRefTo((tab) => {
|
||||
const possibleTabs = tabs.getTabsRefTo((tab) => {
|
||||
const ctx = tab.document.saveContext
|
||||
|
||||
if (!ctx) return false
|
||||
|
||||
@@ -203,12 +203,15 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
|
||||
import { computed, ref } from "vue"
|
||||
import { getTabsRefTo } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const toast = useToast()
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps({
|
||||
picked: { type: Object, default: null },
|
||||
// Whether the request is in a selectable mode (activates 'select' event)
|
||||
@@ -277,7 +280,7 @@ const removeFolder = () => {
|
||||
emit("select", { picked: null })
|
||||
}
|
||||
|
||||
const possibleTabs = getTabsRefTo((tab) => {
|
||||
const possibleTabs = tabs.getTabsRefTo((tab) => {
|
||||
const ctx = tab.document.saveContext
|
||||
|
||||
if (!ctx) return false
|
||||
|
||||
@@ -260,6 +260,13 @@ const importFromJSON = () => {
|
||||
|
||||
const exportJSON = () => {
|
||||
const dataToWrite = collectionJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
@@ -137,12 +137,8 @@ import { useToast } from "@composables/toast"
|
||||
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { removeGraphqlRequest } from "~/newstore/collections"
|
||||
import {
|
||||
createNewTab,
|
||||
getTabRefWithSaveContext,
|
||||
currentTabID,
|
||||
currentActiveTab,
|
||||
} from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
// Template refs
|
||||
const tippyActions = ref<any | null>(null)
|
||||
@@ -154,6 +150,8 @@ const deleteAction = ref<any | null>(null)
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps({
|
||||
// Whether the object is selected (show the tick mark)
|
||||
picked: { type: Object, default: null },
|
||||
@@ -165,7 +163,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const isActive = computed(() => {
|
||||
const saveCtx = currentActiveTab.value?.document.saveContext
|
||||
const saveCtx = tabs.currentActiveTab.value?.document.saveContext
|
||||
|
||||
if (!saveCtx) return false
|
||||
|
||||
@@ -201,7 +199,7 @@ const selectRequest = () => {
|
||||
if (props.saveRequest) {
|
||||
pick()
|
||||
} else {
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
@@ -209,11 +207,11 @@ const selectRequest = () => {
|
||||
|
||||
// Switch to that request if that request is open
|
||||
if (possibleTab) {
|
||||
currentTabID.value = possibleTab.value.id
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
return
|
||||
}
|
||||
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
@@ -253,7 +251,7 @@ const removeRequest = () => {
|
||||
}
|
||||
|
||||
// Detach the request from any of the tabs
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: props.folderPath,
|
||||
requestIndex: props.requestIndex,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
v-if="!saveRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('modal.import_export')"
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
</div>
|
||||
@@ -66,12 +66,27 @@
|
||||
:alt="`${t('empty.collections')}`"
|
||||
:text="t('empty.collections')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-secondaryLight text-center">
|
||||
{{ t("collection.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex gap-4 flex-col items-stretch">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="t('add.new')"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||
@@ -140,12 +155,13 @@ import {
|
||||
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useColorMode } from "@composables/theming"
|
||||
import { platform } from "~/platform"
|
||||
import { createNewTab, currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
@@ -158,14 +174,16 @@ export default defineComponent({
|
||||
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
return {
|
||||
collections,
|
||||
colorMode,
|
||||
t,
|
||||
tabs,
|
||||
IconPlus,
|
||||
IconHelpCircle,
|
||||
IconArchive,
|
||||
IconImport,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -267,13 +285,13 @@ export default defineComponent({
|
||||
},
|
||||
onAddRequest({ name, path, index }) {
|
||||
const newRequest = {
|
||||
...currentActiveTab.value.document.request,
|
||||
...this.tabs.currentActiveTab.value.document.request,
|
||||
name,
|
||||
}
|
||||
|
||||
saveGraphqlRequestAs(path, newRequest)
|
||||
|
||||
createNewTab({
|
||||
this.tabs.createNewTab({
|
||||
saveContext: {
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
|
||||
@@ -219,12 +219,6 @@ import {
|
||||
import * as E from "fp-ts/Either"
|
||||
import { platform } from "~/platform"
|
||||
import { createCollectionGists } from "~/helpers/gist"
|
||||
import {
|
||||
createNewTab,
|
||||
currentActiveTab,
|
||||
currentTabID,
|
||||
getTabRefWithSaveContext,
|
||||
} from "~/helpers/rest/tab"
|
||||
import {
|
||||
getRequestsByPath,
|
||||
resolveSaveContextOnRequestReorder,
|
||||
@@ -239,9 +233,11 @@ import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { WorkspaceService } from "~/services/workspace.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const props = defineProps({
|
||||
saveRequest: {
|
||||
@@ -377,22 +373,26 @@ const updateSelectedTeam = (team: SelectedTeam) => {
|
||||
const workspace = workspaceService.currentWorkspace
|
||||
|
||||
// Used to switch collection type and team when user switch workspace in the global workspace switcher
|
||||
// Check if there is a teamID in the workspace, if yes, switch to team collection and select the team
|
||||
// If there is no teamID, switch to my environment
|
||||
// Check if there is a teamID in the workspace, if yes, switch to team collections and select the team
|
||||
// If there is no teamID, switch to my collections
|
||||
watch(
|
||||
() => {
|
||||
const space = workspace.value
|
||||
|
||||
if (space.type === "personal") return undefined
|
||||
else return space.teamID
|
||||
return space.type === "personal" ? undefined : space.teamID
|
||||
},
|
||||
(teamID) => {
|
||||
if (!teamID) {
|
||||
switchToMyCollections()
|
||||
} else if (teamID) {
|
||||
if (teamID) {
|
||||
const team = myTeams.value?.find((t) => t.id === teamID)
|
||||
if (team) updateSelectedTeam(team)
|
||||
if (team) {
|
||||
updateSelectedTeam(team)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return switchToMyCollections()
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -650,7 +650,7 @@ const addRequest = (payload: {
|
||||
|
||||
const onAddRequest = (requestName: string) => {
|
||||
const newRequest = {
|
||||
...cloneDeep(currentActiveTab.value.document.request),
|
||||
...cloneDeep(tabs.currentActiveTab.value.document.request),
|
||||
name: requestName,
|
||||
}
|
||||
|
||||
@@ -659,7 +659,7 @@ const onAddRequest = (requestName: string) => {
|
||||
if (!path) return
|
||||
const insertionIndex = saveRESTRequestAs(path, newRequest)
|
||||
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -708,7 +708,7 @@ const onAddRequest = (requestName: string) => {
|
||||
(result) => {
|
||||
const { createRequestInCollection } = result
|
||||
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: newRequest,
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -931,7 +931,7 @@ const updateEditingRequest = (newName: string) => {
|
||||
|
||||
if (folderPath === null || requestIndex === null) return
|
||||
|
||||
const possibleActiveTab = getTabRefWithSaveContext({
|
||||
const possibleActiveTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
requestIndex,
|
||||
folderPath,
|
||||
@@ -975,7 +975,7 @@ const updateEditingRequest = (newName: string) => {
|
||||
)
|
||||
)()
|
||||
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID,
|
||||
})
|
||||
@@ -1211,7 +1211,7 @@ const onRemoveRequest = () => {
|
||||
emit("select", null)
|
||||
}
|
||||
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath,
|
||||
requestIndex,
|
||||
@@ -1271,7 +1271,7 @@ const onRemoveRequest = () => {
|
||||
)()
|
||||
|
||||
// If there is a tab attached to this request, dissociate its state and mark it dirty
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID,
|
||||
})
|
||||
@@ -1304,14 +1304,14 @@ const selectRequest = (selectedRequest: {
|
||||
let possibleTab = null
|
||||
|
||||
if (collectionsType.value.type === "team-collections") {
|
||||
possibleTab = getTabRefWithSaveContext({
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
})
|
||||
if (possibleTab) {
|
||||
currentTabID.value = possibleTab.value.id
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
} else {
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: cloneDeep(request),
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -1321,16 +1321,16 @@ const selectRequest = (selectedRequest: {
|
||||
})
|
||||
}
|
||||
} else {
|
||||
possibleTab = getTabRefWithSaveContext({
|
||||
possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
requestIndex: parseInt(requestIndex),
|
||||
folderPath: folderPath!,
|
||||
})
|
||||
if (possibleTab) {
|
||||
currentTabID.value = possibleTab.value.id
|
||||
tabs.setActiveTab(possibleTab.value.id)
|
||||
} else {
|
||||
// If not, open the request in a new tab
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: cloneDeep(request),
|
||||
isDirty: false,
|
||||
saveContext: {
|
||||
@@ -1373,7 +1373,7 @@ const dropRequest = (payload: {
|
||||
destinationCollectionIndex
|
||||
)
|
||||
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath,
|
||||
requestIndex: pathToLastIndex(requestIndex),
|
||||
@@ -1422,7 +1422,7 @@ const dropRequest = (payload: {
|
||||
1
|
||||
)
|
||||
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const possibleTab = tabs.getTabRefWithSaveContext({
|
||||
originLocation: "team-collection",
|
||||
requestID: requestIndex,
|
||||
})
|
||||
@@ -1938,6 +1938,12 @@ const exportJSONCollection = async () => {
|
||||
|
||||
await getJSONCollection()
|
||||
|
||||
const parsedCollections = JSON.parse(collectionJSON.value)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_collections_to_export"))
|
||||
}
|
||||
|
||||
initializeDownloadCollection(collectionJSON.value, null)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,11 +83,14 @@ import {
|
||||
import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import { updateTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
position: { top: number; left: number }
|
||||
@@ -189,8 +192,8 @@ const addEnvironment = async () => {
|
||||
//replace the current tab endpoint with the variable name with << and >>
|
||||
const variableName = `<<${editingName.value}>>`
|
||||
//replace the currenttab endpoint containing the value in the text with variablename
|
||||
currentActiveTab.value.document.request.endpoint =
|
||||
currentActiveTab.value.document.request.endpoint.replace(
|
||||
tabs.currentActiveTab.value.document.request.endpoint =
|
||||
tabs.currentActiveTab.value.document.request.endpoint.replace(
|
||||
editingValue.value,
|
||||
variableName
|
||||
)
|
||||
|
||||
@@ -377,6 +377,13 @@ const importFromPostman = ({
|
||||
|
||||
const exportJSON = () => {
|
||||
const dataToWrite = environmentJson.value
|
||||
|
||||
const parsedCollections = JSON.parse(dataToWrite)
|
||||
|
||||
if (!parsedCollections.length) {
|
||||
return toast.error(t("error.no_environments_to_export"))
|
||||
}
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
@@ -478,7 +478,8 @@ watch(
|
||||
teamEnvListAdapter.changeTeamID(newVal.teamID)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const selectedEnv = computed(() => {
|
||||
|
||||
@@ -60,17 +60,7 @@
|
||||
:placeholder="`${t('count.value', { count: index + 1 })}`"
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
:secret="env.secret"
|
||||
/>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.secret')"
|
||||
:icon="env.secret ? IconEyeOff : IconEye"
|
||||
@click="toggleEnvironmentSecret(index)"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
@@ -120,8 +110,6 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconTrash from "~icons/lucide/trash"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconEyeOff from "~icons/lucide/eye-off"
|
||||
import { clone } from "lodash-es"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import * as E from "fp-ts/Either"
|
||||
@@ -152,7 +140,6 @@ type EnvironmentVariable = {
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,7 +172,7 @@ const idTicker = ref(0)
|
||||
|
||||
const editingName = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
|
||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||
@@ -216,8 +203,6 @@ const workingEnv = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const oldEnvironments = ref<string[]>([])
|
||||
|
||||
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||
|
||||
const evnExpandError = computed(() => {
|
||||
@@ -249,28 +234,6 @@ const liveEnvs = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(liveEnvs, (newLiveEnv, oldLiveEnv) => {
|
||||
const oldEnvLength = oldLiveEnv.length
|
||||
const newEnvLength = newLiveEnv.length
|
||||
if (oldEnvLength === newEnvLength) {
|
||||
const _oldEnvironments = []
|
||||
for (let i = 0; i < newEnvLength; i++) {
|
||||
const newVar = newLiveEnv[i]
|
||||
const oldVar = oldLiveEnv[i]
|
||||
let newValue = ""
|
||||
if (!newVar.secret) {
|
||||
newValue = newVar.value
|
||||
} else if (!oldVar.secret) {
|
||||
newValue = oldVar.value
|
||||
} else {
|
||||
newValue = oldEnvironments.value[i]
|
||||
}
|
||||
_oldEnvironments.push(newValue)
|
||||
}
|
||||
oldEnvironments.value = _oldEnvironments
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
@@ -299,7 +262,6 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -308,23 +270,12 @@ const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const toggleEnvironmentSecret = (index: number) => {
|
||||
vars.value[index].env.secret = !vars.value[index].env.secret
|
||||
}
|
||||
|
||||
const saveEnvironment = () => {
|
||||
if (!editingName.value) {
|
||||
toast.error(`${t("environment.invalid_name")}`)
|
||||
return
|
||||
}
|
||||
const _vars = vars.value
|
||||
for (let i = 0; i < vars.value.length; i++) {
|
||||
const value = oldEnvironments.value[i]
|
||||
if (value) {
|
||||
_vars[i].env.value = value
|
||||
}
|
||||
}
|
||||
vars.value = _vars
|
||||
|
||||
const filterdVariables = pipe(
|
||||
vars.value,
|
||||
A.filterMap(
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
role="menu"
|
||||
@keyup.e="edit!.$el.click()"
|
||||
@keyup.d="duplicate!.$el.click()"
|
||||
@keyup.j="exportAsJsonEl!.$el.click()"
|
||||
@keyup.delete="
|
||||
!(environmentIndex === 'Global')
|
||||
? deleteAction!.$el.click()
|
||||
@@ -77,6 +78,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
:shortcut="['J']"
|
||||
@click="
|
||||
() => {
|
||||
exportEnvironmentAsJSON()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
v-if="environmentIndex !== 'Global'"
|
||||
ref="deleteAction"
|
||||
@@ -121,6 +134,7 @@ import { useI18n } from "@composables/i18n"
|
||||
import { useToast } from "@composables/toast"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -136,10 +150,18 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () => {
|
||||
const { environment, environmentIndex } = props
|
||||
exportAsJSON(environment, environmentIndex)
|
||||
? toast.success(t("state.download_started"))
|
||||
: toast.error(t("state.download_failed"))
|
||||
}
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const exportAsJsonEl = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
@@ -33,17 +33,32 @@
|
||||
@edit-environment="editEnvironment(index)"
|
||||
/>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="environments.length === 0"
|
||||
v-if="!environments.length"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-secondaryLight text-center">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex gap-4 flex-col items-stretch">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:icon="IconPlus"
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<EnvironmentsMyDetails
|
||||
:show="showModalDetails"
|
||||
@@ -66,8 +81,8 @@ import { environments$ } from "~/newstore/environments"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import { useReadonlyStream } from "@composables/stream"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
@@ -63,17 +63,7 @@
|
||||
:envs="liveEnvs"
|
||||
:name="'value' + index"
|
||||
:readonly="isViewer"
|
||||
:secret="env.secret"
|
||||
/>
|
||||
<div v-if="!isViewer" class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.secret')"
|
||||
:icon="env.secret ? IconEyeOff : IconEye"
|
||||
@click="toggleEnvironmentSecret(index)"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isViewer" class="flex">
|
||||
<HoppButtonSecondary
|
||||
id="variable"
|
||||
@@ -149,8 +139,6 @@ import IconTrash from "~icons/lucide/trash"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconDone from "~icons/lucide/check"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconEye from "~icons/lucide/eye"
|
||||
import IconEyeOff from "~icons/lucide/eye-off"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
type EnvironmentVariable = {
|
||||
@@ -158,7 +146,6 @@ type EnvironmentVariable = {
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,10 +182,9 @@ const idTicker = ref(0)
|
||||
|
||||
const editingName = ref<string | null>(null)
|
||||
const vars = ref<EnvironmentVariable[]>([
|
||||
{ id: idTicker.value++, env: { key: "", value: "", secret: false } },
|
||||
{ id: idTicker.value++, env: { key: "", value: "" } },
|
||||
])
|
||||
|
||||
const oldEnvironments = ref<string[]>([])
|
||||
const clearIcon = refAutoReset<typeof IconTrash2 | typeof IconDone>(
|
||||
IconTrash2,
|
||||
1000
|
||||
@@ -226,28 +212,6 @@ const liveEnvs = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(liveEnvs, (newLiveEnv, oldLiveEnv) => {
|
||||
const oldEnvLength = oldLiveEnv.length
|
||||
const newEnvLength = newLiveEnv.length
|
||||
if (oldEnvLength === newEnvLength) {
|
||||
const _oldEnvironments = []
|
||||
for (let i = 0; i < newEnvLength; i++) {
|
||||
const newVar = newLiveEnv[i]
|
||||
const oldVar = oldLiveEnv[i]
|
||||
let newValue = ""
|
||||
if (!newVar.secret) {
|
||||
newValue = newVar.value
|
||||
} else if (!oldVar.secret) {
|
||||
newValue = oldVar.value
|
||||
} else {
|
||||
newValue = oldEnvironments.value[i]
|
||||
}
|
||||
_oldEnvironments.push(newValue)
|
||||
}
|
||||
oldEnvironments.value = _oldEnvironments
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
(show) => {
|
||||
@@ -256,7 +220,7 @@ watch(
|
||||
editingName.value = null
|
||||
vars.value = pipe(
|
||||
props.envVars() ?? [],
|
||||
A.map((e: { key: string; value: string; secret: boolean }) => ({
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
@@ -265,7 +229,7 @@ watch(
|
||||
editingName.value = props.editingEnvironment.environment.name ?? null
|
||||
vars.value = pipe(
|
||||
props.editingEnvironment.environment.variables ?? [],
|
||||
A.map((e: { key: string; value: string; secret: boolean }) => ({
|
||||
A.map((e: { key: string; value: string }) => ({
|
||||
id: idTicker.value++,
|
||||
env: clone(e),
|
||||
}))
|
||||
@@ -287,7 +251,6 @@ const addEnvironmentVariable = () => {
|
||||
env: {
|
||||
key: "",
|
||||
value: "",
|
||||
secret: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -296,10 +259,6 @@ const removeEnvironmentVariable = (index: number) => {
|
||||
vars.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const toggleEnvironmentSecret = (index: number) => {
|
||||
vars.value[index].env.secret = !vars.value[index].env.secret
|
||||
}
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const saveEnvironment = async () => {
|
||||
@@ -310,15 +269,6 @@ const saveEnvironment = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const _vars = vars.value
|
||||
for (let i = 0; i < vars.value.length; i++) {
|
||||
const value = oldEnvironments.value[i]
|
||||
if (value) {
|
||||
_vars[i].env.value = value
|
||||
}
|
||||
}
|
||||
vars.value = _vars
|
||||
|
||||
const filterdVariables = pipe(
|
||||
vars.value,
|
||||
A.filterMap(
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
role="menu"
|
||||
@keyup.e="edit!.$el.click()"
|
||||
@keyup.d="duplicate!.$el.click()"
|
||||
@keyup.j="exportAsJsonEl!.$el.click()"
|
||||
@keyup.delete="deleteAction!.$el.click()"
|
||||
@keyup.escape="options!.tippy().hide()"
|
||||
>
|
||||
@@ -54,6 +55,7 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
|
||||
<HoppSmartItem
|
||||
ref="duplicate"
|
||||
:icon="IconCopy"
|
||||
@@ -66,6 +68,18 @@
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="exportAsJsonEl"
|
||||
:icon="IconEdit"
|
||||
:label="`${t('export.as_json')}`"
|
||||
:shortcut="['J']"
|
||||
@click="
|
||||
() => {
|
||||
exportEnvironmentAsJSON()
|
||||
hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<HoppSmartItem
|
||||
ref="deleteAction"
|
||||
:icon="IconTrash2"
|
||||
@@ -109,6 +123,7 @@ import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { HoppSmartItem } from "@hoppscotch/ui"
|
||||
import { exportAsJSON } from "~/helpers/import-export/export/environment"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
@@ -124,11 +139,17 @@ const emit = defineEmits<{
|
||||
|
||||
const confirmRemove = ref(false)
|
||||
|
||||
const exportEnvironmentAsJSON = () =>
|
||||
exportAsJSON(props.environment)
|
||||
? toast.success(t("state.download_started"))
|
||||
: toast.error(t("state.download_failed"))
|
||||
|
||||
const tippyActions = ref<TippyComponent | null>(null)
|
||||
const options = ref<TippyComponent | null>(null)
|
||||
const edit = ref<typeof HoppSmartItem>()
|
||||
const duplicate = ref<typeof HoppSmartItem>()
|
||||
const deleteAction = ref<typeof HoppSmartItem>()
|
||||
const exportAsJsonEl = ref<typeof HoppSmartItem>()
|
||||
|
||||
const removeEnvironment = () => {
|
||||
pipe(
|
||||
|
||||
@@ -31,40 +31,49 @@
|
||||
v-if="team !== undefined && team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArchive"
|
||||
:icon="IconImport"
|
||||
:title="t('modal.import_export')"
|
||||
@click="displayModalImportExport(true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HoppSmartPlaceholder
|
||||
v-if="!loading && teamEnvironments.length === 0 && !adapterError"
|
||||
v-if="!loading && !teamEnvironments.length && !adapterError"
|
||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||
:alt="`${t('empty.environments')}`"
|
||||
:text="t('empty.environments')"
|
||||
>
|
||||
<HoppButtonSecondary
|
||||
v-if="team === undefined || team.myRole === 'VIEWER'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
disabled
|
||||
filled
|
||||
:icon="IconPlus"
|
||||
:title="t('team.no_access')"
|
||||
:label="t('action.new')"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
@click="displayModalAdd(true)"
|
||||
/>
|
||||
<div class="flex flex-col items-center space-y-4">
|
||||
<span class="text-secondaryLight text-center">
|
||||
{{ t("environment.import_or_create") }}
|
||||
</span>
|
||||
<div class="flex gap-4 flex-col items-stretch">
|
||||
<HoppButtonPrimary
|
||||
:icon="IconImport"
|
||||
:label="t('import.title')"
|
||||
filled
|
||||
outline
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
:label="`${t('add.new')}`"
|
||||
filled
|
||||
outline
|
||||
:icon="IconPlus"
|
||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||
:disabled="isTeamViewer"
|
||||
@click="isTeamViewer ? null : displayModalAdd(true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</HoppSmartPlaceholder>
|
||||
<div v-else-if="!loading">
|
||||
<EnvironmentsTeamsEnvironment
|
||||
@@ -108,14 +117,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue"
|
||||
import { computed, ref } from "vue"
|
||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { useColorMode } from "~/composables/theming"
|
||||
import IconPlus from "~icons/lucide/plus"
|
||||
import IconArchive from "~icons/lucide/archive"
|
||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||
import IconImport from "~icons/lucide/folder-down"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||
|
||||
@@ -138,6 +147,8 @@ const action = ref<"new" | "edit">("edit")
|
||||
const editingEnvironment = ref<TeamEnvironment | null>(null)
|
||||
const editingVariableName = ref("")
|
||||
|
||||
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
|
||||
|
||||
const displayModalAdd = (shouldDisplay: boolean) => {
|
||||
action.value = "new"
|
||||
showModalDetails.value = shouldDisplay
|
||||
|
||||
@@ -64,7 +64,6 @@
|
||||
<script setup lang="ts">
|
||||
import { platform } from "~/platform"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computed, ref, watch } from "vue"
|
||||
import { connection } from "~/helpers/graphql/connection"
|
||||
import { connect } from "~/helpers/graphql/connection"
|
||||
@@ -72,8 +71,10 @@ import { disconnect } from "~/helpers/graphql/connection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { useService } from "dioc/vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
@@ -82,9 +83,9 @@ const connectionSwitchModal = ref(false)
|
||||
const connected = computed(() => connection.state === "CONNECTED")
|
||||
|
||||
const url = computed({
|
||||
get: () => currentActiveTab.value?.document.request.url ?? "",
|
||||
get: () => tabs.currentActiveTab.value?.document.request.url ?? "",
|
||||
set: (value) => {
|
||||
currentActiveTab.value!.document.request.url = value
|
||||
tabs.currentActiveTab.value!.document.request.url = value
|
||||
},
|
||||
})
|
||||
|
||||
@@ -97,7 +98,7 @@ const onConnectClick = () => {
|
||||
}
|
||||
|
||||
const gqlConnect = () => {
|
||||
connect(url.value, currentActiveTab.value?.document.request.headers)
|
||||
connect(url.value, tabs.currentActiveTab.value?.document.request.headers)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REQUEST_RUN",
|
||||
@@ -114,7 +115,7 @@ const switchConnection = () => {
|
||||
const lastTwoUrls = ref<string[]>([])
|
||||
|
||||
watch(
|
||||
currentActiveTab,
|
||||
tabs.currentActiveTab,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
lastTwoUrls.value.push(newVal.document.request.url)
|
||||
|
||||
@@ -58,8 +58,7 @@ import { computed, ref, watch } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { platform } from "~/platform"
|
||||
import { currentActiveTab } from "~/helpers/graphql/tab"
|
||||
import { computedWithControl } from "@vueuse/core"
|
||||
import { computedWithControl, useVModel } from "@vueuse/core"
|
||||
import {
|
||||
GQLResponseEvent,
|
||||
runGQLOperation,
|
||||
@@ -68,26 +67,39 @@ import {
|
||||
import { useService } from "dioc/vue"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { editGraphqlRequest } from "~/newstore/collections"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const VALID_GQL_OPERATIONS = [
|
||||
"query",
|
||||
"headers",
|
||||
"variables",
|
||||
"authorization",
|
||||
] as const
|
||||
|
||||
export type GQLOptionTabs = (typeof VALID_GQL_OPERATIONS)[number]
|
||||
|
||||
export type GQLOptionTabs = "query" | "headers" | "variables" | "authorization"
|
||||
const selectedOptionTab = ref<GQLOptionTabs>("query")
|
||||
const interceptorService = useService(InterceptorService)
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppGQLRequest
|
||||
response?: GQLResponseEvent[] | null
|
||||
optionTab?: GQLOptionTabs
|
||||
tabId: string
|
||||
}>(),
|
||||
{
|
||||
response: null,
|
||||
optionTab: "query",
|
||||
}
|
||||
)
|
||||
const emit = defineEmits(["update:modelValue", "update:response"])
|
||||
const selectedOptionTab = useVModel(props, "optionTab", emit)
|
||||
|
||||
const request = ref(props.modelValue)
|
||||
|
||||
@@ -100,8 +112,8 @@ watch(
|
||||
)
|
||||
|
||||
const url = computedWithControl(
|
||||
() => currentActiveTab.value,
|
||||
() => currentActiveTab.value.document.request.url
|
||||
() => tabs.currentActiveTab.value,
|
||||
() => tabs.currentActiveTab.value.document.request.url
|
||||
)
|
||||
|
||||
const activeGQLHeadersCount = computed(
|
||||
@@ -136,6 +148,9 @@ const runQuery = async (
|
||||
const duration = Date.now() - startTime
|
||||
completePageProgress()
|
||||
toast.success(`${t("state.finished_in", { duration })}`)
|
||||
if (definition?.operation === "subscription" && request.value.auth) {
|
||||
toast.success(t("authorization.graphql_headers"))
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.log(e)
|
||||
// response.value = [`${e}`]
|
||||
@@ -182,17 +197,17 @@ const hideRequestModal = () => {
|
||||
}
|
||||
const saveRequest = () => {
|
||||
if (
|
||||
currentActiveTab.value.document.saveContext &&
|
||||
currentActiveTab.value.document.saveContext.originLocation ===
|
||||
tabs.currentActiveTab.value.document.saveContext &&
|
||||
tabs.currentActiveTab.value.document.saveContext.originLocation ===
|
||||
"user-collection"
|
||||
) {
|
||||
editGraphqlRequest(
|
||||
currentActiveTab.value.document.saveContext.folderPath,
|
||||
currentActiveTab.value.document.saveContext.requestIndex,
|
||||
currentActiveTab.value.document.request
|
||||
tabs.currentActiveTab.value.document.saveContext.folderPath,
|
||||
tabs.currentActiveTab.value.document.saveContext.requestIndex,
|
||||
tabs.currentActiveTab.value.document.request
|
||||
)
|
||||
|
||||
currentActiveTab.value.document.isDirty = false
|
||||
tabs.currentActiveTab.value.document.isDirty = false
|
||||
} else {
|
||||
showSaveRequestModal.value = true
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
<template #primary>
|
||||
<GraphqlRequestOptions
|
||||
v-model="tab.document.request"
|
||||
v-model:response="tab.response"
|
||||
v-model:response="tab.document.response"
|
||||
v-model:option-tab="tab.document.optionTabPreference"
|
||||
:tab-id="tab.id"
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<GraphqlResponse :response="tab.response" />
|
||||
<GraphqlResponse :response="tab.document.response" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
@@ -18,14 +19,15 @@ import { useVModel } from "@vueuse/core"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { watch } from "vue"
|
||||
import { isEqualHoppGQLRequest } from "~/helpers/graphql"
|
||||
import { HoppGQLTab } from "~/helpers/graphql/tab"
|
||||
import { HoppGQLDocument } from "~/helpers/graphql/document"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
|
||||
// TODO: Move Response and Request execution code to over here
|
||||
|
||||
const props = defineProps<{ modelValue: HoppGQLTab }>()
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppGQLDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppGQLTab): void
|
||||
(e: "update:modelValue", val: HoppTab<HoppGQLDocument>): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
@@ -92,12 +92,13 @@ import IconXCircle from "~icons/lucide/x-circle"
|
||||
import IconXSquare from "~icons/lucide/x-square"
|
||||
import IconFileEdit from "~icons/lucide/file-edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import { HoppGQLTab } from "~/helpers/graphql/tab"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppGQLDocument } from "~/helpers/graphql/document"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
tab: HoppGQLTab
|
||||
tab: HoppTab<HoppGQLDocument>
|
||||
isRemovable: boolean
|
||||
}>()
|
||||
|
||||
|
||||
@@ -67,9 +67,11 @@ import IconMaximize2 from "~icons/lucide/maximize-2"
|
||||
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { makeGQLRequest } from "@hoppscotch/data"
|
||||
import { createNewTab } from "~/helpers/graphql/tab"
|
||||
import { useService } from "dioc/vue"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
entry: GQLHistoryEntry
|
||||
@@ -93,7 +95,7 @@ const query = computed(() =>
|
||||
)
|
||||
|
||||
const useEntry = () => {
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: makeGQLRequest({
|
||||
name: props.entry.request.name,
|
||||
url: props.entry.request.url,
|
||||
|
||||
@@ -176,8 +176,9 @@ import {
|
||||
|
||||
import HistoryRestCard from "./rest/Card.vue"
|
||||
import HistoryGraphqlCard from "./graphql/Card.vue"
|
||||
import { createNewTab } from "~/helpers/rest/tab"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { useService } from "dioc/vue"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
type HistoryEntry = GQLHistoryEntry | RESTHistoryEntry
|
||||
|
||||
@@ -293,8 +294,9 @@ const clearHistory = () => {
|
||||
|
||||
// NOTE: For GQL, the HistoryGraphqlCard component already implements useEntry
|
||||
// (That is not a really good behaviour tho ¯\_(ツ)_/¯)
|
||||
const tabs = useService(RESTTabService)
|
||||
const useHistory = (entry: RESTHistoryEntry) => {
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: entry.request,
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
@@ -59,7 +59,9 @@
|
||||
:key="`contentTypeItem-${contentTypeIndex}`"
|
||||
:label="contentTypeItem"
|
||||
:info-icon="
|
||||
contentTypeItem === body.contentType ? IconDone : null
|
||||
contentTypeItem === body.contentType
|
||||
? IconDone
|
||||
: undefined
|
||||
"
|
||||
:active-info-icon="contentTypeItem === body.contentType"
|
||||
@click="
|
||||
@@ -136,7 +138,7 @@ import IconDone from "~icons/lucide/check"
|
||||
import IconExternalLink from "~icons/lucide/external-link"
|
||||
import IconInfo from "~icons/lucide/info"
|
||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||
import { RequestOptionTabs } from "./RequestOptions.vue"
|
||||
import { RESTOptionTabs } from "./RequestOptions.vue"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
const t = useI18n()
|
||||
@@ -147,7 +149,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "change-tab", value: RequestOptionTabs): void
|
||||
(e: "change-tab", value: RESTOptionTabs): void
|
||||
(e: "update:headers", value: HoppRESTHeader[]): void
|
||||
(e: "update:body", value: HoppRESTReqBody): void
|
||||
}>()
|
||||
@@ -164,7 +166,7 @@ const overridenContentType = computed(() =>
|
||||
)
|
||||
)
|
||||
|
||||
const contentTypeOverride = (tab: RequestOptionTabs) => {
|
||||
const contentTypeOverride = (tab: RESTOptionTabs) => {
|
||||
emit("change-tab", tab)
|
||||
if (!isContentTypeAlreadyExist()) {
|
||||
// TODO: Fix this
|
||||
|
||||
@@ -157,9 +157,10 @@ import {
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import cloneDeep from "lodash-es/cloneDeep"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
@@ -173,7 +174,8 @@ const emit = defineEmits<{
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const request = ref(cloneDeep(currentActiveTab.value.document.request))
|
||||
const tabs = useService(RESTTabService)
|
||||
const request = ref(cloneDeep(tabs.currentActiveTab.value.document.request))
|
||||
const codegenType = ref<CodegenName>("shell-curl")
|
||||
const errorState = ref(false)
|
||||
|
||||
@@ -242,7 +244,7 @@ watch(
|
||||
() => props.show,
|
||||
(goingToShow) => {
|
||||
if (goingToShow) {
|
||||
request.value = cloneDeep(currentActiveTab.value.document.request)
|
||||
request.value = cloneDeep(tabs.currentActiveTab.value.document.request)
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_CODEGEN_OPENED",
|
||||
|
||||
@@ -185,18 +185,24 @@
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-if="header.source === 'auth'"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t(masking ? 'state.show' : 'state.hide')"
|
||||
:icon="masking ? IconEye : IconEyeOff"
|
||||
@click="toggleMask()"
|
||||
/>
|
||||
<HoppButtonSecondary
|
||||
v-else
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArrowUpRight"
|
||||
:title="t('request.go_to_authorization_tab')"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<HoppButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:icon="IconArrowUpRight"
|
||||
:title="t('request.go_to_authorization_tab')"
|
||||
@click="changeTab(header.source)"
|
||||
/>
|
||||
</span>
|
||||
@@ -267,10 +273,13 @@ import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { currentTabID } from "~/helpers/rest/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const idTicker = ref(0)
|
||||
@@ -509,13 +518,13 @@ const changeTab = (tab: ComputedHeader["source"]) => {
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const headerKeyResults = inspectionService.getResultViewFor(
|
||||
currentTabID.value,
|
||||
tabs.currentTabID.value,
|
||||
(result) =>
|
||||
result.locations.type === "header" && result.locations.position === "key"
|
||||
)
|
||||
|
||||
const headerValueResults = inspectionService.getResultViewFor(
|
||||
currentTabID.value,
|
||||
tabs.currentTabID.value,
|
||||
(result) =>
|
||||
result.locations.type === "header" && result.locations.position === "value"
|
||||
)
|
||||
|
||||
@@ -93,13 +93,16 @@ import IconWrapText from "~icons/lucide/wrap-text"
|
||||
import IconClipboard from "~icons/lucide/clipboard"
|
||||
import IconCheck from "~icons/lucide/check"
|
||||
import IconTrash2 from "~icons/lucide/trash-2"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const curl = ref("")
|
||||
|
||||
const curlEditor = ref<any | null>(null)
|
||||
@@ -149,7 +152,7 @@ const handleImport = () => {
|
||||
type: "HOPP_REST_IMPORT_CURL",
|
||||
})
|
||||
|
||||
currentActiveTab.value.document.request = req
|
||||
tabs.currentActiveTab.value.document.request = req
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
toast.error(`${t("error.curl_invalid_format")}`)
|
||||
|
||||
@@ -202,12 +202,13 @@ import { objRemoveKey } from "@functional/object"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService, InspectorResult } from "~/services/inspection"
|
||||
import { currentTabID } from "~/helpers/rest/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const colorMode = useColorMode()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const idTicker = ref(0)
|
||||
|
||||
@@ -410,13 +411,13 @@ const clearContent = () => {
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const parameterKeyResults = inspectionService.getResultViewFor(
|
||||
currentTabID.value,
|
||||
tabs.currentTabID.value,
|
||||
(result) =>
|
||||
result.locations.type === "parameter" && result.locations.position === "key"
|
||||
)
|
||||
|
||||
const parameterValueResults = inspectionService.getResultViewFor(
|
||||
currentTabID.value,
|
||||
tabs.currentTabID.value,
|
||||
(result) =>
|
||||
result.locations.type === "parameter" &&
|
||||
result.locations.position === "value"
|
||||
|
||||
@@ -217,6 +217,7 @@
|
||||
@hide-modal="showCurlImportModal = false"
|
||||
/>
|
||||
<HttpCodegenModal
|
||||
v-if="showCodegenModal"
|
||||
:show="showCodegenModal"
|
||||
@hide-modal="showCodegenModal = false"
|
||||
/>
|
||||
@@ -257,7 +258,6 @@ import IconLink2 from "~icons/lucide/link-2"
|
||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||
import IconSave from "~icons/lucide/save"
|
||||
import IconShare2 from "~icons/lucide/share-2"
|
||||
import { HoppRESTTab, currentTabID } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { RESTHistoryEntry, restHistory$ } from "~/newstore/history"
|
||||
import { platform } from "~/platform"
|
||||
@@ -265,6 +265,9 @@ import { HoppGQLRequest, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const t = useI18n()
|
||||
const interceptorService = useService(InterceptorService)
|
||||
@@ -286,7 +289,7 @@ const toast = useToast()
|
||||
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
|
||||
const emit = defineEmits(["update:modelValue"])
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
@@ -426,7 +429,7 @@ const updateMethod = (method: string) => {
|
||||
|
||||
const onSelectMethod = (e: Event | any) => {
|
||||
// type any because of value property not being recognized by TS in the event.target object. It is a valid property though.
|
||||
updateMethod(e.value)
|
||||
updateMethod(e.target.value)
|
||||
}
|
||||
|
||||
const clearContent = () => {
|
||||
@@ -434,7 +437,7 @@ const clearContent = () => {
|
||||
}
|
||||
|
||||
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||
tab.value.response = response
|
||||
tab.value.document.response = response
|
||||
}
|
||||
|
||||
const copyLinkIcon = refAutoReset<
|
||||
@@ -642,5 +645,6 @@ const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const tabResults = inspectionService.getResultViewFor(currentTabID.value)
|
||||
const tabs = useService(RESTTabService)
|
||||
const tabResults = inspectionService.getResultViewFor(tabs.currentTabID.value)
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<HoppSmartTabs
|
||||
v-model="selectedOptionsTab"
|
||||
v-model="selectedOptionTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
render-inactive-tabs
|
||||
>
|
||||
@@ -15,7 +15,7 @@
|
||||
<HttpBody
|
||||
v-model:headers="request.headers"
|
||||
v-model:body="request.body"
|
||||
@change-tab="changeTab"
|
||||
@change-tab="changeOptionTab"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
@@ -23,7 +23,7 @@
|
||||
:label="`${t('tab.headers')}`"
|
||||
:info="`${newActiveHeadersCount$}`"
|
||||
>
|
||||
<HttpHeaders v-model="request" @change-tab="changeTab" />
|
||||
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
||||
<HttpAuthorization v-model="request.auth" />
|
||||
@@ -55,31 +55,43 @@
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed, ref } from "vue"
|
||||
import { computed } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
|
||||
export type RequestOptionTabs =
|
||||
| "params"
|
||||
| "bodyParams"
|
||||
| "headers"
|
||||
| "authorization"
|
||||
| "preRequestScript"
|
||||
| "tests"
|
||||
const VALID_OPTION_TABS = [
|
||||
"params",
|
||||
"bodyParams",
|
||||
"headers",
|
||||
"authorization",
|
||||
"preRequestScript",
|
||||
"tests",
|
||||
] as const
|
||||
|
||||
export type RESTOptionTabs = (typeof VALID_OPTION_TABS)[number]
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
// v-model integration with props and emit
|
||||
const props = defineProps<{ modelValue: HoppRESTRequest }>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: HoppRESTRequest
|
||||
optionTab: RESTOptionTabs
|
||||
}>(),
|
||||
{
|
||||
optionTab: "params",
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: HoppRESTRequest): void
|
||||
(e: "update:optionTab", value: RESTOptionTabs): void
|
||||
}>()
|
||||
|
||||
const request = useVModel(props, "modelValue", emit)
|
||||
const selectedOptionTab = useVModel(props, "optionTab", emit)
|
||||
|
||||
const selectedOptionsTab = ref<RequestOptionTabs>("params")
|
||||
|
||||
const changeTab = (e: RequestOptionTabs) => {
|
||||
selectedOptionsTab.value = e
|
||||
const changeOptionTab = (e: RESTOptionTabs) => {
|
||||
selectedOptionTab.value = e
|
||||
}
|
||||
|
||||
const newActiveParamsCount$ = computed(() => {
|
||||
@@ -101,6 +113,6 @@ const newActiveHeadersCount$ = computed(() => {
|
||||
})
|
||||
|
||||
defineActionHandler("request.open-tab", ({ tab }) => {
|
||||
selectedOptionsTab.value = tab as RequestOptionTabs
|
||||
selectedOptionTab.value = tab as RESTOptionTabs
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
<AppPaneLayout layout-id="rest-primary">
|
||||
<template #primary>
|
||||
<HttpRequest v-model="tab" />
|
||||
<HttpRequestOptions v-model="tab.document.request" />
|
||||
<HttpRequestOptions
|
||||
v-model="tab.document.request"
|
||||
v-model:option-tab="tab.document.optionTabPreference"
|
||||
/>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<HttpResponse v-model:tab="tab" />
|
||||
<HttpResponse v-model:document="tab.document" />
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
@@ -13,16 +16,17 @@
|
||||
<script setup lang="ts">
|
||||
import { watch } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { isEqualHoppRESTRequest } from "@hoppscotch/data"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
// TODO: Move Response and Request execution code to over here
|
||||
|
||||
const props = defineProps<{ modelValue: HoppRESTTab }>()
|
||||
const props = defineProps<{ modelValue: HoppTab<HoppRESTDocument> }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", val: HoppRESTTab): void
|
||||
(e: "update:modelValue", val: HoppTab<HoppRESTDocument>): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "modelValue", emit)
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1 relative">
|
||||
<HttpResponseMeta :response="tab.response" />
|
||||
<HttpResponseMeta :response="doc.response" />
|
||||
<LensesResponseBodyRenderer
|
||||
v-if="!loading && hasResponse"
|
||||
v-model:selected-tab-preference="selectedTabPreference"
|
||||
v-model:tab="tab"
|
||||
v-model:document="doc"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { computed } from "vue"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
const props = defineProps<{
|
||||
tab: HoppRESTTab
|
||||
document: HoppRESTDocument
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRESTTab): void
|
||||
(e: "update:tab", val: HoppRESTDocument): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "tab", emit)
|
||||
|
||||
const selectedTabPreference = ref<string | null>(null)
|
||||
const doc = useVModel(props, "document", emit)
|
||||
|
||||
const hasResponse = computed(
|
||||
() =>
|
||||
tab.value.response?.type === "success" ||
|
||||
tab.value.response?.type === "fail"
|
||||
doc.value.response?.type === "success" ||
|
||||
doc.value.response?.type === "fail"
|
||||
)
|
||||
|
||||
const loading = computed(() => tab.value.response?.type === "loading")
|
||||
const loading = computed(() => doc.value.response?.type === "loading")
|
||||
</script>
|
||||
|
||||
@@ -93,10 +93,11 @@ import { useColorMode } from "@composables/theming"
|
||||
import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
||||
import { useService } from "dioc/vue"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { currentTabID } from "~/helpers/rest/tab"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const t = useI18n()
|
||||
const colorMode = useColorMode()
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const props = defineProps<{
|
||||
response: HoppRESTResponse | null | undefined
|
||||
@@ -146,7 +147,7 @@ const statusCategory = computed(() => {
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const tabResults = inspectionService.getResultViewFor(
|
||||
currentTabID.value,
|
||||
tabs.currentTabID.value,
|
||||
(result) => result.locations.type === "response"
|
||||
)
|
||||
</script>
|
||||
|
||||
@@ -96,16 +96,17 @@ import { ref } from "vue"
|
||||
import { TippyComponent } from "vue-tippy"
|
||||
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||
import { useI18n } from "~/composables/i18n"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import IconXCircle from "~icons/lucide/x-circle"
|
||||
import IconXSquare from "~icons/lucide/x-square"
|
||||
import IconFileEdit from "~icons/lucide/file-edit"
|
||||
import IconCopy from "~icons/lucide/copy"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
defineProps<{
|
||||
tab: HoppRESTTab
|
||||
tab: HoppTab<HoppRESTDocument>
|
||||
isRemovable: boolean
|
||||
}>()
|
||||
|
||||
|
||||
@@ -279,7 +279,6 @@ const globalEnvVars = useReadonlyStream(globalEnv$, []) as Ref<
|
||||
Array<{
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
}>
|
||||
>
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ type Props = {
|
||||
env: {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
previousValue?: string
|
||||
}
|
||||
status: Status
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<HoppSmartTabs
|
||||
v-if="tab.response"
|
||||
v-if="doc.response"
|
||||
v-model="selectedLensTab"
|
||||
styles="sticky overflow-x-auto flex-shrink-0 z-10 bg-primary top-lowerPrimaryStickyFold"
|
||||
>
|
||||
@@ -13,7 +13,7 @@
|
||||
>
|
||||
<component
|
||||
:is="lensRendererFor(lens.renderer)"
|
||||
:response="tab.response"
|
||||
:response="doc.response"
|
||||
/>
|
||||
</HoppSmartTab>
|
||||
<HoppSmartTab
|
||||
@@ -28,19 +28,10 @@
|
||||
<HoppSmartTab
|
||||
id="results"
|
||||
:label="t('test.results')"
|
||||
:indicator="
|
||||
tab.testResults &&
|
||||
(tab.testResults.expectResults.length ||
|
||||
tab.testResults.tests.length ||
|
||||
tab.testResults.envDiff.selected.additions.length ||
|
||||
tab.testResults.envDiff.selected.updations.length ||
|
||||
tab.testResults.envDiff.global.updations.length)
|
||||
? true
|
||||
: false
|
||||
"
|
||||
:indicator="showIndicator"
|
||||
class="flex flex-col flex-1"
|
||||
>
|
||||
<HttpTestResult v-model="tab.testResults" />
|
||||
<HttpTestResult v-model="doc.testResults" />
|
||||
</HoppSmartTab>
|
||||
</HoppSmartTabs>
|
||||
</template>
|
||||
@@ -54,20 +45,30 @@ import {
|
||||
} from "~/helpers/lenses/lenses"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { HoppRESTTab } from "~/helpers/rest/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
const props = defineProps<{
|
||||
tab: HoppRESTTab
|
||||
selectedTabPreference: string | null
|
||||
document: HoppRESTDocument
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:tab", val: HoppRESTTab): void
|
||||
(e: "update:selectedTabPreference", newTab: string): void
|
||||
(e: "update:document", document: HoppRESTDocument): void
|
||||
}>()
|
||||
|
||||
const tab = useVModel(props, "tab", emit)
|
||||
const selectedTabPreference = useVModel(props, "selectedTabPreference", emit)
|
||||
const doc = useVModel(props, "document", emit)
|
||||
|
||||
const showIndicator = computed(() => {
|
||||
if (!doc.value.testResults) return false
|
||||
|
||||
const { expectResults, tests, envDiff } = doc.value.testResults
|
||||
return Boolean(
|
||||
expectResults.length ||
|
||||
tests.length ||
|
||||
envDiff.selected.additions.length ||
|
||||
envDiff.selected.updations.length ||
|
||||
envDiff.global.updations.length
|
||||
)
|
||||
})
|
||||
|
||||
const allLensRenderers = getLensRenderers()
|
||||
|
||||
@@ -81,19 +82,19 @@ const selectedLensTab = ref("")
|
||||
|
||||
const maybeHeaders = computed(() => {
|
||||
if (
|
||||
!tab.value.response ||
|
||||
!doc.value.response ||
|
||||
!(
|
||||
tab.value.response.type === "success" ||
|
||||
tab.value.response.type === "fail"
|
||||
doc.value.response.type === "success" ||
|
||||
doc.value.response.type === "fail"
|
||||
)
|
||||
)
|
||||
return null
|
||||
return tab.value.response.headers
|
||||
return doc.value.response.headers
|
||||
})
|
||||
|
||||
const validLenses = computed(() => {
|
||||
if (!tab.value.response) return []
|
||||
return getSuitableLenses(tab.value.response)
|
||||
if (!doc.value.response) return []
|
||||
return getSuitableLenses(doc.value.response)
|
||||
})
|
||||
|
||||
watch(
|
||||
@@ -107,11 +108,13 @@ watch(
|
||||
"results",
|
||||
]
|
||||
|
||||
const { responseTabPreference } = doc.value
|
||||
|
||||
if (
|
||||
selectedTabPreference.value &&
|
||||
validRenderers.includes(selectedTabPreference.value)
|
||||
responseTabPreference &&
|
||||
validRenderers.includes(responseTabPreference)
|
||||
) {
|
||||
selectedLensTab.value = selectedTabPreference.value
|
||||
selectedLensTab.value = responseTabPreference
|
||||
} else {
|
||||
selectedLensTab.value = newLenses[0].renderer
|
||||
}
|
||||
@@ -120,6 +123,6 @@ watch(
|
||||
)
|
||||
|
||||
watch(selectedLensTab, (newLensID) => {
|
||||
selectedTabPreference.value = newLensID
|
||||
doc.value.responseTabPreference = newLensID
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -55,12 +55,7 @@ import {
|
||||
keymap,
|
||||
tooltips,
|
||||
} from "@codemirror/view"
|
||||
import {
|
||||
EditorSelection,
|
||||
EditorState,
|
||||
Extension,
|
||||
StateEffect,
|
||||
} from "@codemirror/state"
|
||||
import { EditorSelection, EditorState, Extension } from "@codemirror/state"
|
||||
import { clone } from "lodash-es"
|
||||
import { history, historyKeymap } from "@codemirror/commands"
|
||||
import { inputTheme } from "~/helpers/editor/themes/baseTheme"
|
||||
@@ -77,17 +72,13 @@ const props = withDefaults(
|
||||
modelValue?: string
|
||||
placeholder?: string
|
||||
styles?: string
|
||||
envs?:
|
||||
| { key: string; value: string; secret: boolean; source: string }[]
|
||||
| null
|
||||
envs?: { key: string; value: string; source: string }[] | null
|
||||
focus?: boolean
|
||||
selectTextOnMount?: boolean
|
||||
environmentHighlights?: boolean
|
||||
readonly?: boolean
|
||||
autoCompleteSource?: string[]
|
||||
inspectionResults?: InspectorResult[] | undefined
|
||||
defaultValue?: string
|
||||
secret?: boolean
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
@@ -100,8 +91,6 @@ const props = withDefaults(
|
||||
autoCompleteSource: undefined,
|
||||
inspectionResult: undefined,
|
||||
inspectionResults: undefined,
|
||||
defaultValue: "",
|
||||
secret: false,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -115,8 +104,6 @@ const emit = defineEmits<{
|
||||
(e: "click", ev: any): void
|
||||
}>()
|
||||
|
||||
const asterikedText = computed(() => "*".repeat(props.modelValue.length))
|
||||
|
||||
const cachedValue = ref(props.modelValue)
|
||||
|
||||
const view = ref<EditorView>()
|
||||
@@ -276,22 +263,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const prevModelValue = ref(props.modelValue)
|
||||
|
||||
watch(
|
||||
() => props.secret,
|
||||
(newValue) => {
|
||||
let visibleValue = asterikedText.value
|
||||
if (!newValue) {
|
||||
visibleValue = prevModelValue.value
|
||||
} else {
|
||||
prevModelValue.value = props.modelValue
|
||||
}
|
||||
emit("update:modelValue", visibleValue)
|
||||
updateEditorViewTheme(newValue)
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Used to scroll the active suggestion into view
|
||||
*/
|
||||
@@ -313,6 +284,7 @@ watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
const singleLinedText = newVal.replaceAll("\n", "")
|
||||
|
||||
const currDoc = view.value?.state.doc
|
||||
.toJSON()
|
||||
.join(view.value.state.lineBreak)
|
||||
@@ -348,7 +320,6 @@ const envVars = computed(() =>
|
||||
? props.envs.map((x) => ({
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
secret: x.secret,
|
||||
sourceEnv: x.source,
|
||||
}))
|
||||
: aggregateEnvs.value
|
||||
@@ -383,14 +354,6 @@ function handleTextSelection() {
|
||||
}
|
||||
}
|
||||
}
|
||||
const updateEditorViewTheme = (readonly: boolean) => {
|
||||
if (view.value) {
|
||||
const extensions: Extension = getExtensions(readonly)
|
||||
view.value.dispatch({
|
||||
effects: [StateEffect.reconfigure.of(extensions)],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const initView = (el: any) => {
|
||||
// Debounce to prevent double click from selecting the word
|
||||
@@ -401,31 +364,17 @@ const initView = (el: any) => {
|
||||
el.addEventListener("mouseup", debounceFn)
|
||||
el.addEventListener("keyup", debounceFn)
|
||||
|
||||
if (props.secret) {
|
||||
emit("update:modelValue", asterikedText.value)
|
||||
}
|
||||
const extensions: Extension = getExtensions(props.readonly || props.secret)
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const getExtensions = (readonly: boolean): Extension => {
|
||||
const extensions: Extension = [
|
||||
EditorView.contentAttributes.of({ "aria-label": props.placeholder }),
|
||||
EditorView.contentAttributes.of({ "data-enable-grammarly": "false" }),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (readonly) {
|
||||
if (props.readonly) {
|
||||
update.view.contentDOM.inputMode = "none"
|
||||
}
|
||||
}),
|
||||
EditorState.changeFilter.of(() => !readonly),
|
||||
EditorState.changeFilter.of(() => !props.readonly),
|
||||
inputTheme,
|
||||
readonly
|
||||
props.readonly
|
||||
? EditorView.theme({
|
||||
".cm-content": {
|
||||
caretColor: "var(--secondary-dark-color)",
|
||||
@@ -457,7 +406,7 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
ViewPlugin.fromClass(
|
||||
class {
|
||||
update(update: ViewUpdate) {
|
||||
if (readonly) return
|
||||
if (props.readonly) return
|
||||
|
||||
if (update.docChanged) {
|
||||
const prevValue = clone(cachedValue.value)
|
||||
@@ -469,14 +418,9 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
// We do not update the cache directly in this case (to trigger value watcher to dispatch)
|
||||
// So, we desync cachedValue a bit so we can trigger updates
|
||||
const value = clone(cachedValue.value).replaceAll("\n", "")
|
||||
if (props.secret) {
|
||||
const asterikedValue = "*".repeat(value.length)
|
||||
emit("update:modelValue", asterikedValue)
|
||||
emit("change", asterikedValue)
|
||||
} else {
|
||||
emit("update:modelValue", value)
|
||||
emit("change", value)
|
||||
}
|
||||
|
||||
emit("update:modelValue", value)
|
||||
emit("change", value)
|
||||
|
||||
const pasted = !!update.transactions.find((txn) =>
|
||||
txn.isUserEvent("input.paste")
|
||||
@@ -501,7 +445,14 @@ const getExtensions = (readonly: boolean): Extension => {
|
||||
history(),
|
||||
keymap.of([...historyKeymap]),
|
||||
]
|
||||
return extensions
|
||||
|
||||
view.value = new EditorView({
|
||||
parent: el,
|
||||
state: EditorState.create({
|
||||
doc: props.modelValue,
|
||||
extensions,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
const triggerTextSelection = () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
WatchStopHandle,
|
||||
watchSyncEffect,
|
||||
watch,
|
||||
nextTick,
|
||||
} from "vue"
|
||||
import {
|
||||
client,
|
||||
@@ -101,7 +102,7 @@ export const useGQLQuery = <
|
||||
|
||||
const rerunQuery = () => {
|
||||
source.value = !isPaused.value
|
||||
? client.value.executeQuery<DocType, DocVarType>(request.value, {
|
||||
? client.value?.executeQuery<DocType, DocVarType>(request.value, {
|
||||
requestPolicy: "network-only",
|
||||
})
|
||||
: undefined
|
||||
@@ -126,7 +127,7 @@ export const useGQLQuery = <
|
||||
|
||||
const invalidateStops = args.updateSubs!.map((sub) => {
|
||||
return wonkaPipe(
|
||||
client.value.executeSubscription(sub),
|
||||
client.value!.executeSubscription(sub),
|
||||
onEnd(() => {
|
||||
if (source.value) execute()
|
||||
}),
|
||||
@@ -191,10 +192,12 @@ export const useGQLQuery = <
|
||||
} else {
|
||||
args.variables = updatedVars
|
||||
}
|
||||
nextTick(rerunQuery)
|
||||
} else {
|
||||
rerunQuery()
|
||||
}
|
||||
|
||||
isPaused.value = false
|
||||
rerunQuery()
|
||||
}
|
||||
|
||||
const pause = () => {
|
||||
|
||||
@@ -29,8 +29,9 @@ import {
|
||||
setGlobalEnvVariables,
|
||||
updateEnvironment,
|
||||
} from "~/newstore/environments"
|
||||
import { HoppRESTTab } from "./rest/tab"
|
||||
import { Ref } from "vue"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "./rest/document"
|
||||
|
||||
const getTestableBody = (
|
||||
res: HoppRESTResponse & { type: "success" | "fail" }
|
||||
@@ -69,7 +70,7 @@ export const executedResponses$ = new Subject<
|
||||
>()
|
||||
|
||||
export function runRESTRequest$(
|
||||
tab: Ref<HoppRESTTab>
|
||||
tab: Ref<HoppTab<HoppRESTDocument>>
|
||||
): [
|
||||
() => void,
|
||||
Promise<
|
||||
@@ -127,7 +128,7 @@ export function runRESTRequest$(
|
||||
)()
|
||||
|
||||
if (E.isRight(runResult)) {
|
||||
tab.value.testResults = translateToSandboxTestResults(
|
||||
tab.value.document.testResults = translateToSandboxTestResults(
|
||||
runResult.right
|
||||
)
|
||||
|
||||
@@ -163,7 +164,7 @@ export function runRESTRequest$(
|
||||
)()
|
||||
}
|
||||
} else {
|
||||
tab.value.testResults = {
|
||||
tab.value.document.testResults = {
|
||||
description: "",
|
||||
expectResults: [],
|
||||
tests: [],
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { getTabsRefTo } from "../rest/tab"
|
||||
import { getAffectedIndexes } from "./affectedIndex"
|
||||
import { GetSingleRequestDocument } from "../backend/graphql"
|
||||
import { runGQLQuery } from "../backend/GQLClient"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
/**
|
||||
* Resolve save context on reorder
|
||||
@@ -56,7 +57,9 @@ export function resolveSaveContextOnCollectionReorder(
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = getTabsRefTo((tab) => {
|
||||
const tabService = getService(RESTTabService)
|
||||
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
affectedPaths.has(tab.document.saveContext.folderPath)
|
||||
@@ -84,7 +87,8 @@ export function updateSaveContextForAffectedRequests(
|
||||
oldFolderPath: string,
|
||||
newFolderPath: string
|
||||
) {
|
||||
const tabs = getTabsRefTo((tab) => {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.document.saveContext.folderPath.startsWith(oldFolderPath)
|
||||
@@ -105,7 +109,8 @@ export function updateSaveContextForAffectedRequests(
|
||||
}
|
||||
|
||||
function resetSaveContextForAffectedRequests(folderPath: string) {
|
||||
const tabs = getTabsRefTo((tab) => {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.document.saveContext.folderPath.startsWith(folderPath)
|
||||
@@ -124,7 +129,8 @@ function resetSaveContextForAffectedRequests(folderPath: string) {
|
||||
*/
|
||||
|
||||
export async function resetTeamRequestsContext() {
|
||||
const tabs = getTabsRefTo((tab) => {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
return tab.document.saveContext?.originLocation === "team-collection"
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { getTabsRefTo } from "../rest/tab"
|
||||
import { getAffectedIndexes } from "./affectedIndex"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
/**
|
||||
* Resolve save context on reorder
|
||||
@@ -32,7 +33,8 @@ export function resolveSaveContextOnRequestReorder(payload: {
|
||||
// if (newIndex === -1) remove it from the map because it will be deleted
|
||||
if (newIndex === -1) affectedIndexes.delete(lastIndex)
|
||||
|
||||
const tabs = getTabsRefTo((tab) => {
|
||||
const tabService = getService(RESTTabService)
|
||||
const tabs = tabService.getTabsRefTo((tab) => {
|
||||
return (
|
||||
tab.document.saveContext?.originLocation === "user-collection" &&
|
||||
tab.document.saveContext.folderPath === folderPath &&
|
||||
|
||||
@@ -66,10 +66,7 @@ const cursorTooltipField = (aggregateEnvs: AggregateEnvironment[]) =>
|
||||
|
||||
const envName = tooltipEnv?.sourceEnv ?? "Choose an Environment"
|
||||
|
||||
let envValue = tooltipEnv?.value ?? "Not found"
|
||||
if (tooltipEnv?.secret) {
|
||||
envValue = "******"
|
||||
}
|
||||
const envValue = tooltipEnv?.value ?? "Not found"
|
||||
|
||||
const result = parseTemplateStringE(envValue, aggregateEnvs)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { GQLHeader, HoppGQLAuth, makeGQLRequest } from "@hoppscotch/data"
|
||||
import { OperationType } from "@urql/core"
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
GraphQLEnumType,
|
||||
GraphQLInputObjectType,
|
||||
@@ -11,11 +12,12 @@ import {
|
||||
printSchema,
|
||||
} from "graphql"
|
||||
import { computed, reactive, ref } from "vue"
|
||||
import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"
|
||||
import { currentTabID } from "./tab"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
import { addGraphqlHistoryEntry, makeGQLHistoryEntry } from "~/newstore/history"
|
||||
|
||||
import { InterceptorService } from "~/services/interceptor.service"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||
|
||||
@@ -61,6 +63,9 @@ type Connection = {
|
||||
schema: GraphQLSchema | null
|
||||
}
|
||||
|
||||
const tabs = getService(GQLTabService)
|
||||
const currentTabID = computed(() => tabs.currentTabID.value)
|
||||
|
||||
export const connection = reactive<Connection>({
|
||||
state: "DISCONNECTED",
|
||||
subscriptionState: new Map<string, SubscriptionState>(),
|
||||
@@ -268,7 +273,7 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||
}
|
||||
|
||||
if (operationType === "subscription") {
|
||||
return runSubscription(options)
|
||||
return runSubscription(options, finalHeaders)
|
||||
}
|
||||
|
||||
const interceptorService = getService(InterceptorService)
|
||||
@@ -299,7 +304,10 @@ export const runGQLOperation = async (options: RunQueryOptions) => {
|
||||
return responseText
|
||||
}
|
||||
|
||||
export const runSubscription = (options: RunQueryOptions) => {
|
||||
export const runSubscription = (
|
||||
options: RunQueryOptions,
|
||||
headers?: Record<string, string>
|
||||
) => {
|
||||
const { url, query, operationName } = options
|
||||
const wsUrl = url.replace(/^http/, "ws")
|
||||
|
||||
@@ -309,10 +317,11 @@ export const runSubscription = (options: RunQueryOptions) => {
|
||||
|
||||
connection.socket.onopen = (event) => {
|
||||
console.log("WebSocket is open now.", event)
|
||||
|
||||
connection.socket?.send(
|
||||
JSON.stringify({
|
||||
type: GQL.CONNECTION_INIT,
|
||||
payload: {},
|
||||
payload: headers ?? {},
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { HoppGQLRequest } from "@hoppscotch/data"
|
||||
import { GQLResponseEvent } from "./connection"
|
||||
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
|
||||
|
||||
export type HoppGQLSaveContext =
|
||||
| {
|
||||
@@ -55,4 +57,20 @@ export type HoppGQLDocument = {
|
||||
* This contains where the request is originated from basically.
|
||||
*/
|
||||
saveContext?: HoppGQLSaveContext
|
||||
|
||||
/**
|
||||
* The response as it is in the document
|
||||
* (if any)
|
||||
*/
|
||||
response?: GQLResponseEvent[] | null
|
||||
|
||||
/**
|
||||
* Response tab preference for the current tab's document
|
||||
*/
|
||||
responseTabPreference?: string
|
||||
|
||||
/**
|
||||
* Options tab preference for the current tab's document
|
||||
*/
|
||||
optionTabPreference?: GQLOptionTabs
|
||||
}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { refWithControl } from "@vueuse/core"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { v4 as uuidV4 } from "uuid"
|
||||
import { computed, reactive, ref, shallowReadonly, watch } from "vue"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { GQLResponseEvent } from "./connection"
|
||||
import { getDefaultGQLRequest } from "./default"
|
||||
import { HoppGQLDocument, HoppGQLSaveContext } from "./document"
|
||||
|
||||
export type HoppGQLTab = {
|
||||
id: string
|
||||
document: HoppGQLDocument
|
||||
response?: GQLResponseEvent[] | null
|
||||
testResults?: HoppTestResult | null
|
||||
}
|
||||
|
||||
export type PersistableGQLTabState = {
|
||||
lastActiveTabID: string
|
||||
orderedDocs: Array<{
|
||||
tabID: string
|
||||
doc: HoppGQLDocument
|
||||
}>
|
||||
}
|
||||
|
||||
export const currentTabID = refWithControl("test", {
|
||||
onBeforeChange(newTabID) {
|
||||
if (!newTabID || !tabMap.has(newTabID)) {
|
||||
console.warn(
|
||||
`Tried to set current tab id to an invalid value. (value: ${newTabID})`
|
||||
)
|
||||
|
||||
// Don't allow change
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const tabMap = reactive(
|
||||
new Map<string, HoppGQLTab>([
|
||||
[
|
||||
"test",
|
||||
{
|
||||
id: "test",
|
||||
document: {
|
||||
request: getDefaultGQLRequest(),
|
||||
isDirty: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
)
|
||||
const tabOrdering = ref<string[]>(["test"])
|
||||
|
||||
watch(
|
||||
tabOrdering,
|
||||
(newOrdering) => {
|
||||
if (!currentTabID.value || !newOrdering.includes(currentTabID.value)) {
|
||||
currentTabID.value = newOrdering[newOrdering.length - 1] // newOrdering should always be non-empty
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
export const persistableTabState = computed<PersistableGQLTabState>(() => ({
|
||||
lastActiveTabID: currentTabID.value,
|
||||
orderedDocs: tabOrdering.value.map((tabID) => {
|
||||
const tab = tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
|
||||
return {
|
||||
tabID: tab.id,
|
||||
doc: tab.document,
|
||||
}
|
||||
}),
|
||||
}))
|
||||
|
||||
export const currentActiveTab = computed(() => tabMap.get(currentTabID.value)!) // Guaranteed to not be undefined
|
||||
|
||||
// TODO: Mark this unknown and do validations
|
||||
export function loadTabsFromPersistedState(data: PersistableGQLTabState) {
|
||||
if (data) {
|
||||
tabMap.clear()
|
||||
tabOrdering.value = []
|
||||
|
||||
for (const doc of data.orderedDocs) {
|
||||
tabMap.set(doc.tabID, {
|
||||
id: doc.tabID,
|
||||
document: doc.doc,
|
||||
})
|
||||
|
||||
tabOrdering.value.push(doc.tabID)
|
||||
}
|
||||
|
||||
currentTabID.value = data.lastActiveTabID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the active Tab IDs in order
|
||||
*/
|
||||
export function getActiveTabs() {
|
||||
return shallowReadonly(
|
||||
computed(() => tabOrdering.value.map((x) => tabMap.get(x)!))
|
||||
)
|
||||
}
|
||||
|
||||
export function getTabRef(tabID: string) {
|
||||
return computed({
|
||||
get() {
|
||||
const result = tabMap.get(tabID)
|
||||
|
||||
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
|
||||
|
||||
return result
|
||||
},
|
||||
set(value) {
|
||||
return tabMap.set(tabID, value)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function generateNewTabID() {
|
||||
while (true) {
|
||||
const id = uuidV4()
|
||||
|
||||
if (!tabMap.has(id)) return id
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTab(tabUpdate: HoppGQLTab) {
|
||||
if (!tabMap.has(tabUpdate.id)) {
|
||||
console.warn(
|
||||
`Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
|
||||
)
|
||||
}
|
||||
|
||||
tabMap.set(tabUpdate.id, tabUpdate)
|
||||
}
|
||||
|
||||
export function createNewTab(document: HoppGQLDocument, switchToIt = true) {
|
||||
const id = generateNewTabID()
|
||||
|
||||
const tab: HoppGQLTab = { id, document }
|
||||
|
||||
tabMap.set(id, tab)
|
||||
tabOrdering.value.push(id)
|
||||
|
||||
if (switchToIt) {
|
||||
currentTabID.value = id
|
||||
}
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
export function updateTabOrdering(fromIndex: number, toIndex: number) {
|
||||
tabOrdering.value.splice(
|
||||
toIndex,
|
||||
0,
|
||||
tabOrdering.value.splice(fromIndex, 1)[0]
|
||||
)
|
||||
}
|
||||
|
||||
export function closeTab(tabID: string) {
|
||||
if (!tabMap.has(tabID)) {
|
||||
console.warn(`Tried to close a tab which does not exist (tab id: ${tabID})`)
|
||||
return
|
||||
}
|
||||
|
||||
if (tabOrdering.value.length === 1) {
|
||||
console.warn(
|
||||
`Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
|
||||
|
||||
tabMap.delete(tabID)
|
||||
}
|
||||
|
||||
export function getTabRefWithSaveContext(ctx: HoppGQLSaveContext) {
|
||||
for (const tab of tabMap.values()) {
|
||||
// For `team-collection` request id can be considered unique
|
||||
if (ctx && ctx.originLocation === "team-collection") {
|
||||
if (
|
||||
tab.document.saveContext?.originLocation === "team-collection" &&
|
||||
tab.document.saveContext.requestID === ctx.requestID
|
||||
) {
|
||||
return getTabRef(tab.id)
|
||||
}
|
||||
} else if (isEqual(ctx, tab.document.saveContext)) return getTabRef(tab.id)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getTabsRefTo(func: (tab: HoppGQLTab) => boolean) {
|
||||
return Array.from(tabMap.values())
|
||||
.filter(func)
|
||||
.map((tab) => getTabRef(tab.id))
|
||||
}
|
||||
|
||||
export function closeOtherTabs(tabID: string) {
|
||||
if (!tabMap.has(tabID)) {
|
||||
console.warn(
|
||||
`The tab to close other tabs does not exist (tab id: ${tabID})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
tabOrdering.value = [tabID]
|
||||
|
||||
tabMap.forEach((_, id) => {
|
||||
if (id !== tabID) tabMap.delete(id)
|
||||
})
|
||||
|
||||
currentTabID.value = tabID
|
||||
}
|
||||
|
||||
export function getDirtyTabsCount() {
|
||||
let count = 0
|
||||
|
||||
for (const tab of tabMap.values()) {
|
||||
if (tab.document.isDirty) count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
const getEnvironmentJson = (
|
||||
environmentObj: TeamEnvironment | Environment,
|
||||
environmentIndex?: number | "Global" | null
|
||||
) => {
|
||||
const newEnvironment =
|
||||
"environment" in environmentObj
|
||||
? cloneDeep(environmentObj.environment)
|
||||
: cloneDeep(environmentObj)
|
||||
|
||||
delete newEnvironment.id
|
||||
|
||||
const environmentId =
|
||||
environmentIndex || environmentIndex === 0
|
||||
? environmentIndex
|
||||
: environmentObj.id
|
||||
|
||||
return environmentId !== null
|
||||
? JSON.stringify(newEnvironment, null, 2)
|
||||
: undefined
|
||||
}
|
||||
|
||||
export const exportAsJSON = (
|
||||
environmentObj: Environment | TeamEnvironment,
|
||||
environmentIndex?: number | "Global" | null
|
||||
): boolean => {
|
||||
const dataToWrite = getEnvironmentJson(environmentObj, environmentIndex)
|
||||
|
||||
if (!dataToWrite) return false
|
||||
|
||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
||||
const a = document.createElement("a")
|
||||
const url = URL.createObjectURL(file)
|
||||
a.href = url
|
||||
|
||||
// Extracts the path from url, removes fragment identifier and query parameters if any, appends the ".json" extension, and assigns it
|
||||
a.download = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a)
|
||||
window.URL.revokeObjectURL(url)
|
||||
}, 0)
|
||||
return true
|
||||
}
|
||||
@@ -131,7 +131,8 @@ function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
}
|
||||
|
||||
function getPressedKey(ev: KeyboardEvent): Key | null {
|
||||
const val = ev.code.toLowerCase()
|
||||
// Sometimes the property code is not available on the KeyboardEvent object
|
||||
const val = (ev.code ?? "").toLowerCase()
|
||||
|
||||
// Check arrow keys
|
||||
if (val === "arrowup") return "up"
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
|
||||
|
||||
export type HoppRESTSaveContext =
|
||||
| {
|
||||
@@ -55,4 +58,26 @@ export type HoppRESTDocument = {
|
||||
* This contains where the request is originated from basically.
|
||||
*/
|
||||
saveContext?: HoppRESTSaveContext
|
||||
|
||||
/**
|
||||
* The response as it is in the document
|
||||
* (if any)
|
||||
*/
|
||||
response?: HoppRESTResponse | null
|
||||
|
||||
/**
|
||||
* The test results as it is in the document
|
||||
* (if any)
|
||||
*/
|
||||
testResults?: HoppTestResult | null
|
||||
|
||||
/**
|
||||
* Response tab preference for the current tab's document
|
||||
*/
|
||||
responseTabPreference?: string
|
||||
|
||||
/**
|
||||
* Options tab preference for the current tab's document
|
||||
*/
|
||||
optionTabPreference?: RESTOptionTabs
|
||||
}
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
import { v4 as uuidV4 } from "uuid"
|
||||
import { isEqual } from "lodash-es"
|
||||
import { reactive, watch, computed, ref, shallowReadonly } from "vue"
|
||||
import { HoppRESTDocument, HoppRESTSaveContext } from "./document"
|
||||
import { refWithControl } from "@vueuse/core"
|
||||
import { HoppRESTResponse } from "../types/HoppRESTResponse"
|
||||
import { getDefaultRESTRequest } from "./default"
|
||||
import { HoppTestResult } from "../types/HoppTestResult"
|
||||
import { platform } from "~/platform"
|
||||
import { nextTick } from "vue"
|
||||
|
||||
export type HoppRESTTab = {
|
||||
id: string
|
||||
document: HoppRESTDocument
|
||||
response?: HoppRESTResponse | null
|
||||
testResults?: HoppTestResult | null
|
||||
}
|
||||
|
||||
export type PersistableRESTTabState = {
|
||||
lastActiveTabID: string
|
||||
orderedDocs: Array<{
|
||||
tabID: string
|
||||
doc: HoppRESTDocument
|
||||
}>
|
||||
}
|
||||
|
||||
export const currentTabID = refWithControl("test", {
|
||||
onBeforeChange(newTabID) {
|
||||
if (!newTabID || !tabMap.has(newTabID)) {
|
||||
console.warn(
|
||||
`Tried to set current tab id to an invalid value. (value: ${newTabID})`
|
||||
)
|
||||
|
||||
// Don't allow change
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const tabMap = reactive(
|
||||
new Map<string, HoppRESTTab>([
|
||||
[
|
||||
"test",
|
||||
{
|
||||
id: "test",
|
||||
document: {
|
||||
request: getDefaultRESTRequest(),
|
||||
isDirty: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
])
|
||||
)
|
||||
const tabOrdering = ref<string[]>(["test"])
|
||||
|
||||
watch(
|
||||
tabOrdering,
|
||||
(newOrdering) => {
|
||||
if (!currentTabID.value || !newOrdering.includes(currentTabID.value)) {
|
||||
currentTabID.value = newOrdering[newOrdering.length - 1] // newOrdering should always be non-empty
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
export const persistableTabState = computed<PersistableRESTTabState>(() => ({
|
||||
lastActiveTabID: currentTabID.value,
|
||||
orderedDocs: tabOrdering.value.map((tabID) => {
|
||||
const tab = tabMap.get(tabID)! // tab ordering is guaranteed to have value for this key
|
||||
return {
|
||||
tabID: tab.id,
|
||||
doc: tab.document,
|
||||
}
|
||||
}),
|
||||
}))
|
||||
|
||||
export const currentActiveTab = computed(() => tabMap.get(currentTabID.value)!) // Guaranteed to not be undefined
|
||||
|
||||
// TODO: Mark this unknown and do validations
|
||||
export function loadTabsFromPersistedState(data: PersistableRESTTabState) {
|
||||
if (data) {
|
||||
tabMap.clear()
|
||||
tabOrdering.value = []
|
||||
|
||||
for (const doc of data.orderedDocs) {
|
||||
tabMap.set(doc.tabID, {
|
||||
id: doc.tabID,
|
||||
document: doc.doc,
|
||||
})
|
||||
|
||||
tabOrdering.value.push(doc.tabID)
|
||||
}
|
||||
|
||||
currentTabID.value = data.lastActiveTabID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the active Tab IDs in order
|
||||
*/
|
||||
export function getActiveTabs() {
|
||||
return shallowReadonly(
|
||||
computed(() => tabOrdering.value.map((x) => tabMap.get(x)!))
|
||||
)
|
||||
}
|
||||
|
||||
export function getTabRef(tabID: string) {
|
||||
return computed({
|
||||
get() {
|
||||
const result = tabMap.get(tabID)
|
||||
|
||||
if (result === undefined) throw new Error(`Invalid tab id: ${tabID}`)
|
||||
|
||||
return result
|
||||
},
|
||||
set(value) {
|
||||
return tabMap.set(tabID, value)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function generateNewTabID() {
|
||||
while (true) {
|
||||
const id = uuidV4()
|
||||
|
||||
if (!tabMap.has(id)) return id
|
||||
}
|
||||
}
|
||||
|
||||
export function updateTab(tabUpdate: HoppRESTTab) {
|
||||
if (!tabMap.has(tabUpdate.id)) {
|
||||
console.warn(
|
||||
`Cannot update tab as tab with that tab id does not exist (id: ${tabUpdate.id})`
|
||||
)
|
||||
}
|
||||
|
||||
tabMap.set(tabUpdate.id, tabUpdate)
|
||||
}
|
||||
|
||||
export function createNewTab(document: HoppRESTDocument, switchToIt = true) {
|
||||
const id = generateNewTabID()
|
||||
|
||||
const tab: HoppRESTTab = { id, document }
|
||||
|
||||
tabMap.set(id, tab)
|
||||
tabOrdering.value.push(id)
|
||||
|
||||
if (switchToIt) {
|
||||
currentTabID.value = id
|
||||
}
|
||||
|
||||
platform.analytics?.logEvent({
|
||||
type: "HOPP_REST_NEW_TAB_OPENED",
|
||||
})
|
||||
|
||||
return tab
|
||||
}
|
||||
|
||||
export function updateTabOrdering(fromIndex: number, toIndex: number) {
|
||||
tabOrdering.value.splice(
|
||||
toIndex,
|
||||
0,
|
||||
tabOrdering.value.splice(fromIndex, 1)[0]
|
||||
)
|
||||
}
|
||||
|
||||
export function closeTab(tabID: string) {
|
||||
if (!tabMap.has(tabID)) {
|
||||
console.warn(`Tried to close a tab which does not exist (tab id: ${tabID})`)
|
||||
return
|
||||
}
|
||||
|
||||
if (tabOrdering.value.length === 1) {
|
||||
console.warn(
|
||||
`Tried to close the only tab open, which is not allowed. (tab id: ${tabID})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
tabOrdering.value.splice(tabOrdering.value.indexOf(tabID), 1)
|
||||
|
||||
nextTick(() => {
|
||||
tabMap.delete(tabID)
|
||||
})
|
||||
}
|
||||
|
||||
export function closeOtherTabs(tabID: string) {
|
||||
if (!tabMap.has(tabID)) {
|
||||
console.warn(
|
||||
`The tab to close other tabs does not exist (tab id: ${tabID})`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
tabOrdering.value = [tabID]
|
||||
|
||||
tabMap.forEach((_, id) => {
|
||||
if (id !== tabID) tabMap.delete(id)
|
||||
})
|
||||
|
||||
currentTabID.value = tabID
|
||||
}
|
||||
|
||||
export function getDirtyTabsCount() {
|
||||
let count = 0
|
||||
|
||||
for (const tab of tabMap.values()) {
|
||||
if (tab.document.isDirty) count++
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
export function getTabRefWithSaveContext(ctx: HoppRESTSaveContext) {
|
||||
for (const tab of tabMap.values()) {
|
||||
// For `team-collection` request id can be considered unique
|
||||
if (ctx && ctx.originLocation === "team-collection") {
|
||||
if (
|
||||
tab.document.saveContext?.originLocation === "team-collection" &&
|
||||
tab.document.saveContext.requestID === ctx.requestID
|
||||
) {
|
||||
return getTabRef(tab.id)
|
||||
}
|
||||
} else if (isEqual(ctx, tab.document.saveContext)) return getTabRef(tab.id)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function getTabsRefTo(func: (tab: HoppRESTTab) => boolean) {
|
||||
return Array.from(tabMap.values())
|
||||
.filter(func)
|
||||
.map((tab) => getTabRef(tab.id))
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"!name": "pw-pre",
|
||||
"pw": {
|
||||
"env": {
|
||||
"set": "fn(key: string, value: string, secret: boolean)",
|
||||
"set": "fn(key: string, value: string)",
|
||||
"get": "fn(key: string) -> string",
|
||||
"getResolve": "fn(key: string) -> string",
|
||||
"resolve": "fn(value: string) -> string"
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"body": "?"
|
||||
},
|
||||
"env": {
|
||||
"set": "fn(key: string, value: string, secret:boolean)",
|
||||
"set": "fn(key: string, value: string)",
|
||||
"get": "fn(key: string) -> string",
|
||||
"getResolve": "fn(key: string) -> string",
|
||||
"resolve": "fn(value: string) -> string"
|
||||
|
||||
@@ -198,11 +198,11 @@ export const resolvesEnvsInBody = (
|
||||
}
|
||||
),
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
contentType: body.contentType,
|
||||
body: parseTemplateString(body.body, env.variables),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contentType: body.contentType,
|
||||
body: parseTemplateString(body.body ?? "", env.variables),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,9 +210,7 @@ function getFinalBodyFromRequest(
|
||||
request: HoppRESTRequest,
|
||||
envVariables: Environment["variables"]
|
||||
): FormData | string | null {
|
||||
if (request.body.contentType === null) {
|
||||
return null
|
||||
}
|
||||
if (request.body.contentType === null) return null
|
||||
|
||||
if (request.body.contentType === "application/x-www-form-urlencoded") {
|
||||
const parsedBodyRecord = pipe(
|
||||
@@ -280,7 +278,10 @@ function getFinalBodyFromRequest(
|
||||
),
|
||||
toFormData
|
||||
)
|
||||
} else return parseBodyEnvVariables(request.body.body, envVariables)
|
||||
}
|
||||
|
||||
// body can be null if the content-type is not set
|
||||
return parseBodyEnvVariables(request.body.body ?? "", envVariables)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { getTabRefWithSaveContext } from "~/helpers/rest/tab"
|
||||
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
|
||||
const defaultRESTCollectionState = {
|
||||
state: [
|
||||
@@ -454,7 +455,10 @@ const restCollectionDispatchers = defineDispatchers({
|
||||
|
||||
// Deal with situations where a tab with the given thing is deleted
|
||||
// We are just going to dissociate the save context of the tab and mark it dirty
|
||||
const tab = getTabRefWithSaveContext({
|
||||
|
||||
const tabService = getService(RESTTabService)
|
||||
|
||||
const tab = tabService.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex: requestIndex,
|
||||
@@ -512,7 +516,8 @@ const restCollectionDispatchers = defineDispatchers({
|
||||
destLocation.requests.push(req)
|
||||
targetLocation.requests.splice(requestIndex, 1)
|
||||
|
||||
const possibleTab = getTabRefWithSaveContext({
|
||||
const tabService = getService(RESTTabService)
|
||||
const possibleTab = tabService.getTabRefWithSaveContext({
|
||||
originLocation: "user-collection",
|
||||
folderPath: path,
|
||||
requestIndex,
|
||||
|
||||
@@ -186,19 +186,14 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
addEnvironmentVariable(
|
||||
{ environments }: EnvironmentStore,
|
||||
{
|
||||
envIndex,
|
||||
key,
|
||||
value,
|
||||
secret,
|
||||
}: { envIndex: number; key: string; value: string; secret: boolean }
|
||||
{ envIndex, key, value }: { envIndex: number; key: string; value: string }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: [...env.variables, { key, value, secret }],
|
||||
variables: [...env.variables, { key, value }],
|
||||
}
|
||||
: env
|
||||
),
|
||||
@@ -226,10 +221,7 @@ const dispatchers = defineDispatchers({
|
||||
{
|
||||
envIndex,
|
||||
vars,
|
||||
}: {
|
||||
envIndex: number
|
||||
vars: { key: string; value: string; secret: boolean }[]
|
||||
}
|
||||
}: { envIndex: number; vars: { key: string; value: string }[] }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
@@ -249,13 +241,11 @@ const dispatchers = defineDispatchers({
|
||||
variableIndex,
|
||||
updatedKey,
|
||||
updatedValue,
|
||||
updatedSecret,
|
||||
}: {
|
||||
envIndex: number
|
||||
variableIndex: number
|
||||
updatedKey: string
|
||||
updatedValue: string
|
||||
updatedSecret: boolean
|
||||
}
|
||||
) {
|
||||
return {
|
||||
@@ -265,11 +255,7 @@ const dispatchers = defineDispatchers({
|
||||
...env,
|
||||
variables: env.variables.map((v, vIndex) =>
|
||||
vIndex === variableIndex
|
||||
? {
|
||||
key: updatedKey,
|
||||
value: updatedValue,
|
||||
secret: updatedSecret,
|
||||
}
|
||||
? { key: updatedKey, value: updatedValue }
|
||||
: v
|
||||
),
|
||||
}
|
||||
@@ -373,7 +359,6 @@ export const currentEnvironment$: Observable<Environment | undefined> =
|
||||
export type AggregateEnvironment = {
|
||||
key: string
|
||||
value: string
|
||||
secret: boolean
|
||||
sourceEnv: string
|
||||
}
|
||||
|
||||
@@ -388,11 +373,11 @@ export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
||||
map(([selectedEnv, globalVars]) => {
|
||||
const results: AggregateEnvironment[] = []
|
||||
|
||||
selectedEnv?.variables.forEach(({ key, value, secret }) =>
|
||||
results.push({ key, value, secret, sourceEnv: selectedEnv.name })
|
||||
selectedEnv?.variables.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
||||
)
|
||||
globalVars.forEach(({ key, value, secret }) =>
|
||||
results.push({ key, value, secret, sourceEnv: "Global" })
|
||||
globalVars.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: "Global" })
|
||||
)
|
||||
|
||||
return results
|
||||
@@ -608,7 +593,7 @@ export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
|
||||
|
||||
export function setEnvironmentVariables(
|
||||
envIndex: number,
|
||||
vars: { key: string; value: string; secret: boolean }[]
|
||||
vars: { key: string; value: string }[]
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "setEnvironmentVariables",
|
||||
@@ -621,7 +606,7 @@ export function setEnvironmentVariables(
|
||||
|
||||
export function addEnvironmentVariable(
|
||||
envIndex: number,
|
||||
{ key, value, secret }: { key: string; value: string; secret: boolean }
|
||||
{ key, value }: { key: string; value: string }
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "addEnvironmentVariable",
|
||||
@@ -629,7 +614,6 @@ export function addEnvironmentVariable(
|
||||
envIndex,
|
||||
key,
|
||||
value,
|
||||
secret,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -650,7 +634,7 @@ export function removeEnvironmentVariable(
|
||||
export function updateEnvironmentVariable(
|
||||
envIndex: number,
|
||||
variableIndex: number,
|
||||
{ key, value, secret }: { key: string; value: string; secret: boolean }
|
||||
{ key, value }: { key: string; value: string }
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "updateEnvironmentVariable",
|
||||
@@ -659,7 +643,6 @@ export function updateEnvironmentVariable(
|
||||
variableIndex,
|
||||
updatedKey: key,
|
||||
updatedValue: value,
|
||||
updatedSecret: secret,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,14 +44,9 @@ import { SSERequest$, setSSERequest } from "./SSESession"
|
||||
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
|
||||
import { bulkApplyLocalState, localStateStore } from "./localstate"
|
||||
import { StorageLike, watchDebounced } from "@vueuse/core"
|
||||
import {
|
||||
loadTabsFromPersistedState,
|
||||
persistableTabState,
|
||||
} from "~/helpers/rest/tab"
|
||||
import {
|
||||
loadTabsFromPersistedState as loadGQLTabsFromPersistedState,
|
||||
persistableTabState as persistableGQLTabState,
|
||||
} from "~/helpers/graphql/tab"
|
||||
import { getService } from "~/modules/dioc"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
function checkAndMigrateOldSettings() {
|
||||
if (window.localStorage.getItem("selectedEnvIndex")) {
|
||||
@@ -320,11 +315,13 @@ function setupGlobalEnvsPersistence() {
|
||||
|
||||
// TODO: Graceful error handling ?
|
||||
export function setupRESTTabsPersistence() {
|
||||
const tabService = getService(RESTTabService)
|
||||
|
||||
try {
|
||||
const state = window.localStorage.getItem("restTabState")
|
||||
if (state) {
|
||||
const data = JSON.parse(state)
|
||||
loadTabsFromPersistedState(data)
|
||||
tabService.loadTabsFromPersistedState(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -334,7 +331,7 @@ export function setupRESTTabsPersistence() {
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
persistableTabState,
|
||||
tabService.persistableTabState,
|
||||
(state) => {
|
||||
window.localStorage.setItem("restTabState", JSON.stringify(state))
|
||||
},
|
||||
@@ -343,11 +340,13 @@ export function setupRESTTabsPersistence() {
|
||||
}
|
||||
|
||||
function setupGQLTabsPersistence() {
|
||||
const tabService = getService(GQLTabService)
|
||||
|
||||
try {
|
||||
const state = window.localStorage.getItem("gqlTabState")
|
||||
if (state) {
|
||||
const data = JSON.parse(state)
|
||||
loadGQLTabsFromPersistedState(data)
|
||||
tabService.loadTabsFromPersistedState(data)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(
|
||||
@@ -357,7 +356,7 @@ function setupGQLTabsPersistence() {
|
||||
}
|
||||
|
||||
watchDebounced(
|
||||
persistableGQLTabState,
|
||||
tabService.persistableTabState,
|
||||
(state) => {
|
||||
window.localStorage.setItem("gqlTabState", JSON.stringify(state))
|
||||
},
|
||||
|
||||
@@ -7,23 +7,24 @@
|
||||
<HoppSmartWindows
|
||||
v-if="currentTabID"
|
||||
:id="'gql_windows'"
|
||||
v-model="currentTabID"
|
||||
:model-value="currentTabID"
|
||||
@update:model-value="(tabID) => tabs.setActiveTab(tabID)"
|
||||
@remove-tab="removeTab"
|
||||
@add-tab="addNewTab"
|
||||
@sort="sortTabs"
|
||||
>
|
||||
<HoppSmartWindow
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in activeTabs"
|
||||
:id="tab.id"
|
||||
:key="'removable_tab_' + tab.id"
|
||||
:label="tab.document.request.name"
|
||||
:is-removable="tabs.length > 1"
|
||||
:is-removable="activeTabs.length > 1"
|
||||
:close-visibility="'hover'"
|
||||
>
|
||||
<template #tabhead>
|
||||
<GraphqlTabHead
|
||||
:tab="tab"
|
||||
:is-removable="tabs.length > 1"
|
||||
:is-removable="activeTabs.length > 1"
|
||||
@open-rename-modal="openReqRenameModal(tab)"
|
||||
@close-tab="removeTab(tab.id)"
|
||||
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
||||
@@ -89,21 +90,15 @@ import { computed, onBeforeUnmount, ref } from "vue"
|
||||
import { defineActionHandler } from "~/helpers/actions"
|
||||
import { connection, disconnect } from "~/helpers/graphql/connection"
|
||||
import { getDefaultGQLRequest } from "~/helpers/graphql/default"
|
||||
import {
|
||||
HoppGQLTab,
|
||||
closeOtherTabs,
|
||||
closeTab,
|
||||
createNewTab,
|
||||
currentTabID,
|
||||
getActiveTabs,
|
||||
getDirtyTabsCount,
|
||||
getTabRef,
|
||||
updateTab,
|
||||
updateTabOrdering,
|
||||
} from "~/helpers/graphql/tab"
|
||||
import { HoppGQLDocument } from "~/helpers/graphql/document"
|
||||
import { InspectionService } from "~/services/inspection"
|
||||
import { HoppTab } from "~/services/tab"
|
||||
import { GQLTabService } from "~/services/tab/graphql"
|
||||
|
||||
const t = useI18n()
|
||||
const tabs = useService(GQLTabService)
|
||||
|
||||
const currentTabID = computed(() => tabs.currentTabID.value)
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
@@ -113,27 +108,27 @@ usePageHead({
|
||||
title: computed(() => t("navigation.graphql")),
|
||||
})
|
||||
|
||||
const tabs = getActiveTabs()
|
||||
const activeTabs = tabs.getActiveTabs()
|
||||
|
||||
const addNewTab = () => {
|
||||
const tab = createNewTab({
|
||||
const tab = tabs.createNewTab({
|
||||
request: getDefaultGQLRequest(),
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
currentTabID.value = tab.id
|
||||
tabs.setActiveTab(tab.id)
|
||||
}
|
||||
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||
updateTabOrdering(e.oldIndex, e.newIndex)
|
||||
tabs.updateTabOrdering(e.oldIndex, e.newIndex)
|
||||
}
|
||||
|
||||
const removeTab = (tabID: string) => {
|
||||
const tabState = getTabRef(tabID).value
|
||||
const tabState = tabs.getTabRef(tabID).value
|
||||
|
||||
if (tabState.document.isDirty) {
|
||||
confirmingCloseForTabID.value = tabID
|
||||
} else {
|
||||
closeTab(tabState.id)
|
||||
tabs.closeTab(tabState.id)
|
||||
inspectionService.deleteTabInspectorResult(tabState.id)
|
||||
}
|
||||
}
|
||||
@@ -150,7 +145,7 @@ const onCloseConfirm = () => {
|
||||
*/
|
||||
const onResolveConfirm = () => {
|
||||
if (confirmingCloseForTabID.value) {
|
||||
closeTab(confirmingCloseForTabID.value)
|
||||
tabs.closeTab(confirmingCloseForTabID.value)
|
||||
confirmingCloseForTabID.value = null
|
||||
}
|
||||
}
|
||||
@@ -160,24 +155,24 @@ const unsavedTabsCount = ref(0)
|
||||
const exceptedTabID = ref<string | null>(null)
|
||||
|
||||
const closeOtherTabsAction = (tabID: string) => {
|
||||
const dirtyTabCount = getDirtyTabsCount()
|
||||
const dirtyTabCount = tabs.getDirtyTabsCount()
|
||||
// If there are dirty tabs, show the confirm modal
|
||||
if (dirtyTabCount > 0) {
|
||||
confirmingCloseAllTabs.value = true
|
||||
unsavedTabsCount.value = dirtyTabCount
|
||||
exceptedTabID.value = tabID
|
||||
} else {
|
||||
closeOtherTabs(tabID)
|
||||
tabs.closeOtherTabs(tabID)
|
||||
}
|
||||
}
|
||||
|
||||
const onResolveConfirmCloseAllTabs = () => {
|
||||
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
|
||||
if (exceptedTabID.value) tabs.closeOtherTabs(exceptedTabID.value)
|
||||
confirmingCloseAllTabs.value = false
|
||||
}
|
||||
|
||||
const onTabUpdate = (tab: HoppGQLTab) => {
|
||||
updateTab(tab)
|
||||
const onTabUpdate = (tab: HoppTab<HoppGQLDocument>) => {
|
||||
tabs.updateTab(tab)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
@@ -189,33 +184,33 @@ onBeforeUnmount(() => {
|
||||
const editReqModalReqName = ref("")
|
||||
const showRenamingReqNameModalForTabID = ref<string>()
|
||||
|
||||
const openReqRenameModal = (tab: HoppGQLTab) => {
|
||||
const openReqRenameModal = (tab: HoppTab<HoppGQLDocument>) => {
|
||||
editReqModalReqName.value = tab.document.request.name
|
||||
showRenamingReqNameModalForTabID.value = tab.id
|
||||
}
|
||||
|
||||
const renameReqName = () => {
|
||||
const tab = getTabRef(showRenamingReqNameModalForTabID.value!)
|
||||
const tab = tabs.getTabRef(showRenamingReqNameModalForTabID.value!)
|
||||
if (tab.value) {
|
||||
tab.value.document.request.name = editReqModalReqName.value
|
||||
updateTab(tab.value)
|
||||
tabs.updateTab(tab.value)
|
||||
}
|
||||
showRenamingReqNameModalForTabID.value = undefined
|
||||
}
|
||||
|
||||
const duplicateTab = (tabID: string) => {
|
||||
const tab = getTabRef(tabID)
|
||||
const tab = tabs.getTabRef(tabID)
|
||||
if (tab.value) {
|
||||
const newTab = createNewTab({
|
||||
const newTab = tabs.createNewTab({
|
||||
request: tab.value.document.request,
|
||||
isDirty: true,
|
||||
})
|
||||
currentTabID.value = newTab.id
|
||||
tabs.setActiveTab(newTab.id)
|
||||
}
|
||||
}
|
||||
|
||||
defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
saveContext,
|
||||
request: request,
|
||||
isDirty: false,
|
||||
@@ -223,7 +218,7 @@ defineActionHandler("gql.request.open", ({ request, saveContext }) => {
|
||||
})
|
||||
|
||||
defineActionHandler("request.rename", () => {
|
||||
openReqRenameModal(getTabRef(currentTabID.value).value!)
|
||||
openReqRenameModal(tabs.getTabRef(currentTabID.value).value!)
|
||||
})
|
||||
|
||||
defineActionHandler("tab.duplicate-tab", ({ tabID }) => {
|
||||
@@ -233,7 +228,7 @@ defineActionHandler("tab.close-current", () => {
|
||||
removeTab(currentTabID.value)
|
||||
})
|
||||
defineActionHandler("tab.close-other", () => {
|
||||
closeOtherTabs(currentTabID.value)
|
||||
tabs.closeOtherTabs(currentTabID.value)
|
||||
})
|
||||
defineActionHandler("tab.open-new", addNewTab)
|
||||
</script>
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
@sort="sortTabs"
|
||||
>
|
||||
<HoppSmartWindow
|
||||
v-for="tab in tabs"
|
||||
v-for="tab in activeTabs"
|
||||
:id="tab.id"
|
||||
:key="tab.id"
|
||||
:label="tab.document.request.name"
|
||||
:is-removable="tabs.length > 1"
|
||||
:is-removable="activeTabs.length > 1"
|
||||
:close-visibility="'hover'"
|
||||
>
|
||||
<template #tabhead>
|
||||
<HttpTabHead
|
||||
:tab="tab"
|
||||
:is-removable="tabs.length > 1"
|
||||
:is-removable="activeTabs.length > 1"
|
||||
@open-rename-modal="openReqRenameModal(tab.id)"
|
||||
@close-tab="removeTab(tab.id)"
|
||||
@close-other-tabs="closeOtherTabsAction(tab.id)"
|
||||
@@ -99,21 +99,6 @@ import { safelyExtractRESTRequest } from "@hoppscotch/data"
|
||||
import { translateExtURLParams } from "~/helpers/RESTExtURLParams"
|
||||
import { useRoute } from "vue-router"
|
||||
import { useI18n } from "@composables/i18n"
|
||||
import {
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
createNewTab,
|
||||
currentActiveTab,
|
||||
currentTabID,
|
||||
getActiveTabs,
|
||||
getTabRef,
|
||||
HoppRESTTab,
|
||||
loadTabsFromPersistedState,
|
||||
persistableTabState,
|
||||
updateTab,
|
||||
updateTabOrdering,
|
||||
getDirtyTabsCount,
|
||||
} from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||
import { onLoggedIn } from "~/composables/auth"
|
||||
@@ -128,7 +113,6 @@ import {
|
||||
Subscription,
|
||||
} from "rxjs"
|
||||
import { useToast } from "~/composables/toast"
|
||||
import { PersistableRESTTabState } from "~/helpers/rest/tab"
|
||||
import { watchDebounced } from "@vueuse/core"
|
||||
import { oauthRedirect } from "~/helpers/oauth"
|
||||
import { useReadonlyStream } from "~/composables/stream"
|
||||
@@ -142,6 +126,9 @@ import { HeaderInspectorService } from "~/services/inspection/inspectors/header.
|
||||
import { EnvironmentInspectorService } from "~/services/inspection/inspectors/environment.inspector"
|
||||
import { ResponseInspectorService } from "~/services/inspection/inspectors/response.inspector"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { HoppTab, PersistableTabState } from "~/services/tab"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
const savingRequest = ref(false)
|
||||
const confirmingCloseForTabID = ref<string | null>(null)
|
||||
@@ -155,6 +142,10 @@ const renameTabID = ref<string | null>(null)
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const currentTabID = tabs.currentTabID
|
||||
|
||||
type PopupDetails = {
|
||||
show: boolean
|
||||
position: {
|
||||
@@ -173,13 +164,13 @@ const contextMenu = ref<PopupDetails>({
|
||||
text: null,
|
||||
})
|
||||
|
||||
const tabs = getActiveTabs()
|
||||
const activeTabs = tabs.getActiveTabs()
|
||||
|
||||
const confirmSync = useReadonlyStream(currentSyncingStatus$, {
|
||||
isInitialSync: false,
|
||||
shouldSync: true,
|
||||
})
|
||||
const tabStateForSync = ref<PersistableRESTTabState | null>(null)
|
||||
const tabStateForSync = ref<PersistableTabState<HoppRESTDocument> | null>(null)
|
||||
|
||||
function bindRequestToURLParams() {
|
||||
const route = useRoute()
|
||||
@@ -190,91 +181,92 @@ function bindRequestToURLParams() {
|
||||
// We skip URL params parsing
|
||||
if (Object.keys(query).length === 0 || query.code || query.error) return
|
||||
|
||||
const request = currentActiveTab.value.document.request
|
||||
const request = tabs.currentActiveTab.value.document.request
|
||||
|
||||
currentActiveTab.value.document.request = safelyExtractRESTRequest(
|
||||
tabs.currentActiveTab.value.document.request = safelyExtractRESTRequest(
|
||||
translateExtURLParams(query, request),
|
||||
getDefaultRESTRequest()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const onTabUpdate = (tab: HoppRESTTab) => {
|
||||
updateTab(tab)
|
||||
const onTabUpdate = (tab: HoppTab<HoppRESTDocument>) => {
|
||||
tabs.updateTab(tab)
|
||||
}
|
||||
|
||||
const addNewTab = () => {
|
||||
const tab = createNewTab({
|
||||
const tab = tabs.createNewTab({
|
||||
request: getDefaultRESTRequest(),
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
currentTabID.value = tab.id
|
||||
tabs.setActiveTab(tab.id)
|
||||
}
|
||||
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||
updateTabOrdering(e.oldIndex, e.newIndex)
|
||||
tabs.updateTabOrdering(e.oldIndex, e.newIndex)
|
||||
}
|
||||
|
||||
const inspectionService = useService(InspectionService)
|
||||
|
||||
const removeTab = (tabID: string) => {
|
||||
const tabState = getTabRef(tabID).value
|
||||
const tabState = tabs.getTabRef(tabID).value
|
||||
|
||||
if (tabState.document.isDirty) {
|
||||
confirmingCloseForTabID.value = tabID
|
||||
} else {
|
||||
closeTab(tabState.id)
|
||||
tabs.closeTab(tabState.id)
|
||||
inspectionService.deleteTabInspectorResult(tabState.id)
|
||||
}
|
||||
}
|
||||
|
||||
const closeOtherTabsAction = (tabID: string) => {
|
||||
const isTabDirty = getTabRef(tabID).value?.document.isDirty
|
||||
const dirtyTabCount = getDirtyTabsCount()
|
||||
const isTabDirty = tabs.getTabRef(tabID).value?.document.isDirty
|
||||
const dirtyTabCount = tabs.getDirtyTabsCount()
|
||||
// If current tab is dirty, so we need to subtract 1 from the dirty tab count
|
||||
const balanceDirtyTabCount = isTabDirty ? dirtyTabCount - 1 : dirtyTabCount
|
||||
|
||||
// If there are dirty tabs, show the confirm modal
|
||||
if (balanceDirtyTabCount > 0) {
|
||||
confirmingCloseAllTabs.value = true
|
||||
unsavedTabsCount.value = balanceDirtyTabCount
|
||||
exceptedTabID.value = tabID
|
||||
} else {
|
||||
closeOtherTabs(tabID)
|
||||
tabs.closeOtherTabs(tabID)
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateTab = (tabID: string) => {
|
||||
const tab = getTabRef(tabID)
|
||||
const tab = tabs.getTabRef(tabID)
|
||||
if (tab.value) {
|
||||
const newTab = createNewTab({
|
||||
const newTab = tabs.createNewTab({
|
||||
request: cloneDeep(tab.value.document.request),
|
||||
isDirty: true,
|
||||
})
|
||||
currentTabID.value = newTab.id
|
||||
tabs.setActiveTab(newTab.id)
|
||||
}
|
||||
}
|
||||
|
||||
const onResolveConfirmCloseAllTabs = () => {
|
||||
if (exceptedTabID.value) closeOtherTabs(exceptedTabID.value)
|
||||
if (exceptedTabID.value) tabs.closeOtherTabs(exceptedTabID.value)
|
||||
confirmingCloseAllTabs.value = false
|
||||
}
|
||||
|
||||
const openReqRenameModal = (tabID?: string) => {
|
||||
if (tabID) {
|
||||
const tab = getTabRef(tabID)
|
||||
const tab = tabs.getTabRef(tabID)
|
||||
reqName.value = tab.value.document.request.name
|
||||
renameTabID.value = tabID
|
||||
} else {
|
||||
reqName.value = currentActiveTab.value.document.request.name
|
||||
reqName.value = tabs.currentActiveTab.value.document.request.name
|
||||
}
|
||||
showRenamingReqNameModal.value = true
|
||||
}
|
||||
|
||||
const renameReqName = () => {
|
||||
const tab = getTabRef(renameTabID.value ?? currentTabID.value)
|
||||
const tab = tabs.getTabRef(renameTabID.value ?? currentTabID.value)
|
||||
if (tab.value) {
|
||||
tab.value.document.request.name = reqName.value
|
||||
updateTab(tab.value)
|
||||
tabs.updateTab(tab.value)
|
||||
}
|
||||
showRenamingReqNameModal.value = false
|
||||
}
|
||||
@@ -284,7 +276,7 @@ const renameReqName = () => {
|
||||
*/
|
||||
const onCloseConfirmSaveTab = () => {
|
||||
if (!savingRequest.value && confirmingCloseForTabID.value) {
|
||||
closeTab(confirmingCloseForTabID.value)
|
||||
tabs.closeTab(confirmingCloseForTabID.value)
|
||||
inspectionService.deleteTabInspectorResult(confirmingCloseForTabID.value)
|
||||
confirmingCloseForTabID.value = null
|
||||
}
|
||||
@@ -294,11 +286,11 @@ const onCloseConfirmSaveTab = () => {
|
||||
* Called when the user confirms they want to save the tab
|
||||
*/
|
||||
const onResolveConfirmSaveTab = () => {
|
||||
if (currentActiveTab.value.document.saveContext) {
|
||||
if (tabs.currentActiveTab.value.document.saveContext) {
|
||||
invokeAction("request.save")
|
||||
|
||||
if (confirmingCloseForTabID.value) {
|
||||
closeTab(confirmingCloseForTabID.value)
|
||||
tabs.closeTab(confirmingCloseForTabID.value)
|
||||
confirmingCloseForTabID.value = null
|
||||
}
|
||||
} else {
|
||||
@@ -312,13 +304,14 @@ const onResolveConfirmSaveTab = () => {
|
||||
const onSaveModalClose = () => {
|
||||
savingRequest.value = false
|
||||
if (confirmingCloseForTabID.value) {
|
||||
closeTab(confirmingCloseForTabID.value)
|
||||
tabs.closeTab(confirmingCloseForTabID.value)
|
||||
confirmingCloseForTabID.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const syncTabState = () => {
|
||||
if (tabStateForSync.value) loadTabsFromPersistedState(tabStateForSync.value)
|
||||
if (tabStateForSync.value)
|
||||
tabs.loadTabsFromPersistedState(tabStateForSync.value)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,10 +322,11 @@ const syncTabState = () => {
|
||||
*/
|
||||
function startTabStateSync(): Subscription {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
const tabState$ = new BehaviorSubject<PersistableRESTTabState | null>(null)
|
||||
const tabState$ =
|
||||
new BehaviorSubject<PersistableTabState<HoppRESTDocument> | null>(null)
|
||||
|
||||
watchDebounced(
|
||||
persistableTabState,
|
||||
tabs.persistableTabState,
|
||||
(state) => {
|
||||
tabState$.next(state)
|
||||
},
|
||||
@@ -429,9 +423,10 @@ function oAuthURL() {
|
||||
tokenInfo.hasOwnProperty("access_token")
|
||||
) {
|
||||
if (
|
||||
currentActiveTab.value.document.request.auth.authType === "oauth-2"
|
||||
tabs.currentActiveTab.value.document.request.auth.authType ===
|
||||
"oauth-2"
|
||||
) {
|
||||
currentActiveTab.value.document.request.auth.token =
|
||||
tabs.currentActiveTab.value.document.request.auth.token =
|
||||
tokenInfo.access_token
|
||||
}
|
||||
}
|
||||
@@ -462,7 +457,7 @@ bindRequestToURLParams()
|
||||
oAuthURL()
|
||||
|
||||
defineActionHandler("rest.request.open", ({ doc }) => {
|
||||
createNewTab(doc)
|
||||
tabs.createNewTab(doc)
|
||||
})
|
||||
|
||||
defineActionHandler("request.rename", openReqRenameModal)
|
||||
@@ -473,7 +468,7 @@ defineActionHandler("tab.close-current", () => {
|
||||
removeTab(currentTabID.value)
|
||||
})
|
||||
defineActionHandler("tab.close-other", () => {
|
||||
closeOtherTabs(currentTabID.value)
|
||||
tabs.closeOtherTabs(currentTabID.value)
|
||||
})
|
||||
defineActionHandler("tab.open-new", addNewTab)
|
||||
|
||||
|
||||
@@ -82,15 +82,18 @@ import {
|
||||
|
||||
import IconHome from "~icons/lucide/home"
|
||||
import IconRefreshCW from "~icons/lucide/refresh-cw"
|
||||
import { createNewTab } from "~/helpers/rest/tab"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
import { platform } from "~/platform"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { useService } from "dioc/vue"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const tabs = useService(RESTTabService)
|
||||
|
||||
const invalidLink = ref(false)
|
||||
const shortcodeID = ref("")
|
||||
|
||||
@@ -127,7 +130,7 @@ const addRequestToTab = () => {
|
||||
|
||||
const request: unknown = JSON.parse(data.right.shortcode?.request as string)
|
||||
|
||||
createNewTab({
|
||||
tabs.createNewTab({
|
||||
request: safelyExtractRESTRequest(request, getDefaultRESTRequest()),
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PersistableRESTTabState } from "~/helpers/rest/tab"
|
||||
import { PersistableTabState } from "~/services/tab"
|
||||
import { HoppUser } from "./auth"
|
||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||
|
||||
export type TabStatePlatformDef = {
|
||||
loadTabStateFromSync: () => Promise<PersistableRESTTabState | null>
|
||||
loadTabStateFromSync: () => Promise<PersistableTabState<HoppRESTDocument> | null>
|
||||
writeCurrentTabState: (
|
||||
user: HoppUser,
|
||||
persistableTabState: PersistableRESTTabState
|
||||
persistableTabState: PersistableTabState<HoppRESTDocument>
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -11,15 +11,6 @@ vi.mock("~/modules/i18n", () => ({
|
||||
getI18n: () => (x: string) => x,
|
||||
}))
|
||||
|
||||
const tabMock = vi.hoisted(() => ({
|
||||
currentActiveTab: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("~/helpers/rest/tab", () => ({
|
||||
__esModule: true,
|
||||
currentActiveTab: tabMock.currentActiveTab,
|
||||
}))
|
||||
|
||||
describe("ParameterMenuService", () => {
|
||||
it("registers with the contextmenu service upon initialization", () => {
|
||||
const container = new TestContainer()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { TestContainer } from "dioc/testing"
|
||||
import { describe, expect, it, vi } from "vitest"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { ContextMenuService } from "../.."
|
||||
import { URLMenuService } from "../url.menu"
|
||||
import { getDefaultRESTRequest } from "~/helpers/rest/default"
|
||||
@@ -9,15 +10,6 @@ vi.mock("~/modules/i18n", () => ({
|
||||
getI18n: () => (x: string) => x,
|
||||
}))
|
||||
|
||||
const tabMock = vi.hoisted(() => ({
|
||||
createNewTab: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock("~/helpers/rest/tab", () => ({
|
||||
__esModule: true,
|
||||
createNewTab: tabMock.createNewTab,
|
||||
}))
|
||||
|
||||
describe("URLMenuService", () => {
|
||||
it("registers with the contextmenu service upon initialization", () => {
|
||||
const container = new TestContainer()
|
||||
@@ -64,6 +56,10 @@ describe("URLMenuService", () => {
|
||||
|
||||
it("should call the openNewTab function when action is called and a new hoppscotch tab is opened", () => {
|
||||
const container = new TestContainer()
|
||||
const createNewTabFn = vi.fn()
|
||||
container.bindMock(RESTTabService, {
|
||||
createNewTab: createNewTabFn,
|
||||
})
|
||||
const url = container.bind(URLMenuService)
|
||||
|
||||
const test = "https://hoppscotch.io"
|
||||
@@ -76,8 +72,8 @@ describe("URLMenuService", () => {
|
||||
endpoint: test,
|
||||
}
|
||||
|
||||
expect(tabMock.createNewTab).toHaveBeenCalledOnce()
|
||||
expect(tabMock.createNewTab).toHaveBeenCalledWith({
|
||||
expect(createNewTabFn).toHaveBeenCalledOnce()
|
||||
expect(createNewTabFn).toHaveBeenCalledWith({
|
||||
request: request,
|
||||
isDirty: false,
|
||||
})
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
} from "../"
|
||||
import { markRaw, ref } from "vue"
|
||||
import IconArrowDownRight from "~icons/lucide/arrow-down-right"
|
||||
import { currentActiveTab } from "~/helpers/rest/tab"
|
||||
import { getI18n } from "~/modules/i18n"
|
||||
import { RESTTabService } from "~/services/tab/rest"
|
||||
import { getService } from "~/modules/dioc"
|
||||
|
||||
//regex containing both url and parameter
|
||||
const urlAndParameterRegex = new RegExp("[^&?]*?=[^&?]*")
|
||||
@@ -88,20 +89,23 @@ export class ParameterMenuService extends Service implements ContextMenu {
|
||||
queryParams.push({ key, value, active: true })
|
||||
}
|
||||
|
||||
const tabService = getService(RESTTabService)
|
||||
|
||||
// add the parameters to the current request parameters
|
||||
currentActiveTab.value.document.request.params = [
|
||||
...currentActiveTab.value.document.request.params,
|
||||
tabService.currentActiveTab.value.document.request.params = [
|
||||
...tabService.currentActiveTab.value.document.request.params,
|
||||
...queryParams,
|
||||
]
|
||||
|
||||
if (newURL) {
|
||||
currentActiveTab.value.document.request.endpoint = newURL
|
||||
tabService.currentActiveTab.value.document.request.endpoint = newURL
|
||||
} else {
|
||||
// remove the parameter from the URL
|
||||
const textRegex = new RegExp(`\\b${text.replace(/\?/g, "")}\\b`, "gi")
|
||||
const sanitizedWord = currentActiveTab.value.document.request.endpoint
|
||||
const sanitizedWord =
|
||||
tabService.currentActiveTab.value.document.request.endpoint
|
||||
const newURL = sanitizedWord.replace(textRegex, "")
|
||||
currentActiveTab.value.document.request.endpoint = newURL
|
||||
tabService.currentActiveTab.value.document.request.endpoint = newURL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user