Compare commits

..

29 Commits

Author SHA1 Message Date
Mir Arif Hasan
eec5b8f9c8 chore: app shutdown way changed 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
99d369fc79 chore: renamed an enum 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
7da5063a8c chore: feedback resolve 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
1957556b5a test: fix test case 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
84fdcaf840 fix: add validation checks for MAILER_ADDRESS_FROM 2023-12-12 16:40:06 +06:00
Andrew Bastin
1f684d47e2 chore: update lockfile 2023-12-12 16:40:06 +06:00
Mir Arif Hasan
59dcd57d9c fix: uppercase issue of VITE_ALLOWED_AUTH_PROVIDERS 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
fc7817780d chore: update code comments 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
0f7aa5e84d feat: feedback resolve 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
85dea99452 feat: get api added for fetch provider list 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
94ca981144 fix: validateSMTPUrl check 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
8035d1d592 fix: allowedAuthProviders and enableAndDisableSSO 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
02dcc018fa fix: pnpm i without db connection 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
11e2e18aa3 fix: all sso disabling is handled 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
97d71e5032 chore: smtp url validation added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
180b9d776f fix: mailer module init issue 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
c4a9038579 feat: add query infra-configs 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
8df8b056d9 feat: added mutations and query 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
01ccb321f1 fix: mailer module init with right env value 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
9f62ed6c90 fix: config service precedence 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
ef8412544a feat: removed saml stuffs 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
1d05a76b59 chore: remove saml stuff 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
6157848f56 feat: utilise ConfigService in util functions 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
0a10f7c654 feat: update infra configs mutation added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
4e188f3c11 test: fix test case failure 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
c3522025c8 feat: infra config module add with get-update-reset functionality 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
74cec0365b test: fix all broken test case 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
5bee3471c5 feat: nestjs config package added 2023-12-12 16:39:30 +06:00
Mir Arif Hasan
49c2ee0d38 feat: restart cmd added in aio service 2023-12-12 16:38:50 +06:00
142 changed files with 1193 additions and 4945 deletions

View File

@@ -1,64 +1,63 @@
import { ExecException } from "child_process";
import { HoppErrorCode } from "../../types/errors";
import { runCLI, getErrorCode, getTestJsonFilePath } from "../utils";
import { execAsync, getErrorCode, getTestJsonFilePath } from "../utils";
describe("Test 'hopp test <file>' command:", () => {
test("No collection file path provided.", async () => {
const args = "test";
const { stderr } = await runCLI(args);
const cmd = `node ./bin/hopp test`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not found.", async () => {
const args = "test notfound.json";
const { stderr } = await runCLI(args);
const cmd = `node ./bin/hopp test notfound.json`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("Collection file is invalid JSON.", async () => {
const args = `test ${getTestJsonFilePath(
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection.json"
)}`;
const { stderr } = await runCLI(args);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const args = `test ${getTestJsonFilePath(
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
)}`;
const { stderr } = await runCLI(args);
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("MALFORMED_COLLECTION");
});
test("Invalid arguement.", async () => {
const args = "invalid-arg";
const { stderr } = await runCLI(args);
const cmd = `node ./bin/hopp invalid-arg`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Collection file not JSON type.", async () => {
const args = `test ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await runCLI(args);
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("Some errors occured (exit code 1).", async () => {
const args = `test ${getTestJsonFilePath("fails.json")}`;
const { error } = await runCLI(args);
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("fails.json")}`;
const { error } = await execAsync(cmd);
expect(error).not.toBeNull();
expect(error).toMatchObject(<ExecException>{
@@ -67,83 +66,75 @@ describe("Test 'hopp test <file>' command:", () => {
});
test("No errors occured (exit code 0).", async () => {
const args = `test ${getTestJsonFilePath("passes.json")}`;
const { error } = await runCLI(args);
const cmd = `node ./bin/hopp test ${getTestJsonFilePath("passes.json")}`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});
test("Supports inheriting headers and authorization set at the root collection", async () => {
const args = `test ${getTestJsonFilePath("collection-level-headers-auth.json")}`;
const { error } = await runCLI(args);
expect(error).toBeNull();
})
});
describe("Test 'hopp test <file> --env <file>' command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No env file path provided.", async () => {
const args = `${VALID_TEST_ARGS} --env`;
const { stderr } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --env`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("ENV file not JSON type.", async () => {
const args = `${VALID_TEST_ARGS} --env ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --env ${getTestJsonFilePath("notjson.txt")}`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_FILE_TYPE");
});
test("ENV file not found.", async () => {
const args = `${VALID_TEST_ARGS} --env notfound.json`;
const { stderr } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --env notfound.json`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("FILE_NOT_FOUND");
});
test("No errors occured (exit code 0).", async () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
const args = `test ${TESTS_PATH} --env ${ENV_PATH}`;
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
const { error, stdout } = await execAsync(cmd);
const { error } = await runCLI(args);
expect(error).toBeNull();
});
});
describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const VALID_TEST_ARGS = `test ${getTestJsonFilePath(
const VALID_TEST_CMD = `node ./bin/hopp test ${getTestJsonFilePath(
"passes.json"
)}`;
test("No value passed to delay flag.", async () => {
const args = `${VALID_TEST_ARGS} --delay`;
const { stderr } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --delay`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Invalid value passed to delay flag.", async () => {
const args = `${VALID_TEST_ARGS} --delay 'NaN'`;
const { stderr } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});
test("Valid value passed to delay flag.", async () => {
const args = `${VALID_TEST_ARGS} --delay 1`;
const { error } = await runCLI(args);
const cmd = `${VALID_TEST_CMD} --delay 1`;
const { error } = await execAsync(cmd);
expect(error).toBeNull();
});

View File

@@ -1,221 +0,0 @@
[
{
"v": 1,
"name": "CollectionA",
"folders": [
{
"v": 1,
"name": "FolderA",
"folders": [
{
"v": 1,
"name": "FolderB",
"folders": [
{
"v": 1,
"name": "FolderC",
"folders": [],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestD",
"params": [],
"headers": [
{
"active": true,
"key": "X-Test-Header",
"value": "Overriden at RequestD"
}
],
"method": "GET",
"auth": {
"authType": "basic",
"authActive": true,
"username": "username",
"password": "password"
},
"preRequestScript": "",
"testScript": "pw.test(\"Overrides auth and headers set at the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Overriden at RequestD\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Basic dXNlcm5hbWU6cGFzc3dvcmQ=\");\n});",
"body": {
"contentType": null,
"body": null
}
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestC",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "inherit",
"authActive": true
},
"preRequestScript": "",
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Overriden at FolderB\");\n pw.expect(pw.response.body.headers[\"key\"]).toBe(\"test-key\");\n});",
"body": {
"contentType": null,
"body": null
}
}
],
"auth": {
"authType": "api-key",
"authActive": true,
"addTo": "Headers",
"key": "key",
"value": "test-key"
},
"headers": [
{
"active": true,
"key": "X-Test-Header",
"value": "Overriden at FolderB"
}
]
}
],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "inherit",
"authActive": true
},
"preRequestScript": "",
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
"body": {
"contentType": null,
"body": null
},
"id": "clpttpdq00003qp16kut6doqv"
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "inherit",
"authActive": true
},
"preRequestScript": "",
"testScript": "pw.test(\"Correctly inherits auth and headers from the root collection\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
"body": {
"contentType": null,
"body": null
},
"id": "clpttpdq00003qp16kut6doqv"
}
],
"headers": [
{
"active": true,
"key": "X-Test-Header",
"value": "Set at root collection"
}
],
"auth": {
"authType": "bearer",
"authActive": true,
"token": "BearerToken"
}
},
{
"v": 1,
"name": "CollectionB",
"folders": [
{
"v": 1,
"name": "FolderA",
"folders": [],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestB",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "inherit",
"authActive": true
},
"preRequestScript": "",
"testScript": "pw.test(\"Correctly inherits auth and headers from the parent folder\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
"body": {
"contentType": null,
"body": null
},
"id": "clpttpdq00003qp16kut6doqv"
}
],
"auth": {
"authType": "inherit",
"authActive": true
},
"headers": []
}
],
"requests": [
{
"v": "1",
"endpoint": "https://echo.hoppscotch.io",
"name": "RequestA",
"params": [],
"headers": [],
"method": "GET",
"auth": {
"authType": "inherit",
"authActive": true
},
"preRequestScript": "",
"testScript": "pw.test(\"Correctly inherits auth and headers from the root collection\", ()=> {\n pw.expect(pw.response.body.headers[\"x-test-header\"]).toBe(\"Set at root collection\");\n pw.expect(pw.response.body.headers[\"authorization\"]).toBe(\"Bearer BearerToken\");\n});",
"body": {
"contentType": null,
"body": null
},
"id": "clpttpdq00003qp16kut6doqv"
}
],
"headers": [
{
"active": true,
"key": "X-Test-Header",
"value": "Set at root collection"
}
],
"auth": {
"authType": "bearer",
"authActive": true,
"token": "BearerToken"
}
}
]

View File

@@ -1,17 +1,10 @@
import { exec } from "child_process";
import { resolve } from "path";
import { ExecResponse } from "./types";
export const runCLI = (args: string): Promise<ExecResponse> =>
{
const CLI_PATH = resolve(__dirname, "../../bin/hopp");
const command = `node ${CLI_PATH} ${args}`
return new Promise((resolve) =>
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
);
}
export const execAsync = (command: string): Promise<ExecResponse> =>
new Promise((resolve) =>
exec(command, (error, stdout, stderr) => resolve({ error, stdout, stderr }))
);
export const trimAnsi = (target: string) => {
const ansiRegex =

View File

@@ -1,8 +1,8 @@
import { HoppCollection } from "@hoppscotch/data";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { HoppEnvs } from "./request";
export type CollectionRunnerParam = {
collections: HoppCollection[];
collections: HoppCollection<HoppRESTRequest>[];
envs: HoppEnvs;
delay?: number;
};

View File

@@ -33,7 +33,7 @@ export type HoppEnvs = {
export type CollectionStack = {
path: string;
collection: HoppCollection;
collection: HoppCollection<HoppRESTRequest>;
};
export type RequestReport = {

View File

@@ -1,4 +1,8 @@
import { HoppCollection, isHoppRESTRequest } from "@hoppscotch/data";
import {
HoppCollection,
HoppRESTRequest,
isHoppRESTRequest,
} from "@hoppscotch/data";
import * as A from "fp-ts/Array";
import { CommanderError } from "commander";
import { HoppCLIError, HoppErrnoException } from "../types/errors";
@@ -20,7 +24,9 @@ export const hasProperty = <P extends PropertyKey>(
* @returns True, if unknown parameter is valid Hoppscotch REST Collection;
* False, otherwise.
*/
export const isRESTCollection = (param: unknown): param is HoppCollection => {
export const isRESTCollection = (
param: unknown
): param is HoppCollection<HoppRESTRequest> => {
if (!!param && typeof param === "object") {
if (!hasProperty(param, "v") || typeof param.v !== "number") {
return false;
@@ -56,6 +62,7 @@ export const isRESTCollection = (param: unknown): param is HoppCollection => {
return false;
};
/**
* Checks if given error data is of type HoppCLIError, based on existence
* of code property.

View File

@@ -1,23 +1,21 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import { bold } from "chalk";
import { log } from "console";
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";
import { bold } from "chalk";
import { log } from "console";
import round from "lodash/round";
import { CollectionRunnerParam } from "../types/collections";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import {
CollectionStack,
HoppEnvs,
ProcessRequestParams,
CollectionStack,
RequestReport,
ProcessRequestParams,
} from "../types/request";
import {
PreRequestMetrics,
RequestMetrics,
TestMetrics,
} from "../types/response";
import { DEFAULT_DURATION_PRECISION } from "./constants";
getRequestMetrics,
preProcessRequest,
processRequest,
} from "./request";
import { exceptionColors } from "./getters";
import {
printErrorsReport,
printFailedTestsReport,
@@ -25,14 +23,15 @@ import {
printRequestsMetrics,
printTestsMetrics,
} from "./display";
import { exceptionColors } from "./getters";
import { getPreRequestMetrics } from "./pre-request";
import {
getRequestMetrics,
preProcessRequest,
processRequest,
} from "./request";
PreRequestMetrics,
RequestMetrics,
TestMetrics,
} from "../types/response";
import { getTestMetrics } from "./test";
import { DEFAULT_DURATION_PRECISION } from "./constants";
import { getPreRequestMetrics } from "./pre-request";
import { CollectionRunnerParam } from "../types/collections";
const { WARN, FAIL } = exceptionColors;
@@ -42,23 +41,23 @@ const { WARN, FAIL } = exceptionColors;
* @param param Data of hopp-collection with hopp-requests, envs to be processed.
* @returns List of report for each processed request.
*/
export const collectionsRunner = async (
param: CollectionRunnerParam
): Promise<RequestReport[]> => {
const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack(
param.collections
);
export const collectionsRunner =
async (param: CollectionRunnerParam): Promise<RequestReport[]> =>
{
const envs: HoppEnvs = param.envs;
const delay = param.delay ?? 0;
const requestsReport: RequestReport[] = [];
const collectionStack: CollectionStack[] = getCollectionStack(
param.collections
);
while (collectionStack.length) {
// Pop out top-most collection from stack to be processed.
const { collection, path } = <CollectionStack>collectionStack.pop();
while (collectionStack.length) {
// Pop out top-most collection from stack to be processed.
const { collection, path } = <CollectionStack>collectionStack.pop();
// Processing each request in collection
for (const request of collection.requests) {
const _request = preProcessRequest(request as HoppRESTRequest, collection);
const _request = preProcessRequest(request);
const requestPath = `${path}/${_request.name}`;
const processRequestParams: ProcessRequestParams = {
path: requestPath,
@@ -70,13 +69,13 @@ export const collectionsRunner = async (
// Request processing initiated message.
log(WARN(`\nRunning: ${bold(requestPath)}`));
// Processing current request.
const result = await processRequest(processRequestParams)();
// Processing current request.
const result = await processRequest(processRequestParams)();
// Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs;
envs.global = global;
envs.selected = selected;
// Updating global & selected envs with new envs from processed-request output.
const { global, selected } = result.envs;
envs.global = global;
envs.selected = selected;
// Storing current request's report.
const requestReport = result.report;
@@ -85,30 +84,15 @@ export const collectionsRunner = async (
// Pushing remaining folders realted collection to stack.
for (const folder of collection.folders) {
const updatedFolder: HoppCollection = { ...folder }
if (updatedFolder.auth?.authType === "inherit") {
updatedFolder.auth = collection.auth;
}
if (collection.headers?.length) {
// Filter out header entries present in the parent collection under the same name
// This ensures the folder headers take precedence over the collection headers
const filteredHeaders = collection.headers.filter((collectionHeaderEntries) => {
return !updatedFolder.headers.some((folderHeaderEntries) => folderHeaderEntries.key === collectionHeaderEntries.key)
})
updatedFolder.headers.push(...filteredHeaders);
}
collectionStack.push({
path: `${path}/${updatedFolder.name}`,
collection: updatedFolder,
path: `${path}/${folder.name}`,
collection: folder,
});
}
}
return requestsReport;
};
return requestsReport;
};
/**
* Transforms collections to generate collection-stack which describes each collection's
@@ -116,7 +100,9 @@ export const collectionsRunner = async (
* @param collections Hopp-collection objects to be mapped to collection-stack type.
* @returns Mapped collections to collection-stack.
*/
const getCollectionStack = (collections: HoppCollection[]): CollectionStack[] =>
const getCollectionStack = (
collections: HoppCollection<HoppRESTRequest>[]
): CollectionStack[] =>
pipe(
collections,
A.map(

View File

@@ -2,7 +2,7 @@ import fs from "fs/promises";
import { FormDataEntry } from "../types/request";
import { error } from "../types/errors";
import { isRESTCollection, isHoppErrnoException } from "./checks";
import { HoppCollection } from "@hoppscotch/data";
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
/**
* Parses array of FormDataEntry to FormData.
@@ -35,20 +35,20 @@ export const parseErrorMessage = (e: unknown) => {
};
export async function readJsonFile(path: string): Promise<unknown> {
if (!path.endsWith(".json")) {
throw error({ code: "INVALID_FILE_TYPE", data: path });
if(!path.endsWith('.json')) {
throw error({ code: "INVALID_FILE_TYPE", data: path })
}
try {
await fs.access(path);
await fs.access(path)
} catch (e) {
throw error({ code: "FILE_NOT_FOUND", path: path });
throw error({ code: "FILE_NOT_FOUND", path: path })
}
try {
return JSON.parse((await fs.readFile(path)).toString());
} catch (e) {
throw error({ code: "UNKNOWN_ERROR", data: e });
return JSON.parse((await fs.readFile(path)).toString())
} catch(e) {
throw error({ code: "UNKNOWN_ERROR", data: e })
}
}
@@ -56,24 +56,22 @@ export async function readJsonFile(path: string): Promise<unknown> {
* Parses collection json file for given path:context.path, and validates
* the parsed collectiona array.
* @param path Collection json file path.
* @returns For successful parsing we get array of HoppCollection,
* @returns For successful parsing we get array of HoppCollection<HoppRESTRequest>,
*/
export async function parseCollectionData(
path: string
): Promise<HoppCollection[]> {
let contents = await readJsonFile(path);
): Promise<HoppCollection<HoppRESTRequest>[]> {
let contents = await readJsonFile(path)
const maybeArrayOfCollections: unknown[] = Array.isArray(contents)
? contents
: [contents];
const maybeArrayOfCollections: unknown[] = Array.isArray(contents) ? contents : [contents]
if (maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
if(maybeArrayOfCollections.some((x) => !isRESTCollection(x))) {
throw error({
code: "MALFORMED_COLLECTION",
path,
data: "Please check the collection data.",
});
})
}
return maybeArrayOfCollections as HoppCollection[];
}
return maybeArrayOfCollections as HoppCollection<HoppRESTRequest>[]
};

View File

@@ -1,31 +1,31 @@
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data";
import axios, { Method } from "axios";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
import { pipe } from "fp-ts/function";
import * as S from "fp-ts/string";
import { hrtime } from "process";
import { URL } from "url";
import { EffectiveHoppRESTRequest, RequestConfig } from "../interfaces/request";
import * as S from "fp-ts/string";
import * as A from "fp-ts/Array";
import * as T from "fp-ts/Task";
import * as E from "fp-ts/Either";
import * as TE from "fp-ts/TaskEither";
import { HoppRESTRequest } from "@hoppscotch/data";
import { responseErrors } from "./constants";
import { getDurationInSeconds, getMetaDataPairs } from "./getters";
import { testRunner, getTestScriptParams, hasFailedTestCases } from "./test";
import { RequestConfig, EffectiveHoppRESTRequest } from "../interfaces/request";
import { RequestRunnerResponse } from "../interfaces/response";
import { HoppCLIError, error } from "../types/errors";
import { preRequestScriptRunner } from "./pre-request";
import {
HoppEnvs,
ProcessRequestParams,
RequestReport,
} from "../types/request";
import { RequestMetrics } from "../types/response";
import { responseErrors } from "./constants";
import {
printPreRequestRunner,
printRequestRunner,
printTestRunner,
} from "./display";
import { getDurationInSeconds, getMetaDataPairs } from "./getters";
import { preRequestScriptRunner } from "./pre-request";
import { getTestScriptParams, hasFailedTestCases, testRunner } from "./test";
import { error, HoppCLIError } from "../types/errors";
import { hrtime } from "process";
import { RequestMetrics } from "../types/response";
import { pipe } from "fp-ts/function";
// !NOTE: The `config.supported` checks are temporary until OAuth2 and Multipart Forms are supported
@@ -309,12 +309,9 @@ export const processRequest =
* @returns Updated request object free of invalid/missing data.
*/
export const preProcessRequest = (
request: HoppRESTRequest,
collection: HoppCollection,
request: HoppRESTRequest
): HoppRESTRequest => {
const tempRequest = Object.assign({}, request);
const { headers: parentHeaders, auth: parentAuth } = collection;
if (!tempRequest.v) {
tempRequest.v = "1";
}
@@ -330,31 +327,18 @@ export const preProcessRequest = (
if (!tempRequest.params) {
tempRequest.params = [];
}
if (parentHeaders?.length) {
// Filter out header entries present in the parent (folder/collection) under the same name
// This ensures the child headers take precedence over the parent headers
const filteredEntries = parentHeaders.filter((parentHeaderEntries) => {
return !tempRequest.headers.some((reqHeaderEntries) => reqHeaderEntries.key === parentHeaderEntries.key)
})
tempRequest.headers.push(...filteredEntries);
} else if (!tempRequest.headers) {
if (!tempRequest.headers) {
tempRequest.headers = [];
}
if (!tempRequest.preRequestScript) {
tempRequest.preRequestScript = "";
}
if (!tempRequest.testScript) {
tempRequest.testScript = "";
}
if (tempRequest.auth?.authType === "inherit") {
tempRequest.auth = parentAuth;
} else if (!tempRequest.auth) {
if (!tempRequest.auth) {
tempRequest.auth = { authActive: false, authType: "none" };
}
if (!tempRequest.body) {
tempRequest.body = { contentType: null, body: null };
}

View File

@@ -158,7 +158,7 @@ a {
@apply shadow-none #{!important};
@apply fixed;
@apply inline-flex;
@apply -mt-7;
@apply -mt-8;
}
}
@@ -368,7 +368,6 @@ pre.ace_editor {
.toasted-container {
@apply max-w-md;
@apply z-[10000];
.toasted {
&.toasted-primary {
@@ -517,10 +516,9 @@ pre.ace_editor {
@apply bg-dividerLight;
@apply rounded;
@apply ml-2;
@apply px-0.5;
@apply min-w-[1rem];
@apply min-h-[1rem];
@apply leading-none;
@apply px-1;
@apply min-w-[1.25rem];
@apply min-h-[1.25rem];
@apply items-center;
@apply justify-center;
@apply border border-dividerDark;

View File

@@ -17,7 +17,6 @@
--lower-tertiary-sticky-fold: 7.125rem;
--lower-fourth-sticky-fold: 9.188rem;
--sidebar-primary-sticky-fold: 2rem;
--properties-primary-sticky-fold: 2.063rem;
}
@mixin light-theme {

View File

@@ -33,7 +33,6 @@
"open_workspace": "Open workspace",
"paste": "Paste",
"prettify": "Prettify",
"properties":"Properties",
"remove": "Remove",
"rename": "Rename",
"restore": "Restore",
@@ -139,11 +138,9 @@
"generate_token": "Generate Token",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Include in URL",
"inherited_from": "Inherited from {auth} from Parent Collection {collection} ",
"learn": "Learn how",
"pass_key_by": "Pass by",
"password": "Password",
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
"token": "Token",
"type": "Authorization Type",
"username": "Username",
@@ -175,8 +172,6 @@
"name_length_insufficient": "Collection name should be at least 3 characters long",
"new": "New Collection",
"order_changed": "Collection Order Updated",
"properties":"Collection Properties",
"properties_updated": "Collection Properties Updated",
"renamed": "Collection renamed",
"request_in_use": "Request in use",
"save_as": "Save as",
@@ -310,8 +305,7 @@
"proxy_error": "Proxy error",
"script_fail": "Could not execute pre-request script",
"something_went_wrong": "Something went wrong",
"test_script_fail": "Could not execute post-request script",
"authproviders_load_error": "Unable to load auth providers"
"test_script_fail": "Could not execute post-request script"
},
"export": {
"as_json": "Export as JSON",
@@ -360,8 +354,6 @@
"offline_short": "You're using Hoppscotch offline.",
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
"collection_properties_header": "This header will be set for every request in this collection.",
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
"test_script_fail": "There seems to be an error with test script. Please fix the errors and run tests again",
"tests": "Write a test script to automate debugging."
@@ -400,8 +392,7 @@
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment from a JSON file",
"insomnia_environment_description": "Import Insomnia Environment from a JSON/YAML file",
"postman_environment_description": "Import Postman Environment JSON file",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"

View File

@@ -56,7 +56,6 @@ declare module 'vue' {
CollectionsGraphqlRequest: typeof import('./components/collections/graphql/Request.vue')['default']
CollectionsImportExport: typeof import('./components/collections/ImportExport.vue')['default']
CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default']
CollectionsProperties: typeof import('./components/collections/Properties.vue')['default']
CollectionsRequest: typeof import('./components/collections/Request.vue')['default']
CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default']
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']

View File

@@ -1,7 +1,7 @@
<template>
<AppShortcuts :show="showShortcuts" @close="showShortcuts = false" />
<AppShare :show="showShare" @hide-modal="showShare = false" />
<FirebaseLogin v-if="showLogin" @hide-modal="showLogin = false" />
<FirebaseLogin :show="showLogin" @hide-modal="showLogin = false" />
</template>
<script setup lang="ts">

View File

@@ -13,7 +13,7 @@
</template>
<script setup lang="ts">
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { graphqlCollectionStore } from "~/newstore/collections"
@@ -28,7 +28,7 @@ const pathFolders = computed(() => {
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection[] = []
const pathItems: HoppCollection<HoppGQLRequest>[] = []
let currentFolder =
graphqlCollectionStore.value.state[folderIndicies.shift()!]

View File

@@ -20,7 +20,7 @@
</template>
<script setup lang="ts">
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed } from "vue"
import { restCollectionStore } from "~/newstore/collections"
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
@@ -36,7 +36,7 @@ const pathFolders = computed(() => {
.slice(0, -1)
.map((x) => parseInt(x))
const pathItems: HoppCollection[] = []
const pathItems: HoppCollection<HoppRESTRequest>[] = []
let currentFolder = restCollectionStore.value.state[folderIndicies.shift()!]
pathItems.push(currentFolder)

View File

@@ -96,7 +96,6 @@
@keyup.e="edit?.$el.click()"
@keyup.delete="deleteAction?.$el.click()"
@keyup.x="exportAction?.$el.click()"
@keyup.p="propertiesAction?.$el.click()"
@keyup.escape="hide()"
>
<HoppSmartItem
@@ -160,18 +159,6 @@
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties')
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -206,9 +193,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
import IconEdit from "~icons/lucide/edit"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import IconSettings2 from "~icons/lucide/settings-2"
import { ref, computed, watch } from "vue"
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { useI18n } from "@composables/i18n"
import { TippyComponent } from "vue-tippy"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
@@ -227,7 +213,7 @@ const props = withDefaults(
defineProps<{
id: string
parentID?: string | null
data: HoppCollection | TeamCollection
data: HoppCollection<HoppRESTRequest> | TeamCollection
/**
* Collection component can be used for both collections and folders.
* folderType is used to determine which one it is.
@@ -259,7 +245,6 @@ const emit = defineEmits<{
(event: "add-request"): void
(event: "add-folder"): void
(event: "edit-collection"): void
(event: "edit-properties"): void
(event: "export-data"): void
(event: "remove-collection"): void
(event: "drop-event", payload: DataTransfer): void
@@ -276,7 +261,6 @@ const edit = ref<HTMLButtonElement | null>(null)
const deleteAction = ref<HTMLButtonElement | null>(null)
const exportAction = ref<HTMLButtonElement | null>(null)
const options = ref<TippyComponent | null>(null)
const propertiesAction = ref<TippyComponent | null>(null)
const dragging = ref(false)
const ordering = ref(false)
@@ -310,8 +294,8 @@ const collectionIcon = computed(() => {
})
const collectionName = computed(() => {
if ((props.data as HoppCollection).name)
return (props.data as HoppCollection).name
if ((props.data as HoppCollection<HoppRESTRequest>).name)
return (props.data as HoppCollection<HoppRESTRequest>).name
return (props.data as TeamCollection).title
})

View File

@@ -29,6 +29,7 @@ import { PropType, computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { HoppRESTRequest } from "@hoppscotch/data"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
@@ -87,7 +88,9 @@ const showImportFailedError = () => {
toast.error(t("import.failed"))
}
const handleImportToStore = async (collections: HoppCollection[]) => {
const handleImportToStore = async (
collections: HoppCollection<HoppRESTRequest>[]
) => {
const importResult =
props.collectionsType.type === "my-collections"
? await importToPersonalWorkspace(collections)
@@ -101,47 +104,26 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
}
}
const importToPersonalWorkspace = (collections: HoppCollection[]) => {
const importToPersonalWorkspace = (
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections)
return E.right({
success: true,
})
}
function translateToTeamCollectionFormat(x: HoppCollection) {
const folders: HoppCollection[] = (x.folders ?? []).map(
translateToTeamCollectionFormat
)
const data = {
auth: x.auth,
headers: x.headers,
}
const obj = {
...x,
folders,
data,
}
if (x.id) obj.id = x.id
return obj
}
const importToTeamsWorkspace = async (collections: HoppCollection[]) => {
const importToTeamsWorkspace = async (
collections: HoppCollection<HoppRESTRequest>[]
) => {
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
return E.left({
success: false,
})
}
const transformedCollection = collections.map((collection) =>
translateToTeamCollectionFormat(collection)
)
const res = await toTeamsImporter(
JSON.stringify(transformedCollection),
JSON.stringify(collections),
selectedTeamID.value
)()
@@ -425,6 +407,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
},
action: async () => {
isHoppTeamCollectionExporterInProgress.value = true
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam

View File

@@ -71,13 +71,6 @@
collection: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'collections' &&
emit('export-data', node.data.data.data)
@@ -146,13 +139,6 @@
folder: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'folders' &&
emit('export-data', node.data.data.data)
@@ -358,7 +344,7 @@ export type Collection = {
isLastItem: boolean
data: {
parentIndex: null
data: HoppCollection
data: HoppCollection<HoppRESTRequest>
}
}
@@ -367,7 +353,7 @@ type Folder = {
isLastItem: boolean
data: {
parentIndex: string
data: HoppCollection
data: HoppCollection<HoppRESTRequest>
}
}
@@ -394,7 +380,7 @@ type CollectionType =
const props = defineProps({
filteredCollections: {
type: Array as PropType<HoppCollection[]>,
type: Array as PropType<HoppCollection<HoppRESTRequest>[]>,
default: () => [],
required: true,
},
@@ -426,35 +412,28 @@ const emit = defineEmits<{
event: "add-request",
payload: {
path: string
folder: HoppCollection
folder: HoppCollection<HoppRESTRequest>
}
): void
(
event: "add-folder",
payload: {
path: string
folder: HoppCollection
folder: HoppCollection<HoppRESTRequest>
}
): void
(
event: "edit-collection",
payload: {
collectionIndex: string
collection: HoppCollection
collection: HoppCollection<HoppRESTRequest>
}
): void
(
event: "edit-folder",
payload: {
folderPath: string
folder: HoppCollection
}
): void
(
event: "edit-properties",
payload: {
collectionIndex: string
collection: HoppCollection
folder: HoppCollection<HoppRESTRequest>
}
): void
(
@@ -472,7 +451,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
}
): void
(event: "export-data", payload: HoppCollection): void
(event: "export-data", payload: HoppCollection<HoppRESTRequest>): void
(event: "remove-collection", payload: string): void
(event: "remove-folder", payload: string): void
(
@@ -686,10 +665,10 @@ const updateCollectionOrder = (
type MyCollectionNode = Collection | Folder | Requests
class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
constructor(public data: Ref<HoppCollection[]>) {}
constructor(public data: Ref<HoppCollection<HoppRESTRequest>[]>) {}
navigateToFolderWithIndexPath(
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest>[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null

View File

@@ -1,164 +0,0 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="t('collection.properties')"
:full-width-body="true"
@close="hideModal"
>
<template #body>
<HoppSmartTabs
v-model="selectedOptionTab"
styles="sticky overflow-x-auto flex-shrink-0 bg-primary top-0 z-10 !-py-4"
render-inactive-tabs
>
<HoppSmartTab :id="'headers'" :label="`${t('tab.headers')}`">
<HttpHeaders
v-model="editableCollection"
:is-collection-property="true"
@change-tab="changeOptionTab"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
>
<icon-lucide-info class="svg-icons mr-2" />
{{ t("helpers.collection_properties_header") }}
</div>
</HoppSmartTab>
<HoppSmartTab
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="editableCollection.auth"
:is-collection-property="true"
:is-root-collection="editingProperties?.isRootCollection"
:inherited-properties="editingProperties?.inheritedProperties"
/>
<div
class="bg-bannerInfo px-4 py-2 flex items-center sticky bottom-0"
>
<icon-lucide-info class="svg-icons mr-2" />
{{ t("helpers.collection_properties_authorization") }}
</div>
</HoppSmartTab>
</HoppSmartTabs>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
:label="t('action.save')"
:loading="loadingState"
outline
@click="saveEditedCollection"
/>
<HoppButtonSecondary
:label="t('action.cancel')"
outline
filled
@click="hideModal"
/>
</span>
</template>
</HoppSmartModal>
</template>
<script setup lang="ts">
import { watch, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
import { RESTOptionTabs } from "../http/RequestOptions.vue"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
import { clone } from "lodash-es"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
type EditingProperties = {
collection: HoppCollection | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties: HoppInheritedProperty | undefined
}
const props = withDefaults(
defineProps<{
show: boolean
loadingState: boolean
editingProperties: EditingProperties | null
}>(),
{
show: false,
loadingState: false,
editingProperties: null,
}
)
const emit = defineEmits<{
(e: "set-collection-properties", newCollection: any): void
(e: "hide-modal"): void
}>()
const editableCollection = ref({
body: {
contentType: null,
body: null,
},
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}) as any
const selectedOptionTab = ref("headers")
const changeOptionTab = (tab: RESTOptionTabs) => {
selectedOptionTab.value = tab
}
watch(
() => props.show,
(show) => {
if (show && props.editingProperties?.collection) {
editableCollection.value.auth = clone(
props.editingProperties.collection.auth
)
editableCollection.value.headers = clone(
props.editingProperties.collection.headers
)
} else {
editableCollection.value = {
body: {
contentType: null,
body: null,
},
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
}
}
}
)
const saveEditedCollection = () => {
if (!props.editingProperties) return
const finalCollection = clone(editableCollection.value)
delete finalCollection.body
const collection = {
path: props.editingProperties.path,
collection: {
...props.editingProperties.collection,
...finalCollection,
},
isRootCollection: props.editingProperties.isRootCollection,
}
emit("set-collection-properties", collection)
}
const hideModal = () => {
emit("hide-modal")
}
</script>

View File

@@ -74,7 +74,6 @@ import { Picked } from "~/helpers/types/HoppPicked"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest,
editRESTRequest,
saveGraphqlRequestAs,
@@ -240,16 +239,6 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
@@ -277,16 +266,6 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: true,
@@ -315,16 +294,6 @@ const saveRequestAs = async () => {
},
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"rest"
)
RESTTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
createdNow: false,
@@ -409,16 +378,6 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-folder") {
// TODO: Check for GQL request ?
@@ -434,16 +393,6 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
picked.value.folderPath,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
} else if (picked.value.pickedType === "gql-my-collection") {
// TODO: Check for GQL request ?
@@ -459,16 +408,6 @@ const saveRequestAs = async () => {
workspaceType: "team",
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${picked.value.collectionIndex}`,
"graphql"
)
GQLTabs.currentActiveTab.value.document.inheritedProperties = {
auth,
headers,
}
requestSaved()
}
}

View File

@@ -88,13 +88,6 @@
collection: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'collections' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'collections' &&
emit('export-data', node.data.data.data)
@@ -166,13 +159,6 @@
folder: node.data.data.data,
})
"
@edit-properties="
node.data.type === 'folders' &&
emit('edit-properties', {
collectionIndex: node.id,
collection: node.data.data.data,
})
"
@export-data="
node.data.type === 'folders' &&
emit('export-data', node.data.data.data)
@@ -252,7 +238,6 @@
selectRequest({
request: node.data.data.data.request,
requestIndex: node.data.data.data.id,
folderPath: getPath(node.id),
})
"
@share-request="
@@ -467,13 +452,6 @@ const emit = defineEmits<{
folder: TeamCollection
}
): void
(
event: "edit-properties",
payload: {
collectionIndex: string
collection: TeamCollection
}
): void
(
event: "edit-request",
payload: {
@@ -504,7 +482,7 @@ const emit = defineEmits<{
request: HoppRESTRequest
requestIndex: string
isActive: boolean
folderPath: string
folderPath?: string | undefined
}
): void
(
@@ -552,12 +530,6 @@ const emit = defineEmits<{
(event: "display-modal-import-export"): void
}>()
const getPath = (path: string) => {
const pathArray = path.split("/")
pathArray.pop()
return pathArray.join("/")
}
const teamCollectionsList = toRef(props, "teamCollectionList")
const hasNoTeamAccess = computed(
@@ -614,7 +586,6 @@ const isActiveRequest = (requestID: string) => {
const selectRequest = (data: {
request: HoppRESTRequest
requestIndex: string
folderPath: string | null
}) => {
const { request, requestIndex } = data
if (props.saveRequest) {
@@ -627,7 +598,6 @@ const selectRequest = (data: {
request: request,
requestIndex: requestIndex,
isActive: isActiveRequest(requestIndex),
folderPath: data.folderPath,
})
}
}

View File

@@ -32,58 +32,58 @@
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref } from "vue"
<script lang="ts">
import { defineComponent } from "vue"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { makeCollection } from "@hoppscotch/data"
import { HoppGQLRequest, makeCollection } from "@hoppscotch/data"
import { addGraphqlCollection } from "~/newstore/collections"
import { platform } from "~/platform"
const t = useI18n()
const toast = useToast()
export default defineComponent({
props: {
show: Boolean,
},
emits: ["hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: null as string | null,
}
},
methods: {
addNewCollection() {
if (!this.name) {
this.toast.error(`${this.t("collection.invalid_name")}`)
return
}
defineProps<{
show: boolean
}>()
addGraphqlCollection(
makeCollection<HoppGQLRequest>({
name: this.name,
folders: [],
requests: [],
})
)
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
this.hideModal()
const name = ref<string | null>(null)
const addNewCollection = () => {
if (!name.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
addGraphqlCollection(
makeCollection({
name: name.value,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
)
hideModal()
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true,
platform: "gql",
workspaceType: "personal",
})
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: true,
platform: "gql",
workspaceType: "personal",
})
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -3,7 +3,7 @@
v-if="show"
dialog
:title="t('folder.new')"
@close="hideModal"
@close="$emit('hide-modal')"
>
<template #body>
<HoppSmartInput
@@ -32,49 +32,47 @@
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref } from "vue"
<script lang="ts">
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { defineComponent } from "vue"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
show: boolean
folderPath?: string
collectionIndex: number
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
(
e: "add-folder",
v: {
name: string
path: string | undefined
export default defineComponent({
props: {
show: Boolean,
folderPath: { type: String, default: null },
collectionIndex: { type: Number, default: null },
},
emits: ["hide-modal", "add-folder"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
): void
}>()
},
data() {
return {
name: null,
}
},
methods: {
addFolder() {
if (!this.name) {
this.toast.error(`${this.t("folder.name_length_insufficient")}`)
return
}
const name = ref<string | null>(null)
this.$emit("add-folder", {
name: this.name,
path: this.folderPath || `${this.collectionIndex}`,
})
const addFolder = () => {
if (!name.value) {
toast.error(`${t("folder.name_length_insufficient")}`)
return
}
emit("add-folder", {
name: name.value,
path: props.folderPath || `${props.collectionIndex}`,
})
hideModal()
}
const hideModal = () => {
name.value = null
emit("hide-modal")
}
this.hideModal()
},
hideModal() {
this.name = null
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -128,21 +128,6 @@
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties', {
collectionIndex: String(collectionIndex),
collection: collection,
})
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -170,15 +155,7 @@
@edit-folder="$emit('edit-folder', $event)"
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@edit-properties="
$emit('edit-properties', {
collectionIndex: `${collectionIndex}/${String(index)}`,
collection: folder,
})
"
@select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
@drop-request="$emit('drop-request', $event)"
/>
<CollectionsGraphqlRequest
v-for="(request, index) in collection.requests"
@@ -194,7 +171,6 @@
@edit-request="$emit('edit-request', $event)"
@duplicate-request="$emit('duplicate-request', $event)"
@select="$emit('select', $event)"
@select-request="$emit('select-request', $event)"
/>
<HoppSmartPlaceholder
v-if="
@@ -238,24 +214,25 @@ import IconFolderPlus from "~icons/lucide/folder-plus"
import IconMoreVertical from "~icons/lucide/more-vertical"
import IconEdit from "~icons/lucide/edit"
import IconTrash2 from "~icons/lucide/trash-2"
import IconSettings2 from "~icons/lucide/settings-2"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { removeGraphqlCollection } from "~/newstore/collections"
import {
removeGraphqlCollection,
moveGraphqlRequest,
} from "~/newstore/collections"
import { Picked } from "~/helpers/types/HoppPicked"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppCollection } from "@hoppscotch/data"
const props = defineProps<{
picked: Picked | null
const props = defineProps({
picked: { type: Object, default: null },
// Whether the viewing context is related to picking (activates 'select' events)
saveRequest: boolean
collectionIndex: number | null
collection: HoppCollection
isFiltered: boolean
}>()
saveRequest: { type: Boolean, default: false },
collectionIndex: { type: Number, default: null },
collection: { type: Object, default: () => ({}) },
isFiltered: Boolean,
})
const colorMode = useColorMode()
const toast = useToast()
@@ -271,23 +248,7 @@ const emit = defineEmits<{
(e: "add-request", i: any): void
(e: "add-folder", i: any): void
(e: "edit-folder", i: any): void
(
e: "edit-properties",
payload: {
collectionIndex: string | null
collection: HoppCollection
}
): void
(e: "edit-collection"): void
(e: "select-request", i: any): void
(
e: "drop-request",
payload: {
folderPath: string
requestIndex: string
collectionIndex: number | null
}
): void
}>()
// Template refs
@@ -363,10 +324,6 @@ const dropEvent = ({ dataTransfer }: any) => {
dragging.value = !dragging.value
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
emit("drop-request", {
folderPath,
requestIndex,
collectionIndex: props.collectionIndex,
})
moveGraphqlRequest(folderPath, requestIndex, `${props.collectionIndex}`)
}
</script>

View File

@@ -37,14 +37,13 @@ import { ref, watch } from "vue"
import { editGraphqlCollection } from "~/newstore/collections"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { HoppCollection } from "@hoppscotch/data"
const props = defineProps<{
show: boolean
editingCollectionIndex: number | null
editingCollection: HoppCollection | null
editingCollectionName: string
}>()
const props = defineProps({
show: Boolean,
editingCollection: { type: Object, default: () => ({}) },
editingCollectionIndex: { type: Number, default: null },
editingCollectionName: { type: String, default: null },
})
const emit = defineEmits<{
(e: "hide-modal"): void

View File

@@ -32,47 +32,52 @@
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
<script lang="ts">
import { defineComponent } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { editGraphqlFolder } from "~/newstore/collections"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
show: boolean
folderPath?: string
folder: any
editingFolderName: string
}>()
const emit = defineEmits(["hide-modal"])
const name = ref("")
watch(
() => props.editingFolderName,
(val) => {
name.value = val
}
)
const editFolder = () => {
if (!name.value) {
toast.error(`${t("collection.invalid_name")}`)
return
}
editGraphqlFolder(props.folderPath, {
...(props.folder as any),
name: name.value,
})
hideModal()
}
const hideModal = () => {
name.value = ""
emit("hide-modal")
}
export default defineComponent({
props: {
show: Boolean,
folder: { type: Object, default: () => ({}) },
folderPath: { type: String, default: null },
editingFolderName: { type: String, default: null },
},
emits: ["hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
name: "",
}
},
watch: {
editingFolderName(val) {
this.name = val
},
},
methods: {
editFolder() {
if (!this.name) {
this.toast.error(`${this.t("collection.invalid_name")}`)
return
}
editGraphqlFolder(this.folderPath, {
...(this.folder as any),
name: this.name,
})
this.hideModal()
},
hideModal() {
this.name = ""
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -32,55 +32,61 @@
</HoppSmartModal>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
<script lang="ts">
import { defineComponent, PropType } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { HoppGQLRequest } from "@hoppscotch/data"
import { editGraphqlRequest } from "~/newstore/collections"
const t = useI18n()
const toast = useToast()
export default defineComponent({
props: {
show: Boolean,
folderPath: { type: String, default: null },
request: { type: Object as PropType<HoppGQLRequest>, default: () => ({}) },
requestIndex: { type: Number, default: null },
editingRequestName: { type: String, default: null },
},
emits: ["hide-modal"],
setup() {
return {
toast: useToast(),
t: useI18n(),
}
},
data() {
return {
requestUpdateData: {
name: null as any | null,
},
}
},
watch: {
editingRequestName(val) {
this.requestUpdateData.name = val
},
},
methods: {
saveRequest() {
if (!this.requestUpdateData.name) {
this.toast.error(`${this.t("collection.invalid_name")}`)
return
}
const props = defineProps<{
show: boolean
folderPath?: string
requestIndex: number | null
request: HoppGQLRequest | null
editingRequestName: string
}>()
// TODO: Type safety goes brrrr. Proper typing plz
const requestUpdated = {
...this.$props.request,
name: this.$data.requestUpdateData.name || this.$props.request.name,
}
const emit = defineEmits<{
(e: "hide-modal"): void
}>()
editGraphqlRequest(this.folderPath, this.requestIndex, requestUpdated)
const requestUpdateData = ref({ name: null as string | null })
watch(
() => props.editingRequestName,
(val) => {
requestUpdateData.value.name = val
}
)
const saveRequest = () => {
if (!requestUpdateData.value.name) {
toast.error(`${t("collection.invalid_name")}`)
return
}
const requestUpdated = {
...(props.request as any),
name: requestUpdateData.value.name || (props.request as any).name,
}
editGraphqlRequest(props.folderPath, props.requestIndex, requestUpdated)
hideModal()
}
const hideModal = () => {
requestUpdateData.value = { name: null }
emit("hide-modal")
}
this.hideModal()
},
hideModal() {
this.requestUpdateData = { name: null }
this.$emit("hide-modal")
},
},
})
</script>

View File

@@ -10,25 +10,24 @@
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<div
class="flex min-w-0 flex-1 items-center justify-center cursor-pointer"
<span
class="flex cursor-pointer items-center justify-center px-4"
@click="toggleShowChildren()"
>
<span class="pointer-events-none flex items-center justify-center px-4">
<component
:is="collectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
<component
:is="collectionIcon"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
@click="toggleShowChildren()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 cursor-pointer py-2 pr-2 transition group-hover:text-secondaryDark"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ folder.name ? folder.name : folder.title }}
</span>
</span>
</div>
</span>
<div class="flex">
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
@@ -121,21 +120,6 @@
}
"
/>
<HoppSmartItem
ref="propertiesAction"
:icon="IconSettings2"
:label="t('action.properties')"
:shortcut="['P']"
@click="
() => {
emit('edit-properties', {
collectionIndex: collectionIndex,
collection: collection,
})
hide()
}
"
/>
</div>
</template>
</tippy>
@@ -164,14 +148,7 @@
@edit-folder="emit('edit-folder', $event)"
@edit-request="emit('edit-request', $event)"
@duplicate-request="emit('duplicate-request', $event)"
@edit-properties="
emit('edit-properties', {
collectionIndex: `${folderPath}/${String(subFolderIndex)}`,
collection: subFolder,
})
"
@select="emit('select', $event)"
@select-request="$emit('select-request', $event)"
/>
<CollectionsGraphqlRequest
v-for="(request, index) in folder.requests"
@@ -187,7 +164,6 @@
@edit-request="emit('edit-request', $event)"
@duplicate-request="emit('duplicate-request', $event)"
@select="emit('select', $event)"
@select-request="$emit('select-request', $event)"
/>
<HoppSmartPlaceholder
@@ -221,16 +197,13 @@ import IconMoreVertical from "~icons/lucide/more-vertical"
import IconCheckCircle from "~icons/lucide/check-circle"
import IconFolder from "~icons/lucide/folder"
import IconFolderOpen from "~icons/lucide/folder-open"
import IconSettings2 from "~icons/lucide/settings-2"
import { useToast } from "@composables/toast"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { removeGraphqlFolder } from "~/newstore/collections"
import { removeGraphqlFolder, moveGraphqlRequest } from "~/newstore/collections"
import { computed, ref } from "vue"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { Picked } from "~/helpers/types/HoppPicked"
import { HoppCollection } from "@hoppscotch/data"
const toast = useToast()
const t = useI18n()
@@ -238,16 +211,16 @@ const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const props = defineProps<{
picked: Picked
const props = defineProps({
picked: { type: Object, default: null },
// Whether the request is in a selectable mode (activates 'select' event)
saveRequest: boolean
folder: HoppCollection
folderIndex: number
collectionIndex: number
folderPath: string
isFiltered: boolean
}>()
saveRequest: { type: Boolean, default: false },
folder: { type: Object, default: () => ({}) },
folderIndex: { type: Number, default: null },
collectionIndex: { type: Number, default: null },
folderPath: { type: String, default: null },
isFiltered: Boolean,
})
const emit = defineEmits([
"select",
@@ -256,9 +229,6 @@ const emit = defineEmits([
"add-folder",
"edit-folder",
"duplicate-request",
"edit-properties",
"select-request",
"drop-request",
])
// Template refs
@@ -333,11 +303,6 @@ const dropEvent = ({ dataTransfer }: any) => {
dragging.value = !dragging.value
const folderPath = dataTransfer.getData("folderPath")
const requestIndex = dataTransfer.getData("requestIndex")
emit("drop-request", {
folderPath,
requestIndex,
collectionIndex: props.folderPath,
})
moveGraphqlRequest(folderPath, requestIndex, props.folderPath)
}
</script>

View File

@@ -11,7 +11,7 @@
<script setup lang="ts">
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
@@ -25,14 +25,13 @@ import { useReadonlyStream } from "~/composables/stream"
import { platform } from "~/platform"
import {
appendGraphqlCollections,
graphqlCollections$,
setGraphqlCollections,
} from "~/newstore/collections"
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
import { gqlCollectionsGistExporter } from "~/helpers/import-export/export/gqlCollectionsGistExporter"
import { computed } from "vue"
import { hoppGQLImporter } from "~/helpers/import-export/import/hopp"
const t = useI18n()
const toast = useToast()
@@ -61,20 +60,15 @@ const GqlCollectionsHoppImporter: ImporterOrExporter = {
showImportFailedError()
return
}
const validatedCollection = await hoppGQLImporter(
JSON.stringify(res.right)
)()
if (E.isRight(validatedCollection)) {
handleImportToStore(validatedCollection.right)
handleImportToStore(res.right)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
platform: "gql",
workspaceType: "personal",
importer: "json",
})
}
platform.analytics?.logEvent({
type: "HOPP_IMPORT_COLLECTION",
platform: "gql",
workspaceType: "personal",
importer: "json",
})
emit("hide-modal")
},
@@ -220,9 +214,11 @@ const showImportFailedError = () => {
toast.error(t("import.failed"))
}
const handleImportToStore = async (gqlCollections: HoppCollection[]) => {
appendGraphqlCollections(gqlCollections)
toast.success(t("state.file_imported"))
const handleImportToStore = async (
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
setGraphqlCollections(gqlCollections)
toast.success(t("import.success"))
}
const emit = defineEmits<{

View File

@@ -9,41 +9,38 @@
@dragend="dragging = false"
@contextmenu.prevent="options.tippy.show()"
>
<div
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center"
<span
class="flex w-16 cursor-pointer items-center justify-center truncate px-2"
@click="selectRequest()"
>
<span
class="pointer-events-none flex w-8 items-center justify-center truncate px-6"
>
<component
:is="isSelected ? IconCheckCircle : IconFile"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
<component
:is="isSelected ? IconCheckCircle : IconFile"
class="svg-icons"
:class="{ 'text-accent': isSelected }"
/>
</span>
<span
class="flex min-w-0 flex-1 cursor-pointer items-center py-2 pr-2 transition group-hover:text-secondaryDark"
@click="selectRequest()"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 items-center py-2 pr-2 transition group-hover:text-secondaryDark"
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span class="truncate" :class="{ 'text-accent': isSelected }">
{{ request.name }}
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
v-if="isActive"
v-tippy="{ theme: 'tooltip' }"
class="relative mx-3 flex h-1.5 w-1.5 flex-shrink-0"
:title="`${t('collection.request_in_use')}`"
>
<span
class="absolute inline-flex h-full w-full flex-shrink-0 animate-ping rounded-full bg-green-500 opacity-75"
>
</span>
<span
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
class="relative inline-flex h-1.5 w-1.5 flex-shrink-0 rounded-full bg-green-500"
></span>
</span>
</div>
</span>
<div class="flex">
<span>
<tippy
@@ -137,7 +134,8 @@ import IconTrash2 from "~icons/lucide/trash-2"
import { PropType, computed, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { HoppGQLRequest } from "@hoppscotch/data"
import { HoppGQLRequest, makeGQLRequest } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import { removeGraphqlRequest } from "~/newstore/collections"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
@@ -177,12 +175,7 @@ const isActive = computed(() => {
})
// TODO: Better types please
const emit = defineEmits([
"select",
"edit-request",
"duplicate-request",
"select-request",
])
const emit = defineEmits(["select", "edit-request", "duplicate-request"])
const dragging = ref(false)
const confirmRemove = ref(false)
@@ -206,11 +199,36 @@ const selectRequest = () => {
if (props.saveRequest) {
pick()
} else {
emit("select-request", {
request: props.request,
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
})
// Switch to that request if that request is open
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
return
}
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: props.folderPath,
requestIndex: props.requestIndex,
},
request: cloneDeep(
makeGQLRequest({
name: props.request.name,
url: props.request.url,
query: props.request.query,
headers: props.request.headers,
variables: props.request.variables,
auth: props.request.auth,
})
),
isDirty: false,
})
}
}

View File

@@ -57,10 +57,7 @@
@edit-request="editRequest($event)"
@duplicate-request="duplicateRequest($event)"
@select-collection="$emit('use-collection', collection)"
@edit-properties="editProperties($event)"
@select="$emit('select', $event)"
@select-request="selectRequest($event)"
@drop-request="dropRequest($event)"
/>
</div>
<HoppSmartPlaceholder
@@ -145,27 +142,19 @@
v-if="showModalImportExport"
@hide-modal="displayModalImportExport(false)"
/>
<CollectionsProperties
:show="showModalEditProperties"
:editing-properties="editingProperties"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
</div>
</template>
<script setup lang="ts">
import { nextTick, ref } from "vue"
import { clone, cloneDeep } from "lodash-es"
<script lang="ts">
// TODO: TypeScript + Script Setup this :)
import { defineComponent } from "vue"
import { cloneDeep, clone } from "lodash-es"
import {
graphqlCollections$,
addGraphqlFolder,
saveGraphqlRequestAs,
cascadeParentCollectionForHeaderAuth,
editGraphqlCollection,
editGraphqlFolder,
moveGraphqlRequest,
} from "~/newstore/collections"
import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
@@ -175,448 +164,213 @@ import { useColorMode } from "@composables/theming"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { GQLTabService } from "~/services/tab/graphql"
import { computed } from "vue"
import {
HoppCollection,
HoppGQLRequest,
makeGQLRequest,
} from "@hoppscotch/data"
import { Picked } from "~/helpers/types/HoppPicked"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { updateInheritedPropertiesForAffectedRequests } from "~/helpers/collection/collection"
import { useToast } from "~/composables/toast"
import { getRequestsByPath } from "~/helpers/collection/request"
const t = useI18n()
const toast = useToast()
export default defineComponent({
props: {
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: { type: Boolean, default: false },
picked: { type: Object, default: null },
},
emits: ["select", "use-collection"],
setup() {
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const t = useI18n()
const tabs = useService(GQLTabService)
defineProps<{
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: boolean
picked: Picked
}>()
const collections = useReadonlyStream(graphqlCollections$, [], "deep")
const colorMode = useColorMode()
const tabs = useService(GQLTabService)
const showModalAdd = ref(false)
const showModalEdit = ref(false)
const showModalImportExport = ref(false)
const showModalAddRequest = ref(false)
const showModalAddFolder = ref(false)
const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false)
const showModalEditProperties = ref(false)
const editingCollection = ref<HoppCollection | null>(null)
const editingCollectionIndex = ref<number | null>(null)
const editingFolder = ref<HoppCollection | null>(null)
const editingFolderName = ref("")
const editingFolderIndex = ref<number | null>(null)
const editingFolderPath = ref("")
const editingRequest = ref<HoppGQLRequest | null>(null)
const editingRequestIndex = ref<number | null>(null)
const editingProperties = ref<{
collection: HoppCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
collection: null,
isRootCollection: false,
path: "",
inheritedProperties: undefined,
})
const filterText = ref("")
const filteredCollections = computed(() => {
const collectionsClone = clone(collections.value)
if (!filterText.value) return collectionsClone
const filterTextLower = filterText.value.toLowerCase()
const filteredCollections = []
for (const collection of collectionsClone) {
const filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterTextLower))
filteredRequests.push(request)
return {
collections,
colorMode,
t,
tabs,
IconPlus,
IconHelpCircle,
IconImport,
}
},
data() {
return {
showModalAdd: false,
showModalEdit: false,
showModalImportExport: false,
showModalAddRequest: false,
showModalAddFolder: false,
showModalEditFolder: false,
showModalEditRequest: false,
editingCollection: undefined,
editingCollectionIndex: undefined,
editingFolder: undefined,
editingFolderName: undefined,
editingFolderIndex: undefined,
editingFolderPath: undefined,
editingRequest: undefined,
editingRequestIndex: undefined,
filterText: "",
}
},
computed: {
filteredCollections() {
const collections = clone(this.collections)
for (const folder of collection.folders) {
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterTextLower))
filteredFolderRequests.push(request)
if (!this.filterText) return collections
const filterText = this.filterText.toLowerCase()
const filteredCollections = []
for (const collection of collections) {
const filteredRequests = []
const filteredFolders = []
for (const request of collection.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredRequests.push(request)
}
for (const folder of collection.folders) {
const filteredFolderRequests = []
for (const request of folder.requests) {
if (request.name.toLowerCase().includes(filterText))
filteredFolderRequests.push(request)
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = Object.assign({}, folder)
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
}
}
if (filteredRequests.length + filteredFolders.length > 0) {
const filteredCollection = Object.assign({}, collection)
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
if (filteredFolderRequests.length > 0) {
const filteredFolder = { ...folder }
filteredFolder.requests = filteredFolderRequests
filteredFolders.push(filteredFolder)
return filteredCollections
},
},
methods: {
displayModalAdd(shouldDisplay) {
this.showModalAdd = shouldDisplay
},
displayModalEdit(shouldDisplay) {
this.showModalEdit = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalImportExport(shouldDisplay) {
this.showModalImportExport = shouldDisplay
},
displayModalAddRequest(shouldDisplay) {
this.showModalAddRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalAddFolder(shouldDisplay) {
this.showModalAddFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditFolder(shouldDisplay) {
this.showModalEditFolder = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
displayModalEditRequest(shouldDisplay) {
this.showModalEditRequest = shouldDisplay
if (!shouldDisplay) this.resetSelectedData()
},
editCollection(collection, collectionIndex) {
this.$data.editingCollection = collection
this.$data.editingCollectionIndex = collectionIndex
this.displayModalEdit(true)
},
onAddRequest({ name, path, index }) {
const newRequest = {
...this.tabs.currentActiveTab.value.document.request,
name,
}
}
if (filteredRequests.length + filteredFolders.length > 0) {
const filteredCollection = { ...collection }
filteredCollection.requests = filteredRequests
filteredCollection.folders = filteredFolders
filteredCollections.push(filteredCollection)
}
}
saveGraphqlRequestAs(path, newRequest)
return filteredCollections
})
const displayModalAdd = (shouldDisplay: boolean) => {
showModalAdd.value = shouldDisplay
}
const displayModalEdit = (shouldDisplay: boolean) => {
showModalEdit.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalImportExport = (shouldDisplay: boolean) => {
showModalImportExport.value = shouldDisplay
}
const displayModalAddRequest = (shouldDisplay: boolean) => {
showModalAddRequest.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalAddFolder = (shouldDisplay: boolean) => {
showModalAddFolder.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditFolder = (shouldDisplay: boolean) => {
showModalEditFolder.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditRequest = (shouldDisplay: boolean) => {
showModalEditRequest.value = shouldDisplay
if (!shouldDisplay) resetSelectedData()
}
const displayModalEditProperties = (show: boolean) => {
showModalEditProperties.value = show
if (!show) resetSelectedData()
}
const editCollection = (
collection: HoppCollection,
collectionIndex: number
) => {
editingCollection.value = collection
editingCollectionIndex.value = collectionIndex
displayModalEdit(true)
}
const onAddRequest = ({
name,
path,
index,
}: {
name: string
path: string
index: number
}) => {
const newRequest = {
...tabs.currentActiveTab.value.document.request,
name,
}
saveGraphqlRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest,
isDirty: false,
inheritedProperties: {
auth,
headers,
},
})
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
displayModalAddRequest(false)
}
const addRequest = (payload: { path: string }) => {
const { path } = payload
editingFolderPath.value = path
displayModalAddRequest(true)
}
const onAddFolder = ({
name,
path,
}: {
name: string
path: string | undefined
}) => {
addGraphqlFolder(name, path ?? "0")
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
displayModalAddFolder(false)
}
const addFolder = (payload: { path: string }) => {
const { path } = payload
editingFolderPath.value = path
displayModalAddFolder(true)
}
const editFolder = (payload: {
folder: HoppCollection
folderPath: string
}) => {
const { folder, folderPath } = payload
editingFolder.value = folder
editingFolderPath.value = folderPath
displayModalEditFolder(true)
}
const editRequest = (payload: {
collectionIndex: number
folderIndex: number
folderName: string
request: HoppGQLRequest
requestIndex: number
folderPath: string
}) => {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
editingFolderPath.value = folderPath
editingCollectionIndex.value = collectionIndex
editingFolderIndex.value = folderIndex
editingFolderName.value = folderName
editingRequest.value = request
editingRequestIndex.value = requestIndex
displayModalEditRequest(true)
}
const duplicateRequest = ({
folderPath,
request,
}: {
folderPath: string
request: HoppGQLRequest
}) => {
saveGraphqlRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${t("action.duplicate")}`,
})
}
const selectRequest = ({
request,
folderPath,
requestIndex,
}: {
request: HoppGQLRequest
folderPath: string
requestIndex: number
}) => {
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
})
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"graphql"
)
// Switch to that request if that request is open
if (possibleTab) {
tabs.setActiveTab(possibleTab.value.id)
return
}
tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: folderPath,
requestIndex: requestIndex,
},
request: cloneDeep(
makeGQLRequest({
name: request.name,
url: request.url,
query: request.query,
headers: request.headers,
variables: request.variables,
auth: request.auth,
this.tabs.createNewTab({
saveContext: {
originLocation: "user-collection",
folderPath: path,
requestIndex: index,
},
request: newRequest,
isDirty: false,
})
),
isDirty: false,
inheritedProperties: {
auth,
headers,
platform.analytics?.logEvent({
type: "HOPP_SAVE_REQUEST",
platform: "gql",
createdNow: true,
workspaceType: "personal",
})
this.displayModalAddRequest(false)
},
})
}
addRequest(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddRequest(true)
},
onAddFolder({ name, path }) {
addGraphqlFolder(name, path)
const dropRequest = ({
folderPath,
requestIndex,
collectionIndex,
}: {
folderPath: string
requestIndex: number
collectionIndex: number
}) => {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${collectionIndex}`,
"graphql"
)
platform.analytics?.logEvent({
type: "HOPP_CREATE_COLLECTION",
isRootCollection: false,
platform: "gql",
workspaceType: "personal",
})
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex: Number(requestIndex),
})
if (possibleTab) {
possibleTab.value.document.saveContext = {
originLocation: "user-collection",
folderPath: `${collectionIndex}`,
requestIndex: getRequestsByPath(collections.value, `${collectionIndex}`)
.length,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
}
moveGraphqlRequest(folderPath, requestIndex, `${collectionIndex}`)
toast.success(`${t("request.moved")}`)
}
/**
* Checks if the collection is already in the root
* @param id - path of the collection
* @returns boolean - true if the collection is already in the root
*/
const isAlreadyInRoot = (id: string) => {
const indexPath = id.split("/")
return indexPath.length === 1
}
const editProperties = ({
collectionIndex,
collection,
}: {
collectionIndex: string | null
collection: HoppCollection | null
}) => {
if (collectionIndex === null || collection === null) return
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties = {}
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
parentIndex,
"graphql"
)
inheritedProperties = {
auth,
headers,
} as HoppInheritedProperty
}
editingProperties.value = {
collection,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
}
displayModalEditProperties(true)
}
const setCollectionProperties = (newCollection: {
collection: HoppCollection
path: string
isRootCollection: boolean
}) => {
const { collection, path, isRootCollection } = newCollection
if (isRootCollection) {
editGraphqlCollection(parseInt(path), collection)
} else {
editGraphqlFolder(path, collection)
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
path,
"graphql"
)
nextTick(() => {
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"graphql"
)
})
displayModalEditProperties(false)
}
const resetSelectedData = () => {
editingCollection.value = null
editingCollectionIndex.value = null
editingFolder.value = null
editingFolderIndex.value = null
editingRequest.value = null
editingRequestIndex.value = null
}
this.displayModalAddFolder(false)
},
addFolder(payload) {
const { path } = payload
this.$data.editingFolderPath = path
this.displayModalAddFolder(true)
},
editFolder(payload) {
const { folder, folderPath } = payload
this.editingFolder = folder
this.editingFolderPath = folderPath
this.displayModalEditFolder(true)
},
editRequest(payload) {
const {
collectionIndex,
folderIndex,
folderName,
request,
requestIndex,
folderPath,
} = payload
this.$data.editingFolderPath = folderPath
this.$data.editingCollectionIndex = collectionIndex
this.$data.editingFolderIndex = folderIndex
this.$data.editingFolderName = folderName
this.$data.editingRequest = request
this.$data.editingRequestIndex = requestIndex
this.displayModalEditRequest(true)
},
resetSelectedData() {
this.$data.editingCollection = undefined
this.$data.editingCollectionIndex = undefined
this.$data.editingFolder = undefined
this.$data.editingFolderIndex = undefined
this.$data.editingRequest = undefined
this.$data.editingRequestIndex = undefined
},
duplicateRequest({ folderPath, request }) {
saveGraphqlRequestAs(folderPath, {
...cloneDeep(request),
name: `${request.name} - ${this.t("action.duplicate")}`,
})
},
},
})
</script>

View File

@@ -38,7 +38,6 @@
@add-request="addRequest"
@edit-collection="editCollection"
@edit-folder="editFolder"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@@ -70,7 +69,6 @@
@add-folder="addFolder"
@edit-collection="editCollection"
@edit-folder="editFolder"
@edit-properties="editProperties"
@export-data="exportData"
@remove-collection="removeCollection"
@remove-folder="removeFolder"
@@ -153,12 +151,6 @@
:show="showTeamModalAdd"
@hide-modal="displayTeamModalAdd(false)"
/>
<CollectionsProperties
:show="showModalEditProperties"
:editing-properties="editingProperties"
@hide-modal="displayModalEditProperties(false)"
@set-collection-properties="setCollectionProperties"
/>
</div>
</template>
@@ -189,13 +181,10 @@ import {
moveRESTFolder,
navigateToFolderWithIndexPath,
restCollectionStore,
cascadeParentCollectionForHeaderAuth,
} from "~/newstore/collections"
import TeamCollectionAdapter from "~/helpers/teams/TeamCollectionAdapter"
import {
HoppCollection,
HoppRESTAuth,
HoppRESTHeaders,
HoppRESTRequest,
makeCollection,
} from "@hoppscotch/data"
@@ -204,10 +193,10 @@ import { GQLError } from "~/helpers/backend/GQLClient"
import {
createNewRootCollection,
createChildCollection,
renameCollection,
deleteCollection,
moveRESTTeamCollection,
updateOrderRESTTeamCollection,
updateTeamCollection,
} from "~/helpers/backend/mutations/TeamCollection"
import {
updateTeamRequest,
@@ -231,7 +220,6 @@ import {
getFoldersByPath,
resolveSaveContextOnCollectionReorder,
updateSaveContextForAffectedRequests,
updateInheritedPropertiesForAffectedRequests,
resetTeamRequestsContext,
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
@@ -239,7 +227,6 @@ import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
const toast = useToast()
@@ -279,11 +266,15 @@ const collectionsType = ref<CollectionType>({
})
// Collection Data
const editingCollection = ref<HoppCollection | TeamCollection | null>(null)
const editingCollection = ref<
HoppCollection<HoppRESTRequest> | TeamCollection | null
>(null)
const editingCollectionName = ref<string | null>(null)
const editingCollectionIndex = ref<number | null>(null)
const editingCollectionID = ref<string | null>(null)
const editingFolder = ref<HoppCollection | TeamCollection | null>(null)
const editingFolder = ref<
HoppCollection<HoppRESTRequest> | TeamCollection | null
>(null)
const editingFolderName = ref<string | null>(null)
const editingFolderPath = ref<string | null>(null)
const editingRequest = ref<HoppRESTRequest | null>(null)
@@ -291,18 +282,6 @@ const editingRequestName = ref("")
const editingRequestIndex = ref<number | null>(null)
const editingRequestID = ref<string | null>(null)
const editingProperties = ref<{
collection: Omit<HoppCollection, "v"> | TeamCollection | null
isRootCollection: boolean
path: string
inheritedProperties?: HoppInheritedProperty
}>({
collection: null,
isRootCollection: false,
path: "",
inheritedProperties: undefined,
})
const confirmModalTitle = ref<string | null>(null)
const filterTexts = ref("")
@@ -541,7 +520,6 @@ const showModalEditCollection = ref(false)
const showModalEditFolder = ref(false)
const showModalEditRequest = ref(false)
const showModalImportExport = ref(false)
const showModalEditProperties = ref(false)
const showConfirmModal = ref(false)
const showTeamModalAdd = ref(false)
@@ -587,12 +565,6 @@ const displayModalImportExport = (show: boolean) => {
if (!show) resetSelectedData()
}
const displayModalEditProperties = (show: boolean) => {
showModalEditProperties.value = show
if (!show) resetSelectedData()
}
const displayConfirmModal = (show: boolean) => {
showConfirmModal.value = show
@@ -612,11 +584,6 @@ const addNewRootCollection = (name: string) => {
name,
folders: [],
requests: [],
headers: [],
auth: {
authType: "inherit",
authActive: false,
},
})
)
@@ -658,7 +625,7 @@ const addNewRootCollection = (name: string) => {
const addRequest = (payload: {
path: string
folder: HoppCollection | TeamCollection
folder: HoppCollection<HoppRESTRequest> | TeamCollection
}) => {
const { path, folder } = payload
editingFolder.value = folder
@@ -672,13 +639,11 @@ const onAddRequest = (requestName: string) => {
name: requestName,
}
const path = editingFolderPath.value
if (!path) return
if (collectionsType.value.type === "my-collections") {
const path = editingFolderPath.value
if (!path) return
const insertionIndex = saveRESTRequestAs(path, newRequest)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
tabs.createNewTab({
request: newRequest,
isDirty: false,
@@ -687,10 +652,6 @@ const onAddRequest = (requestName: string) => {
folderPath: path,
requestIndex: insertionIndex,
},
inheritedProperties: {
auth,
headers,
},
})
platform.analytics?.logEvent({
@@ -731,8 +692,7 @@ const onAddRequest = (requestName: string) => {
},
(result) => {
const { createRequestInCollection } = result
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
tabs.createNewTab({
request: newRequest,
isDirty: false,
@@ -742,10 +702,6 @@ const onAddRequest = (requestName: string) => {
collectionID: createRequestInCollection.collection.id,
teamID: createRequestInCollection.collection.team.id,
},
inheritedProperties: {
auth,
headers,
},
})
modalLoadingState.value = false
@@ -758,7 +714,7 @@ const onAddRequest = (requestName: string) => {
const addFolder = (payload: {
path: string
folder: HoppCollection | TeamCollection
folder: HoppCollection<HoppRESTRequest> | TeamCollection
}) => {
const { path, folder } = payload
editingFolder.value = folder
@@ -817,13 +773,15 @@ const onAddFolder = (folderName: string) => {
const editCollection = (payload: {
collectionIndex: string
collection: HoppCollection | TeamCollection
collection: HoppCollection<HoppRESTRequest> | TeamCollection
}) => {
const { collectionIndex, collection } = payload
editingCollection.value = collection
if (collectionsType.value.type === "my-collections") {
editingCollectionIndex.value = parseInt(collectionIndex)
editingCollectionName.value = (collection as HoppCollection).name
editingCollectionName.value = (
collection as HoppCollection<HoppRESTRequest>
).name
} else {
editingCollectionName.value = (collection as TeamCollection).title
}
@@ -858,7 +816,7 @@ const updateEditingCollection = (newName: string) => {
modalLoadingState.value = true
pipe(
updateTeamCollection(editingCollection.value.id, undefined, newName),
renameCollection(editingCollection.value.id, newName),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
@@ -876,13 +834,13 @@ const updateEditingCollection = (newName: string) => {
const editFolder = (payload: {
folderPath: string | undefined
folder: HoppCollection | TeamCollection
folder: HoppCollection<HoppRESTRequest> | TeamCollection
}) => {
const { folderPath, folder } = payload
editingFolder.value = folder
if (collectionsType.value.type === "my-collections" && folderPath) {
editingFolderPath.value = folderPath
editingFolderName.value = (folder as HoppCollection).name
editingFolderName.value = (folder as HoppCollection<HoppRESTRequest>).name
} else {
editingFolderName.value = (folder as TeamCollection).title
}
@@ -896,7 +854,7 @@ const updateEditingFolder = (newName: string) => {
if (!editingFolderPath.value) return
editRESTFolder(editingFolderPath.value, {
...(editingFolder.value as HoppCollection),
...(editingFolder.value as HoppCollection<HoppRESTRequest>),
name: newName,
})
displayModalEditFolder(false)
@@ -907,7 +865,7 @@ const updateEditingFolder = (newName: string) => {
/* renameCollection can be used to rename both collections and folders
since folder is treated as collection in the BE. */
pipe(
updateTeamCollection(editingFolder.value.id, undefined, newName),
renameCollection(editingFolder.value.id, newName),
TE.match(
(err: GQLError<string>) => {
if (err.error === "team_coll/short_title") {
@@ -1321,18 +1279,16 @@ const selectPicked = (payload: Picked | null) => {
*/
const selectRequest = (selectedRequest: {
request: HoppRESTRequest
folderPath: string
folderPath: string | undefined
requestIndex: string
isActive: boolean
}) => {
const { request, folderPath, requestIndex } = selectedRequest
// If there is a request with this save context, switch into it
let possibleTab = null
if (collectionsType.value.type === "team-collections") {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(folderPath)
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
@@ -1346,19 +1302,10 @@ const selectRequest = (selectedRequest: {
saveContext: {
originLocation: "team-collection",
requestID: requestIndex,
collectionID: folderPath,
},
inheritedProperties: {
auth,
headers,
},
})
}
} else {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath,
"rest"
)
possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
requestIndex: parseInt(requestIndex),
@@ -1376,10 +1323,6 @@ const selectRequest = (selectedRequest: {
folderPath: folderPath!,
requestIndex: parseInt(requestIndex),
},
inheritedProperties: {
auth,
headers,
},
})
}
}
@@ -1406,17 +1349,16 @@ const dropRequest = (payload: {
}) => {
const { folderPath, requestIndex, destinationCollectionIndex } = payload
if (!requestIndex || !destinationCollectionIndex || !folderPath) return
if (!requestIndex || !destinationCollectionIndex) return
let possibleTab = null
if (collectionsType.value.type === "my-collections") {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex,
"rest"
if (collectionsType.value.type === "my-collections" && folderPath) {
moveRESTRequest(
folderPath,
pathToLastIndex(requestIndex),
destinationCollectionIndex
)
possibleTab = tabs.getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "user-collection",
folderPath,
requestIndex: pathToLastIndex(requestIndex),
@@ -1432,11 +1374,6 @@ const dropRequest = (payload: {
destinationCollectionIndex
).length,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
}
// When it's drop it's basically getting deleted from last folder. reordering last folder accordingly
@@ -1446,11 +1383,6 @@ const dropRequest = (payload: {
folderPath,
length: getRequestsByPath(myCollections.value, folderPath).length,
})
moveRESTRequest(
folderPath,
pathToLastIndex(requestIndex),
destinationCollectionIndex
)
toast.success(`${t("request.moved")}`)
draggingToRoot.value = false
@@ -1474,12 +1406,8 @@ const dropRequest = (payload: {
requestMoveLoading.value.indexOf(requestIndex),
1
)
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex
)
possibleTab = tabs.getTabRefWithSaveContext({
const possibleTab = tabs.getTabRefWithSaveContext({
originLocation: "team-collection",
requestID: requestIndex,
})
@@ -1489,10 +1417,6 @@ const dropRequest = (payload: {
originLocation: "team-collection",
requestID: requestIndex,
}
possibleTab.value.document.inheritedProperties = {
auth,
headers,
}
}
toast.success(`${t("request.moved")}`)
}
@@ -1613,22 +1537,6 @@ const dropCollection = (payload: {
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`
)
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
"rest"
)
const inheritedProperty = {
auth,
headers,
}
updateInheritedPropertiesForAffectedRequests(
`${destinationCollectionIndex}/${totalFoldersOfDestinationCollection}`,
inheritedProperty,
"rest"
)
draggingToRoot.value = false
toast.success(`${t("collection.moved")}`)
} else if (hasTeamWriteAccess.value) {
@@ -1654,22 +1562,6 @@ const dropCollection = (payload: {
collectionMoveLoading.value.indexOf(collectionIndexDragged),
1
)
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(
destinationCollectionIndex
)
const inheritedProperty = {
auth,
headers,
}
updateInheritedPropertiesForAffectedRequests(
`${destinationCollectionIndex}`,
inheritedProperty,
"rest"
)
}
)
)()
@@ -1954,11 +1846,13 @@ const initializeDownloadCollection = async (
* Triggered by the export button in the tippy menu
* @param collection - Collection or folder to be exported
*/
const exportData = async (collection: HoppCollection | TeamCollection) => {
const exportData = async (
collection: HoppCollection<HoppRESTRequest> | TeamCollection
) => {
if (collectionsType.value.type === "my-collections") {
const collectionJSON = JSON.stringify(collection)
const name = (collection as HoppCollection).name
const name = (collection as HoppCollection<HoppRESTRequest>).name
initializeDownloadCollection(collectionJSON, name)
} else {
@@ -1999,164 +1893,6 @@ const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
}
}
const editProperties = (payload: {
collectionIndex: string
collection: HoppCollection | TeamCollection
}) => {
const { collection, collectionIndex } = payload
if (collectionsType.value.type === "my-collections") {
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
let inheritedProperties = {
auth: {
parentID: "",
parentName: "",
inheritedAuth: {
authType: "inherit",
authActive: true,
},
},
headers: [
{
parentID: "",
parentName: "",
inheritedHeaders: [],
},
],
} as HoppInheritedProperty
if (parentIndex) {
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
parentIndex,
"rest"
)
inheritedProperties = {
auth,
headers,
}
}
editingProperties.value = {
collection,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
}
} else if (hasTeamWriteAccess.value) {
const parentIndex = collectionIndex.split("/").slice(0, -1).join("/") // remove last folder to get parent folder
const data = (collection as TeamCollection).data
? JSON.parse((collection as TeamCollection).data ?? "")
: null
let inheritedProperties = undefined
let coll = {
id: collection.id,
name: (collection as TeamCollection).title,
auth: {
authType: "inherit",
authActive: true,
} as HoppRESTAuth,
headers: [] as HoppRESTHeaders,
folders: null,
requests: null,
}
if (parentIndex) {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(parentIndex)
inheritedProperties = {
auth,
headers,
}
}
if (data) {
coll = {
...coll,
auth: data.auth,
headers: data.headers as HoppRESTHeaders,
}
}
editingProperties.value = {
collection: coll,
isRootCollection: isAlreadyInRoot(collectionIndex),
path: collectionIndex,
inheritedProperties,
}
}
displayModalEditProperties(true)
}
const setCollectionProperties = (newCollection: {
collection: HoppCollection
path: string
isRootCollection: boolean
}) => {
const { collection, path, isRootCollection } = newCollection
if (collectionsType.value.type === "my-collections") {
if (isRootCollection) {
editRESTCollection(parseInt(path), collection)
} else {
editRESTFolder(path, collection)
}
const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest")
nextTick(() => {
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"rest"
)
})
toast.success(t("collection.properties_updated"))
} else if (hasTeamWriteAccess.value && collection.id) {
const data = {
auth: collection.auth,
headers: collection.headers,
}
pipe(
updateTeamCollection(collection.id, JSON.stringify(data), undefined),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
},
() => {
toast.success(t("collection.properties_updated"))
}
)
)()
//This is a hack to update the inherited properties of the requests if there an tab opened
// since it takes a little bit of time to update the collection tree
setTimeout(() => {
const { auth, headers } =
teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path)
updateInheritedPropertiesForAffectedRequests(
path,
{
auth,
headers,
},
"rest",
"team"
)
}, 200)
}
displayModalEditProperties(false)
}
const resolveConfirmModal = (title: string | null) => {
if (title === `${t("confirm.remove_collection")}`) onRemoveCollection()
else if (title === `${t("confirm.remove_request")}`) onRemoveRequest()

View File

@@ -9,8 +9,6 @@
<template #body>
<HoppSmartPlaceholder
v-if="!currentInterceptorSupportsCookies"
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('cookies.modal.interceptor_no_support')}`"
:text="t('cookies.modal.interceptor_no_support')"
>
<template #body>

View File

@@ -18,22 +18,16 @@ import { GistSource } from "~/helpers/import-export/import/import-sources/GistSo
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
import * as E from "fp-ts/Either"
import {
appendEnvironments,
addGlobalEnvVariable,
environments$,
} from "~/newstore/environments"
import { appendEnvironments, environments$ } from "~/newstore/environments"
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
import { GQLError } from "~/helpers/backend/GQLClient"
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
import { insomniaEnvImporter } from "~/helpers/import-export/import/insomniaEnv"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconPostman from "~icons/hopp/postman"
import IconInsomnia from "~icons/hopp/insomnia"
import IconUser from "~icons/lucide/user"
import { initializeDownloadCollection } from "~/helpers/import-export/export"
import { computed } from "vue"
@@ -142,51 +136,6 @@ const PostmanEnvironmentsImport: ImporterOrExporter = {
}),
}
const insomniaEnvironmentsImport: ImporterOrExporter = {
metadata: {
id: "import.from_insomnia",
name: "import.from_insomnia",
icon: IconInsomnia,
title: "import.from_json",
applicableTo: ["personal-workspace", "team-workspace"],
disabled: false,
},
component: FileSource({
acceptedFileTypes: "application/json",
caption: "import.insomnia_environment_description",
onImportFromFile: async (environments) => {
const res = await insomniaEnvImporter(environments)()
if (E.isLeft(res)) {
showImportFailedError()
return
}
const globalEnvIndex = res.right.findIndex(
(env) => env.name === "Base Environment"
)
const globalEnv =
globalEnvIndex !== -1 ? res.right[globalEnvIndex] : undefined
// remove the global env from the environments array to prevent it from being imported twice
if (globalEnvIndex !== -1) {
res.right.splice(globalEnvIndex, 1)
}
handleImportToStore(res.right, globalEnv)
platform.analytics?.logEvent({
type: "HOPP_IMPORT_ENVIRONMENT",
platform: "rest",
workspaceType: isTeamEnvironment.value ? "team" : "personal",
})
emit("hide-modal")
},
}),
}
const EnvironmentsImportFromGIST: ImporterOrExporter = {
metadata: {
id: "import.environments_from_gist",
@@ -306,7 +255,6 @@ const importerModules = [
HoppEnvironmentsImport,
EnvironmentsImportFromGIST,
PostmanEnvironmentsImport,
insomniaEnvironmentsImport,
]
const exporterModules = computed(() => {
@@ -323,17 +271,7 @@ const showImportFailedError = () => {
toast.error(t("import.failed").toString())
}
const handleImportToStore = async (
environments: Environment[],
globalEnv?: Environment
) => {
// if there's a global env, add them to the store
if (globalEnv) {
globalEnv.variables.forEach(({ key, value }) => {
addGlobalEnvVariable({ key, value })
})
}
const handleImportToStore = async (environments: Environment[]) => {
if (props.environmentType === "MY_ENV") {
appendEnvironments(environments)
toast.success(t("state.file_imported"))

View File

@@ -1,71 +1,64 @@
<template>
<HoppSmartModal
v-if="show"
dialog
:title="`${t('auth.login_to_hoppscotch')}`"
styles="sm:max-w-md"
@close="hideModal"
>
<template #body>
<template v-if="isLoadingAllowedAuthProviders">
<div class="flex justify-center">
<HoppSmartSpinner />
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem
v-for="provider in allowedAuthProviders"
:key="provider.id"
:loading="provider.isLoading.value"
:icon="provider.icon"
:label="provider.label"
@click="provider.action"
/>
<hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem
v-for="loginItem in additonalLoginItems"
:key="loginItem.id"
:icon="loginItem.icon"
:label="loginItem.text(t)"
@click="doAdditionalLoginItemClickAction(loginItem)"
/>
</div>
<form
v-if="mode === 'email'"
class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail"
>
<HoppSmartInput
v-model="form.email"
type="email"
placeholder=" "
:label="t('auth.email')"
input-styles="floating-input"
/>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"
:label="`${t('auth.send_magic_link')}`"
/>
</form>
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
<div class="flex max-w-md flex-col items-center justify-center">
<icon-lucide-inbox class="h-6 w-6 text-accent" />
<h3 class="my-2 text-center text-lg">
{{ t("auth.we_sent_magic_link") }}
</h3>
<p class="text-center">
{{
t("auth.we_sent_magic_link_description", { email: form.email })
}}
</p>
</div>
</template>
<template v-else>
<div v-if="mode === 'sign-in'" class="flex flex-col space-y-2">
<HoppSmartItem
v-for="provider in allowedAuthProviders"
:key="provider.id"
:loading="provider.isLoading.value"
:icon="provider.icon"
:label="provider.label"
@click="provider.action"
/>
<hr v-if="additonalLoginItems.length > 0" />
<HoppSmartItem
v-for="loginItem in additonalLoginItems"
:key="loginItem.id"
:icon="loginItem.icon"
:label="loginItem.text(t)"
@click="doAdditionalLoginItemClickAction(loginItem)"
/>
</div>
<form
v-if="mode === 'email'"
class="flex flex-col space-y-2"
@submit.prevent="signInWithEmail"
>
<HoppSmartInput
v-model="form.email"
type="email"
placeholder=" "
:label="t('auth.email')"
input-styles="floating-input"
/>
<HoppButtonPrimary
:loading="signingInWithEmail"
type="submit"
:label="`${t('auth.send_magic_link')}`"
/>
</form>
<div v-if="mode === 'email-sent'" class="flex flex-col px-4">
<div class="flex max-w-md flex-col items-center justify-center">
<icon-lucide-inbox class="h-6 w-6 text-accent" />
<h3 class="my-2 text-center text-lg">
{{ t("auth.we_sent_magic_link") }}
</h3>
<p class="text-center">
{{
t("auth.we_sent_magic_link_description", { email: form.email })
}}
</p>
</div>
</div>
</template>
</div>
</template>
<template #footer>
<div
@@ -116,7 +109,7 @@
</template>
<script setup lang="ts">
import { Ref, onMounted, ref } from "vue"
import { Ref, computed, onMounted, ref } from "vue"
import { useI18n } from "@composables/i18n"
import { useStreamSubscriber } from "@composables/stream"
@@ -134,7 +127,9 @@ import { useService } from "dioc/vue"
import { LoginItemDef } from "~/platform/auth"
import { PersistenceService } from "~/services/persistence"
import * as E from "fp-ts/Either"
defineProps<{
show: boolean
}>()
const emit = defineEmits<{
(e: "hide-modal"): void
@@ -150,8 +145,6 @@ const form = {
email: "",
}
const isLoadingAllowedAuthProviders = ref(true)
const signingInWithGoogle = ref(false)
const signingInWithGitHub = ref(false)
const signingInWithMicrosoft = ref(false)
@@ -169,42 +162,21 @@ type AuthProviderItem = {
isLoading: Ref<boolean>
}
let allowedAuthProviders: AuthProviderItem[] = []
let additonalLoginItems: LoginItemDef[] = []
const additonalLoginItems = computed(
() => platform.auth.additionalLoginItems ?? []
)
const doAdditionalLoginItemClickAction = async (item: LoginItemDef) => {
await item.onClick()
emit("hide-modal")
}
onMounted(async () => {
onMounted(() => {
const currentUser$ = platform.auth.getCurrentUserStream()
subscribeToStream(currentUser$, (user) => {
if (user) hideModal()
})
const res = await platform.auth.getAllowedAuthProviders()
if (E.isLeft(res)) {
toast.error(`${t("error.authproviders_load_error")}`)
isLoadingAllowedAuthProviders.value = false
return
}
// setup the normal auth providers
const enabledAuthProviders = authProvidersAvailable.filter((provider) =>
res.right.includes(provider.id)
)
allowedAuthProviders = enabledAuthProviders
// setup the additional login items
additonalLoginItems =
platform.auth.additionalLoginItems?.filter((item) =>
res.right.includes(item.id)
) ?? []
isLoadingAllowedAuthProviders.value = false
})
const showLoginSuccess = () => {
@@ -303,7 +275,14 @@ const signInWithEmail = async () => {
})
}
const authProvidersAvailable: AuthProviderItem[] = [
const hideModal = () => {
mode.value = "sign-in"
toast.clear()
emit("hide-modal")
}
const authProviders: AuthProviderItem[] = [
{
id: "GITHUB",
icon: IconGithub,
@@ -336,10 +315,19 @@ const authProvidersAvailable: AuthProviderItem[] = [
},
]
const hideModal = () => {
mode.value = "sign-in"
toast.clear()
// Do not format the `import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS` call into multiple lines!
// prettier-ignore
const allowedAuthProvidersIDsString =
import.meta.env.VITE_ALLOWED_AUTH_PROVIDERS
emit("hide-modal")
}
const allowedAuthProvidersIDs = allowedAuthProvidersIDsString
? allowedAuthProvidersIDsString.split(",")
: []
const allowedAuthProviders =
allowedAuthProvidersIDs.length > 0
? authProviders.filter((provider) =>
allowedAuthProvidersIDs.includes(provider.id)
)
: authProviders
</script>

View File

@@ -1,12 +1,7 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
:class="[
isCollectionProperty
? 'top-propertiesPrimaryStickyFold'
: 'top-sidebarPrimaryStickyFold',
]"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<span class="flex items-center">
<label class="truncate font-semibold text-secondaryLight">
@@ -42,18 +37,6 @@
}
"
/>
<HoppSmartItem
v-if="!isRootCollection"
label="Inherit"
:icon="authName === 'Inherit' ? IconCircleDot : IconCircle"
:active="authName === 'Inherit'"
@click="
() => {
auth.authType = 'inherit'
hide()
}
"
/>
<HoppSmartItem
label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -166,17 +149,6 @@
/>
</div>
</div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
Inherited
{{ getAuthName(inheritedProperties.auth.inheritedAuth.authType) }}
from Parent Collection {{ inheritedProperties?.auth.parentName }}
</span>
<span v-else>
Please save this request in any collection to inherit the
authorization
</span>
</div>
<div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput
@@ -231,8 +203,6 @@ import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
import { onMounted } from "vue"
const t = useI18n()
@@ -240,24 +210,12 @@ const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppGQLAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLAuth): void
}>()
onMounted(() => {
if (props.isRootCollection && auth.value.authType === "inherit") {
auth.value = {
authType: "none",
authActive: true,
}
}
})
const auth = useVModel(props, "modelValue", emit)
const AUTH_KEY_NAME = {
@@ -266,20 +224,12 @@ const AUTH_KEY_NAME = {
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
inherit: "Inherit",
} as const
const authType = pluckRef(auth, "authType")
const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
)
const getAuthName = (type: HoppGQLAuth["authType"] | undefined) => {
if (!type) return "None"
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -1,11 +1,6 @@
<template>
<div
class="sticky z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
:class="[
isCollectionProperty
? 'top-propertiesPrimaryStickyFold'
: 'top-sidebarPrimaryStickyFold',
]"
class="sticky top-sidebarPrimaryStickyFold z-10 flex items-center justify-between border-y border-dividerLight bg-primary pl-4"
>
<label class="font-semibold text-secondaryLight">
{{ t("tab.headers") }}
@@ -82,11 +77,22 @@
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.key"
<HoppSmartAutoComplete
:placeholder="`${t('count.header', { count: index + 1 })}`"
:auto-complete-source="commonHeaders"
@change="
:source="commonHeaders"
:spellcheck="false"
:value="header.key"
autofocus
styles="
bg-transparent
flex
flex-1
py-1
px-4
truncate
"
class="!flex flex-1"
@input="
updateHeader(index, {
id: header.id,
key: $event,
@@ -95,14 +101,17 @@
})
"
/>
<SmartEnvInput
v-model="header.value"
<input
class="flex flex-1 bg-transparent px-4 py-2"
:placeholder="`${t('count.value', { count: index + 1 })}`"
:name="`value ${String(index)}`"
:value="header.value"
autofocus
@change="
updateHeader(index, {
id: header.id,
key: header.key,
value: $event,
value: ($event!.target! as HTMLInputElement).value,
active: header.active,
})
"
@@ -147,119 +156,6 @@
</div>
</template>
</draggable>
<draggable
v-model="computedHeaders"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="mask(header)"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<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')"
/>
</span>
</div>
</template>
</draggable>
<draggable
v-model="inheritedProperties"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="
header.source === 'auth' ? mask(header) : header.header.value
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking && header.source === 'auth' ? IconEye : IconEyeOff"
@click="toggleMask()"
/>
<span v-else class="aspect-square w-[2.05rem]"></span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconInfo"
:title="`This header is inherited from Parent Collection ${
header.inheritedFrom ?? ''
}`"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
@@ -288,12 +184,7 @@ import IconCheckCircle from "~icons/lucide/check-circle"
import IconTrash from "~icons/lucide/trash"
import IconCircle from "~icons/lucide/circle"
import IconWrapText from "~icons/lucide/wrap-text"
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconLock from "~icons/lucide/lock"
import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconInfo from "~icons/lucide/info"
import { computed, reactive, ref, watch } from "vue"
import { reactive, ref, watch } from "vue"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
@@ -315,20 +206,13 @@ import { commonHeaders } from "~/helpers/headers"
import { useCodemirror } from "@composables/codemirror"
import { objRemoveKey } from "~/helpers/functional/object"
import { useVModel } from "@vueuse/core"
import { HoppGQLHeader } from "~/helpers/graphql"
import { throwError } from "~/helpers/functional/error"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const colorMode = useColorMode()
const t = useI18n()
const toast = useToast()
// v-model integration with props and emit
const props = defineProps<{
modelValue: HoppGQLRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const props = defineProps<{ modelValue: HoppGQLRequest }>()
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void
@@ -529,11 +413,7 @@ const deleteHeader = (index: number) => {
})
}
workingHeaders.value = pipe(
workingHeaders.value,
A.deleteAt(index),
O.getOrElseW(() => throwError("Working Headers Deletion Out of Bounds"))
)
workingHeaders.value.splice(index, 1)
}
const clearContent = () => {
@@ -549,151 +429,4 @@ const clearContent = () => {
bulkHeaders.value = ""
}
const getComputedAuthHeaders = (
req?: HoppGQLRequest,
auth?: HoppGQLRequest["auth"]
) => {
const request = auth ? { auth: auth ?? { authActive: false } } : req
// If Authorization header is also being user-defined, that takes priority
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return []
if (!request) return []
if (!request.auth || !request.auth.authActive) return []
const headers: HoppGQLHeader[] = []
// TODO: Support a better b64 implementation than btoa ?
if (request.auth.authType === "basic") {
const username = request.auth.username
const password = request.auth.password
headers.push({
active: true,
key: "Authorization",
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
) {
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${request.auth.token}`,
})
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth
if (addTo === "Headers" && key) {
headers.push({
active: true,
key,
value: request.auth.value ?? "",
})
}
}
return headers
}
const getComputedHeaders = (req: HoppGQLRequest) => {
return [
...getComputedAuthHeaders(req).map((header) => ({
source: "auth" as const,
header,
})),
]
}
const computedHeaders = computed(() =>
getComputedHeaders(request.value).map((header, index) => ({
id: `header-${index}`,
...header,
}))
)
const inheritedProperties = computed(() => {
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
return []
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
)
)
const headers = inheritedHeaders
.filter(
(header) =>
header.inheritedHeader !== null &&
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
)
.map((header, index) => ({
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key: header.inheritedHeader?.key,
value: header.inheritedHeader?.value,
active: header.inheritedHeader?.active,
},
}))
let auth = [] as {
inheritedFrom: string
source: "auth"
id: string
header: {
key: string
value: string
active: boolean
}
}[]
const computedAuthHeader = getComputedAuthHeaders(
request.value,
props.inheritedProperties.auth.inheritedAuth
)[0]
if (
computedAuthHeader &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive
) {
auth = [
{
inheritedFrom: props.inheritedProperties?.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
},
]
}
return [...headers, ...auth]
})
const masking = ref(true)
const toggleMask = () => {
masking.value = !masking.value
}
const mask = (header: any) => {
if (header.source === "auth" && masking.value)
return header.header.value.replace(/\S/gi, "*")
return header.header.value
}
// const changeTab = (tab: ComputedHeader["source"]) => {
// if (tab === "auth") emit("change-tab", "authorization")
// else emit("change-tab", "bodyParams")
// }
</script>

View File

@@ -34,16 +34,10 @@
:label="`${t('tab.headers')}`"
:info="activeGQLHeadersCount === 0 ? null : `${activeGQLHeadersCount}`"
>
<GraphqlHeaders
v-model="request"
:inherited-properties="inheritedProperties"
/>
<GraphqlHeaders v-model="request" />
</HoppSmartTab>
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
<GraphqlAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
/>
<GraphqlAuthorization v-model="request.auth" />
</HoppSmartTab>
</HoppSmartTabs>
<CollectionsSaveRequest
@@ -75,7 +69,6 @@ import { useService } from "dioc/vue"
import { InterceptorService } from "~/services/interceptor.service"
import { editGraphqlRequest } from "~/newstore/collections"
import { GQLTabService } from "~/services/tab/graphql"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const VALID_GQL_OPERATIONS = [
"query",
@@ -100,22 +93,24 @@ const props = withDefaults(
response?: GQLResponseEvent[] | null
optionTab?: GQLOptionTabs
tabId: string
inheritedProperties?: HoppInheritedProperty
}>(),
{
response: null,
optionTab: "query",
}
)
const emit = defineEmits<{
(e: "update:modelValue", value: HoppGQLRequest): void
(e: "update:optionTab", value: GQLOptionTabs): void
(e: "update:response", value: GQLResponseEvent[]): void
}>()
const emit = defineEmits(["update:modelValue", "update:response"])
const selectedOptionTab = useVModel(props, "optionTab", emit)
const request = useVModel(props, "modelValue", emit)
const request = ref(props.modelValue)
watch(
() => request.value,
(newVal) => {
emit("update:modelValue", newVal)
},
{ deep: true }
)
const url = computedWithControl(
() => tabs.currentActiveTab.value,
@@ -136,30 +131,10 @@ const runQuery = async (
startPageProgress()
try {
const runURL = clone(url.value)
const runHeaders = clone(request.value.headers)
const runQuery = clone(request.value.query)
const runVariables = clone(request.value.variables)
const runAuth =
request.value.auth.authType === "inherit" && request.value.auth.authActive
? clone(tabs.currentActiveTab.value.document.inheritedProperties?.auth)
: clone(request.value.auth)
const inheritedHeaders =
tabs.currentActiveTab.value.document.inheritedProperties?.headers.map(
(header) => {
if (header.inheritedHeader) {
return header.inheritedHeader
}
return []
}
)
let runHeaders: HoppGQLRequest["headers"] = []
if (inheritedHeaders) {
runHeaders = [...inheritedHeaders, ...clone(request.value.headers)]
} else {
runHeaders = clone(request.value.headers)
}
const runAuth = clone(request.value.auth)
await runGQLOperation({
name: request.value.name,
@@ -167,7 +142,7 @@ const runQuery = async (
headers: runHeaders,
query: runQuery,
variables: runVariables,
auth: runAuth ?? { authType: "none", authActive: false },
auth: runAuth,
operationName: definition?.name?.value,
operationType: definition?.operation ?? "query",
})

View File

@@ -5,7 +5,6 @@
v-model="tab.document.request"
v-model:response="tab.document.response"
v-model:option-tab="tab.document.optionTabPreference"
v-model:inherited-properties="tab.document.inheritedProperties"
:tab-id="tab.id"
/>
</template>

View File

@@ -1,12 +1,7 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="[
isCollectionProperty
? 'top-propertiesPrimaryStickyFold'
: 'top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold',
]"
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<span class="flex items-center">
<label class="truncate font-semibold text-secondaryLight">
@@ -42,18 +37,6 @@
}
"
/>
<HoppSmartItem
v-if="!isRootCollection"
label="Inherit"
:icon="authName === 'Inherit' ? IconCircleDot : IconCircle"
:active="authName === 'Inherit'"
@click="
() => {
auth.authType = 'inherit'
hide()
}
"
/>
<HoppSmartItem
label="Basic Auth"
:icon="authName === 'Basic Auth' ? IconCircleDot : IconCircle"
@@ -152,21 +135,6 @@
<div v-if="auth.authType === 'basic'">
<HttpAuthorizationBasic v-model="auth" />
</div>
<div v-if="auth.authType === 'inherit'" class="p-4">
<span v-if="inheritedProperties?.auth">
{{
t("authorization.inherited_from", {
auth: getAuthName(
inheritedProperties.auth.inheritedAuth.authType
),
collection: inheritedProperties?.auth.parentName,
})
}}
</span>
<span v-else>
{{ t("authorization.save_to_inherit") }}
</span>
</div>
<div v-if="auth.authType === 'bearer'">
<div class="flex flex-1 border-b border-dividerLight">
<SmartEnvInput v-model="auth.token" placeholder="Token" />
@@ -213,8 +181,6 @@ import { pluckRef } from "@composables/ref"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { useVModel } from "@vueuse/core"
import { onMounted } from "vue"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
@@ -222,9 +188,6 @@ const colorMode = useColorMode()
const props = defineProps<{
modelValue: HoppRESTAuth
isCollectionProperty?: boolean
isRootCollection?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const emit = defineEmits<{
@@ -233,34 +196,18 @@ const emit = defineEmits<{
const auth = useVModel(props, "modelValue", emit)
onMounted(() => {
if (props.isRootCollection && auth.value.authType === "inherit") {
auth.value = {
authType: "none",
authActive: true,
}
}
})
const AUTH_KEY_NAME = {
basic: "Basic Auth",
bearer: "Bearer",
"oauth-2": "OAuth 2.0",
"api-key": "API key",
none: "None",
inherit: "Inherit",
} as const
const authType = pluckRef(auth, "authType")
const authName = computed(() =>
AUTH_KEY_NAME[authType.value] ? AUTH_KEY_NAME[authType.value] : "None"
)
const getAuthName = (type: HoppRESTAuth["authType"] | undefined) => {
if (!type) return "None"
return AUTH_KEY_NAME[type] ? AUTH_KEY_NAME[type] : "None"
}
const authActive = pluckRef(auth, "authActive")
const clearContent = () => {

View File

@@ -1,12 +1,7 @@
<template>
<div class="flex flex-1 flex-col">
<div
class="sticky z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4"
:class="[
isCollectionProperty
? 'top-propertiesPrimaryStickyFold'
: 'top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold',
]"
class="sticky top-upperMobileSecondaryStickyFold z-10 flex flex-shrink-0 items-center justify-between overflow-x-auto border-b border-dividerLight bg-primary pl-4 sm:top-upperSecondaryStickyFold"
>
<label class="truncate font-semibold text-secondaryLight">
{{ t("request.header_list") }}
@@ -208,61 +203,6 @@
</div>
</template>
</draggable>
<draggable
v-model="inheritedProperties"
item-key="id"
animation="250"
handle=".draggable-handle"
draggable=".draggable-content"
ghost-class="cursor-move"
chosen-class="bg-primaryLight"
drag-class="cursor-grabbing"
>
<template #item="{ element: header, index }">
<div
class="draggable-content group flex divide-x divide-dividerLight border-b border-dividerLight"
>
<span>
<HoppButtonSecondary
:icon="IconLock"
class="cursor-auto bg-divider text-secondaryLight opacity-25"
tabindex="-1"
/>
</span>
<SmartEnvInput
v-model="header.header.key"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<SmartEnvInput
:model-value="
header.source === 'auth' ? mask(header) : header.header.value
"
:placeholder="`${t('count.value', { count: index + 1 })}`"
readonly
/>
<HoppButtonSecondary
v-if="header.source === 'auth'"
v-tippy="{ theme: 'tooltip' }"
:title="t(masking ? 'state.show' : 'state.hide')"
:icon="masking && header.source === 'auth' ? IconEye : IconEyeOff"
@click="toggleMask()"
/>
<span v-else class="aspect-square w-[2.05rem]"></span>
<span>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
:icon="IconInfo"
:title="`This header is inherited from Parent Collection ${
header.inheritedFrom ?? ''
}`"
/>
</span>
</div>
</template>
</draggable>
<HoppSmartPlaceholder
v-if="workingHeaders.length === 0"
:src="`/images/states/${colorMode.value}/add_category.svg`"
@@ -296,7 +236,6 @@ import IconEye from "~icons/lucide/eye"
import IconEyeOff from "~icons/lucide/eye-off"
import IconArrowUpRight from "~icons/lucide/arrow-up-right"
import IconWrapText from "~icons/lucide/wrap-text"
import IconInfo from "~icons/lucide/info"
import { useColorMode } from "@composables/theming"
import { computed, reactive, ref, watch } from "vue"
import { isEqual, cloneDeep } from "lodash-es"
@@ -325,14 +264,12 @@ import { objRemoveKey } from "~/helpers/functional/object"
import {
ComputedHeader,
getComputedHeaders,
getComputedAuthHeaders,
} from "~/helpers/utils/EffectiveURL"
import { aggregateEnvs$, getAggregateEnvs } from "~/newstore/environments"
import { useVModel } from "@vueuse/core"
import { useService } from "dioc/vue"
import { InspectionService, InspectorResult } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const t = useI18n()
const toast = useToast()
@@ -351,11 +288,7 @@ const linewrapEnabled = ref(true)
const deletionToast = ref<{ goAway: (delay: number) => void } | null>(null)
// v-model integration with props and emit
const props = defineProps<{
modelValue: HoppRESTRequest
isCollectionProperty?: boolean
inheritedProperties?: HoppInheritedProperty
}>()
const props = defineProps<{ modelValue: HoppRESTRequest }>()
const emit = defineEmits<{
(e: "change-tab", value: RESTOptionTabs): void
@@ -561,72 +494,6 @@ const computedHeaders = computed(() =>
)
)
const inheritedProperties = computed(() => {
if (!props.inheritedProperties?.auth || !props.inheritedProperties.headers)
return []
//filter out headers that are already in the request headers
const inheritedHeaders = props.inheritedProperties.headers.filter(
(header) =>
!request.value.headers.some(
(requestHeader) => requestHeader.key === header.inheritedHeader?.key
)
)
const headers = inheritedHeaders
.filter(
(header) =>
header.inheritedHeader !== null &&
header.inheritedHeader !== undefined &&
header.inheritedHeader.active
)
.map((header, index) => ({
inheritedFrom: props.inheritedProperties?.headers[index].parentName,
source: "headers",
id: `header-${index}`,
header: {
key: header.inheritedHeader?.key,
value: header.inheritedHeader?.value,
active: header.inheritedHeader?.active,
},
}))
let auth = [] as {
inheritedFrom: string
source: "auth"
id: string
header: {
key: string
value: string
active: boolean
}
}[]
const computedAuthHeader = getComputedAuthHeaders(
aggregateEnvs.value,
request.value,
props.inheritedProperties.auth.inheritedAuth
)[0]
if (
computedAuthHeader &&
request.value.auth.authType === "inherit" &&
request.value.auth.authActive
) {
auth = [
{
inheritedFrom: props.inheritedProperties?.auth.parentName,
source: "auth",
id: `header-auth`,
header: computedAuthHeader,
},
]
}
return [...headers, ...auth]
})
const masking = ref(true)
const toggleMask = () => {

View File

@@ -29,21 +29,14 @@
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders
v-model="request"
:inherited-properties="inheritedProperties"
@change-tab="changeOptionTab"
/>
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('authorization') : true"
:id="'authorization'"
:label="`${t('tab.authorization')}`"
>
<HttpAuthorization
v-model="request.auth"
:inherited-properties="inheritedProperties"
/>
<HttpAuthorization v-model="request.auth" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true"
@@ -76,7 +69,6 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { useVModel } from "@vueuse/core"
import { computed } from "vue"
import { defineActionHandler } from "~/helpers/actions"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const VALID_OPTION_TABS = [
"params",
@@ -97,7 +89,6 @@ const props = withDefaults(
modelValue: HoppRESTRequest
optionTab: RESTOptionTabs
properties?: string[]
inheritedProperties?: HoppInheritedProperty
}>(),
{
optionTab: "params",

View File

@@ -5,7 +5,6 @@
<HttpRequestOptions
v-model="tab.document.request"
v-model:option-tab="tab.document.optionTabPreference"
v-model:inherited-properties="tab.document.inheritedProperties"
/>
</template>
<template #secondary>

View File

@@ -1,5 +1,5 @@
<template>
<div class="flex flex-col space-y-2">
<div class="flex flex-col">
<HoppSmartItem
v-for="source in sources"
:key="source.id"

View File

@@ -31,7 +31,7 @@
</template>
<script setup lang="ts">
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { computed, ref } from "vue"
import { useI18n } from "~/composables/i18n"
import { useReadonlyStream } from "~/composables/stream"
@@ -48,7 +48,7 @@ const hasSelectedCollectionID = computed(() => {
const myCollections = useReadonlyStream(restCollections$, [])
const emit = defineEmits<{
(e: "importFromMyCollection", content: HoppCollection): void
(e: "importFromMyCollection", content: HoppCollection<HoppRESTRequest>): void
}>()
const fetchCollectionFromMyCollections = async () => {

View File

@@ -95,41 +95,13 @@ export function runRESTRequest$(
return E.left("script_fail" as const)
}
const requestAuth =
tab.value.document.request.auth.authType === "inherit" &&
tab.value.document.request.auth.authActive
? tab.value.document.inheritedProperties?.auth.inheritedAuth
: tab.value.document.request.auth
let requestHeaders
const inheritedHeaders =
tab.value.document.inheritedProperties?.headers.map((header) => {
if (header.inheritedHeader) {
return header.inheritedHeader
}
return []
})
if (inheritedHeaders) {
requestHeaders = [
...inheritedHeaders,
...tab.value.document.request.headers,
]
} else {
requestHeaders = [...tab.value.document.request.headers]
}
const finalRequest = {
...tab.value.document.request,
auth: requestAuth ?? { authType: "none", authActive: false },
headers: requestHeaders,
}
const effectiveRequest = getEffectiveRESTRequest(finalRequest, {
name: "Env",
variables: combineEnvVariables(envs.right),
})
const effectiveRequest = getEffectiveRESTRequest(
tab.value.document.request,
{
name: "Env",
variables: combineEnvVariables(envs.right),
}
)
const [stream, cancelRun] = createRESTNetworkRequestStream(effectiveRequest)
cancelFunc = cancelRun

View File

@@ -1,15 +0,0 @@
mutation UpdateTeamCollection(
$collectionID: ID!
$newTitle: String
$data: String
) {
updateTeamCollection(
collectionID: $collectionID
newTitle: $newTitle
data: $data
) {
id
title
data
}
}

View File

@@ -3,7 +3,6 @@ query GetCollectionChildren($collectionID: ID!, $cursor: ID) {
children(cursor: $cursor) {
id
title
data
}
}
}

View File

@@ -0,0 +1,5 @@
query GetCollectionTitle($collectionID: ID!) {
collection(collectionID: $collectionID) {
title
}
}

View File

@@ -1,6 +0,0 @@
query GetCollectionTitleAndData($collectionID: ID!) {
collection(collectionID: $collectionID) {
title
data
}
}

View File

@@ -2,7 +2,6 @@ query GetSingleCollection($collectionID: ID!) {
collection(collectionID: $collectionID) {
id
title
data
parent {
id
}

View File

@@ -2,6 +2,5 @@ query RootCollectionsOfTeam($teamID: ID!, $cursor: ID) {
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
id
title
data
}
}

View File

@@ -2,7 +2,6 @@ subscription TeamCollectionAdded($teamID: ID!) {
teamCollectionAdded(teamID: $teamID) {
id
title
data
parent {
id
}

View File

@@ -2,7 +2,6 @@ subscription TeamCollectionUpdated($teamID: ID!) {
teamCollectionUpdated(teamID: $teamID) {
id
title
data
parent {
id
}

View File

@@ -4,6 +4,7 @@ import * as TE from "fp-ts/TaskEither"
import { pipe, flow } from "fp-ts/function"
import {
HoppCollection,
HoppRESTRequest,
makeCollection,
translateToNewRequest,
} from "@hoppscotch/data"
@@ -14,7 +15,7 @@ import {
ExportAsJsonDocument,
GetCollectionChildrenIDsDocument,
GetCollectionRequestsDocument,
GetCollectionTitleAndDataDocument,
GetCollectionTitleDocument,
} from "./graphql"
export const BACKEND_PAGE_SIZE = 10
@@ -84,19 +85,16 @@ export const getCompleteCollectionTree = (
pipe(
TE.Do,
TE.bind("titleAndData", () =>
TE.bind("title", () =>
pipe(
() =>
runGQLQuery({
query: GetCollectionTitleAndDataDocument,
query: GetCollectionTitleDocument,
variables: {
collectionID: collID,
},
}),
TE.map((result) => ({
title: result.collection!.title,
data: result.collection!.data,
}))
TE.map((x) => x.collection!.title)
)
),
TE.bind("children", () =>
@@ -110,36 +108,24 @@ export const getCompleteCollectionTree = (
TE.bind("requests", () => () => getCollectionRequests(collID)),
TE.map(
({ titleAndData, children, requests }) =>
({ title, children, requests }) =>
<TeamCollection>{
id: collID,
children,
requests,
title: titleAndData.title,
data: titleAndData.data,
title,
}
)
)
export const teamCollToHoppRESTColl = (
coll: TeamCollection
): HoppCollection => {
const data =
coll.data && coll.data !== "null"
? JSON.parse(coll.data)
: {
auth: { authType: "inherit", authActive: true },
headers: [],
}
return makeCollection({
): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: coll.title,
folders: coll.children?.map(teamCollToHoppRESTColl) ?? [],
requests: coll.requests?.map((x) => x.request) ?? [],
auth: data.auth ?? { authType: "inherit", authActive: true },
headers: data.headers ?? [],
})
}
/**
* Get the JSON string of all the collection of the specified team

View File

@@ -21,9 +21,6 @@ import {
UpdateCollectionOrderDocument,
UpdateCollectionOrderMutation,
UpdateCollectionOrderMutationVariables,
UpdateTeamCollectionDocument,
UpdateTeamCollectionMutation,
UpdateTeamCollectionMutationVariables,
} from "../graphql"
type CreateNewRootCollectionError = "team_coll/short_title"
@@ -125,18 +122,3 @@ export const importJSONToTeam = (collectionJSON: string, teamID: string) =>
teamID,
}
)
export const updateTeamCollection = (
collectionID: string,
data?: string,
newTitle?: string
) =>
runMutation<
UpdateTeamCollectionMutation,
UpdateTeamCollectionMutationVariables,
""
>(UpdateTeamCollectionDocument, {
collectionID,
data,
newTitle,
})

View File

@@ -1,12 +1,10 @@
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
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"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
import { GQLTabService } from "~/services/tab/graphql"
/**
* Resolve save context on reorder
@@ -110,135 +108,6 @@ export function updateSaveContextForAffectedRequests(
}
}
/**
* Used to check the new folder path is close to the save context folder path or not
* @param folderPathCurrent The path saved as the inherited path in the inherited properties
* @param newFolderPath The incomming path
* @param saveContextPath The save context of the request
* @returns The path which is close to saveContext.folderPath
*/
function folderPathCloseToSaveContext(
folderPathCurrent: string | undefined,
newFolderPath: string,
saveContextPath: string
) {
if (!folderPathCurrent) return newFolderPath
const folderPathCurrentArray = folderPathCurrent.split("/")
const newFolderPathArray = newFolderPath.split("/")
const saveContextFolderPathArray = saveContextPath.split("/")
let folderPathCurrentMatch = 0
for (let i = 0; i < folderPathCurrentArray.length; i++) {
if (folderPathCurrentArray[i] === saveContextFolderPathArray[i]) {
folderPathCurrentMatch++
}
}
let newFolderPathMatch = 0
for (let i = 0; i < newFolderPathArray.length; i++) {
if (newFolderPathArray[i] === saveContextFolderPathArray[i]) {
newFolderPathMatch++
}
}
if (folderPathCurrentMatch > newFolderPathMatch) {
return folderPathCurrent
}
return newFolderPath
}
export function updateInheritedPropertiesForAffectedRequests(
path: string,
inheritedProperties: HoppInheritedProperty,
type: "rest" | "graphql",
workspace: "personal" | "team" = "personal"
) {
const tabService =
type === "rest" ? getService(RESTTabService) : getService(GQLTabService)
let tabs
if (workspace === "personal") {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "user-collection" &&
tab.document.saveContext.folderPath.startsWith(path)
)
})
} else {
tabs = tabService.getTabsRefTo((tab) => {
return (
tab.document.saveContext?.originLocation === "team-collection" &&
tab.document.saveContext.collectionID?.startsWith(path)
)
})
}
const tabsEffectedByAuth = tabs.filter((tab) => {
if (workspace === "personal") {
return (
tab.value.document.saveContext?.originLocation === "user-collection" &&
tab.value.document.saveContext.folderPath.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.folderPath
)
)
}
return (
tab.value.document.saveContext?.originLocation === "team-collection" &&
tab.value.document.saveContext.collectionID?.startsWith(path) &&
path ===
folderPathCloseToSaveContext(
tab.value.document.inheritedProperties?.auth.parentID,
path,
tab.value.document.saveContext.collectionID
)
)
})
const tabsEffectedByHeaders = tabs.filter((tab) => {
return (
tab.value.document.inheritedProperties &&
tab.value.document.inheritedProperties.headers.some(
(header) => header.parentID === path
)
)
})
for (const tab of tabsEffectedByAuth) {
tab.value.document.inheritedProperties = inheritedProperties
}
for (const tab of tabsEffectedByHeaders) {
const headers = tab.value.document.inheritedProperties?.headers.map(
(header) => {
if (header.parentID === path) {
return {
...header,
inheritedHeader: inheritedProperties.headers.find(
(inheritedHeader) =>
inheritedHeader.inheritedHeader?.key ===
header.inheritedHeader?.key
)?.inheritedHeader,
}
}
return header
}
)
tab.value.document.inheritedProperties = {
...tab.value.document.inheritedProperties,
headers,
}
}
}
function resetSaveContextForAffectedRequests(folderPath: string) {
const tabService = getService(RESTTabService)
const tabs = tabService.getTabsRefTo((tab) => {
@@ -283,9 +152,9 @@ export async function resetTeamRequestsContext() {
}
export function getFoldersByPath(
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest>[],
path: string
): HoppCollection[] {
): HoppCollection<HoppRESTRequest>[] {
if (!path) return collections
// path will be like this "0/0/1" these are the indexes of the folders

View File

@@ -1,8 +1,4 @@
import {
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getAffectedIndexes } from "./affectedIndex"
import { RESTTabService } from "~/services/tab/rest"
import { getService } from "~/modules/dioc"
@@ -57,9 +53,9 @@ export function resolveSaveContextOnRequestReorder(payload: {
}
export function getRequestsByPath(
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest>[],
path: string
): HoppRESTRequest[] | HoppGQLRequest[] {
): HoppRESTRequest[] {
// path will be like this "0/0/1" these are the indexes of the folders
const pathArray = path.split("/").map((index) => parseInt(index))

View File

@@ -1,16 +0,0 @@
import yaml from "js-yaml"
import * as O from "fp-ts/Option"
import { safeParseJSON } from "./json"
import { pipe } from "fp-ts/function"
export const safeParseYAML = (str: string) => O.tryCatch(() => yaml.load(str))
export const safeParseJSONOrYAML = (str: string) =>
pipe(
str,
safeParseJSON,
O.match(
() => safeParseYAML(str),
(data) => O.of(data)
)
)

View File

@@ -27,7 +27,7 @@ export const getDefaultGQLRequest = (): HoppGQLRequest => ({
}`,
query: DEFAULT_QUERY,
auth: {
authType: "inherit",
authType: "none",
authActive: true,
},
})

View File

@@ -1,7 +1,6 @@
import { HoppGQLRequest } from "@hoppscotch/data"
import { GQLResponseEvent } from "./connection"
import { GQLOptionTabs } from "~/components/graphql/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
export type HoppGQLSaveContext =
| {
@@ -74,10 +73,4 @@ export type HoppGQLDocument = {
* Options tab preference for the current tab's document
*/
optionTabPreference?: GQLOptionTabs
/**
* The inherited properties from the parent collection
* (if any)
*/
inheritedProperties?: HoppInheritedProperty
}

View File

@@ -1,5 +1,7 @@
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
export const gqlCollectionsExporter = (gqlCollections: HoppCollection[]) => {
export const gqlCollectionsExporter = (
gqlCollections: HoppCollection<HoppGQLRequest>[]
) => {
return JSON.stringify(gqlCollections, null, 2)
}

View File

@@ -1,5 +1,7 @@
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
export const myCollectionsExporter = (myCollections: HoppCollection[]) => {
export const myCollectionsExporter = (
myCollections: HoppCollection<HoppRESTRequest>[]
) => {
return JSON.stringify(myCollections, null, 2)
}

View File

@@ -2,12 +2,15 @@ import { pipe, flow } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option"
import * as RA from "fp-ts/ReadonlyArray"
import { translateToNewRESTCollection, HoppCollection } from "@hoppscotch/data"
import {
translateToNewRESTCollection,
HoppCollection,
HoppRESTRequest,
} from "@hoppscotch/data"
import { isPlainObject as _isPlainObject } from "lodash-es"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { safeParseJSON } from "~/helpers/functional/json"
import { translateToNewGQLCollection } from "@hoppscotch/data"
export const hoppRESTImporter = (content: string) =>
pipe(
@@ -30,10 +33,12 @@ const isPlainObject = (value: any): value is object => _isPlainObject(value)
/**
* checks if a collection matches the schema for a hoppscotch collection.
* here 2 is the latest version of the schema.
* as of now we are only checking if the collection has a "v" key in it.
*/
const isValidCollection = (collection: unknown): collection is HoppCollection =>
isPlainObject(collection) && "v" in collection && collection.v === 2
const isValidCollection = (
collection: unknown
): collection is HoppCollection<HoppRESTRequest> =>
isPlainObject(collection) && "v" in collection
/**
* checks if a collection is a valid hoppscotch collection.
@@ -51,29 +56,3 @@ const validateCollection = (collection: unknown) => {
*/
const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] =>
Array.isArray(collections) ? collections : [collections]
export const hoppGQLImporter = (content: string) =>
pipe(
safeParseJSON(content),
O.chain(
flow(
makeCollectionsArray,
RA.map(validateGQLCollection),
O.sequenceArray,
O.map(RA.toArray)
)
),
TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT)
)
/**
*
* @param collection the collection to validate
* @returns the collection if it is valid, else a translated version of the collection
*/
export const validateGQLCollection = (collection: unknown) => {
if (isValidCollection(collection)) {
return O.some(collection)
}
return O.some(translateToNewGQLCollection(collection))
}

View File

@@ -1,12 +1,12 @@
import { HoppCollection } from "@hoppscotch/data"
import { HoppCollection, HoppGQLRequest } from "@hoppscotch/data"
import * as E from "fp-ts/Either"
// TODO: add zod validation
export const hoppGqlCollectionsImporter = (
content: string
): E.Either<"INVALID_JSON", HoppCollection[]> => {
): E.Either<"INVALID_JSON", HoppCollection<HoppGQLRequest>[]> => {
return E.tryCatch(
() => JSON.parse(content) as HoppCollection[],
() => JSON.parse(content) as HoppCollection<HoppGQLRequest>[],
() => "INVALID_JSON"
)
}

View File

@@ -1,5 +1,5 @@
import { convert, ImportRequest } from "insomnia-importers"
import { pipe } from "fp-ts/function"
import { pipe, flow } from "fp-ts/function"
import {
HoppRESTAuth,
HoppRESTHeader,
@@ -12,10 +12,10 @@ import {
makeCollection,
} from "@hoppscotch/data"
import * as A from "fp-ts/Array"
import * as S from "fp-ts/string"
import * as TO from "fp-ts/TaskOption"
import * as TE from "fp-ts/TaskEither"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { replaceInsomniaTemplating } from "./insomniaEnv"
// TODO: Insomnia allows custom prefixes for Bearer token auth, Hoppscotch doesn't. We just ignore the prefix for now
@@ -32,8 +32,10 @@ type InsomniaRequestResource = ImportRequest & { _type: "request" }
const parseInsomniaDoc = (content: string) =>
TO.tryCatch(() => convert(content))
const replaceVarTemplating = (expression: string) =>
replaceInsomniaTemplating(expression)
const replaceVarTemplating = flow(
S.replace(/{{\s*/g, "<<"),
S.replace(/\s*}}/g, ">>")
)
const getFoldersIn = (
folder: InsomniaFolderResource | null,
@@ -192,15 +194,13 @@ const getHoppRequest = (req: InsomniaRequestResource): HoppRESTRequest =>
const getHoppFolder = (
folderRes: InsomniaFolderResource,
resources: InsomniaResource[]
): HoppCollection =>
): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: folderRes.name ?? "",
folders: getFoldersIn(folderRes, resources).map((f) =>
getHoppFolder(f, resources)
),
requests: getRequestsIn(folderRes, resources).map(getHoppRequest),
auth: { authType: "inherit", authActive: true },
headers: [],
})
const getHoppCollections = (doc: InsomniaDoc) =>

View File

@@ -1,85 +0,0 @@
import * as TE from "fp-ts/TaskEither"
import * as O from "fp-ts/Option"
import { IMPORTER_INVALID_FILE_FORMAT } from "."
import { z } from "zod"
import { Environment } from "@hoppscotch/data"
import { safeParseJSONOrYAML } from "~/helpers/functional/yaml"
const insomniaResourcesSchema = z.object({
resources: z.array(
z
.object({
_type: z.string(),
})
.passthrough()
),
})
const insomniaEnvSchema = z.object({
_type: z.literal("environment"),
name: z.string(),
data: z.record(z.string()),
})
export const replaceInsomniaTemplating = (expression: string) => {
const regex = /\{\{ _\.([^}]+) \}\}/g
return expression.replaceAll(regex, "<<$1>>")
}
export const insomniaEnvImporter = (content: string) => {
const parsedContent = safeParseJSONOrYAML(content)
if (O.isNone(parsedContent)) {
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
}
const validationResult = insomniaResourcesSchema.safeParse(
parsedContent.value
)
if (!validationResult.success) {
return TE.left(IMPORTER_INVALID_FILE_FORMAT)
}
const insomniaEnvs = validationResult.data.resources
.filter((resource) => resource._type === "environment")
.map((envResource) => {
const envResourceData = envResource.data as Record<string, unknown>
const stringifiedData: Record<string, string> = {}
Object.keys(envResourceData).forEach((key) => {
stringifiedData[key] = String(envResourceData[key])
})
return { ...envResource, data: stringifiedData }
})
const environments: Environment[] = []
insomniaEnvs.forEach((insomniaEnv) => {
const parsedInsomniaEnv = insomniaEnvSchema.safeParse(insomniaEnv)
if (parsedInsomniaEnv.success) {
const environment: Environment = {
name: parsedInsomniaEnv.data.name,
variables: Object.entries(parsedInsomniaEnv.data.data).map(
([key, value]) => ({ key, value })
),
}
environments.push(environment)
}
})
const processedEnvironments = environments.map((env) => ({
...env,
variables: env.variables.map((variable) => ({
...variable,
value: replaceInsomniaTemplating(variable.value),
})),
}))
return TE.right(processedEnvironments)
}

View File

@@ -12,6 +12,7 @@ import {
HoppRESTHeader,
HoppRESTParam,
HoppRESTReqBody,
HoppRESTRequest,
knownContentTypes,
makeRESTRequest,
HoppCollection,
@@ -580,7 +581,7 @@ const convertPathToHoppReqs = (
const convertOpenApiDocToHopp = (
doc: OpenAPI.Document
): TE.TaskEither<never, HoppCollection[]> => {
): TE.TaskEither<never, HoppCollection<HoppRESTRequest>[]> => {
const name = doc.info.title
const paths = Object.entries(doc.paths ?? {})
@@ -588,12 +589,10 @@ const convertOpenApiDocToHopp = (
.flat()
return TE.of([
makeCollection({
makeCollection<HoppRESTRequest>({
name,
folders: [],
requests: paths,
auth: { authType: "inherit", authActive: true },
headers: [],
}),
])
}

View File

@@ -283,7 +283,7 @@ const getHoppRequest = (item: Item): HoppRESTRequest => {
})
}
const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection<HoppRESTRequest> =>
makeCollection({
name: ig.name,
folders: pipe(
@@ -292,8 +292,6 @@ const getHoppFolder = (ig: ItemGroup<Item>): HoppCollection =>
A.map(getHoppFolder)
),
requests: pipe(ig.items.all(), A.filter(isPMItem), A.map(getHoppRequest)),
auth: { authType: "inherit", authActive: true },
headers: [],
})
export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll)

View File

@@ -2,7 +2,6 @@ import { HoppRESTRequest } from "@hoppscotch/data"
import { HoppRESTResponse } from "../types/HoppRESTResponse"
import { HoppTestResult } from "../types/HoppTestResult"
import { RESTOptionTabs } from "~/components/http/RequestOptions.vue"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
export type HoppRESTSaveContext =
| {
@@ -81,10 +80,4 @@ export type HoppRESTDocument = {
* Options tab preference for the current tab's document
*/
optionTabPreference?: RESTOptionTabs
/**
* The inherited properties from the parent collection
* (if any)
*/
inheritedProperties?: HoppInheritedProperty
}

View File

@@ -8,5 +8,4 @@ export interface TeamCollection {
title: string
children: TeamCollection[] | null
requests: TeamRequest[] | null
data: string | null
}

View File

@@ -1,10 +1,6 @@
import * as E from "fp-ts/Either"
import { BehaviorSubject, Subscription } from "rxjs"
import {
HoppRESTAuth,
HoppRESTHeader,
translateToNewRequest,
} from "@hoppscotch/data"
import { translateToNewRequest } from "@hoppscotch/data"
import { pull, remove } from "lodash-es"
import { Subscription as WSubscription } from "wonka"
import { runGQLQuery, runGQLSubscription } from "../backend/GQLClient"
@@ -25,7 +21,6 @@ import {
TeamRequestOrderUpdatedDocument,
TeamCollectionOrderUpdatedDocument,
} from "~/helpers/backend/graphql"
import { HoppInheritedProperty } from "../types/HoppInheritedProperties"
const TEAMS_BACKEND_PAGE_SIZE = 10
@@ -547,7 +542,6 @@ export default class NewTeamCollectionAdapter {
children: null,
requests: null,
title: title,
data: null,
},
parentID ?? null
)
@@ -699,7 +693,6 @@ export default class NewTeamCollectionAdapter {
children: null,
requests: null,
title: result.right.teamCollectionAdded.title,
data: result.right.teamCollectionAdded.data ?? null,
},
result.right.teamCollectionAdded.parent?.id ?? null
)
@@ -722,7 +715,6 @@ export default class NewTeamCollectionAdapter {
this.updateCollection({
id: result.right.teamCollectionUpdated.id,
title: result.right.teamCollectionUpdated.title,
data: result.right.teamCollectionUpdated.data,
})
})
@@ -939,7 +931,6 @@ export default class NewTeamCollectionAdapter {
<TeamCollection>{
id: el.id,
title: el.title,
data: el.data,
children: null,
requests: null,
}
@@ -1033,104 +1024,4 @@ export default class NewTeamCollectionAdapter {
)
}
}
public cascadeParentCollectionForHeaderAuth(folderPath: string) {
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const path = folderPath.split("/")
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
for (let i = 0; i < path.length; i++) {
const parentFolder = findCollInTree(this.collections$.value, path[i])
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
}
const data: {
auth: HoppRESTAuth
headers: HoppRESTHeader[]
} = parentFolder.data
? JSON.parse(parentFolder.data)
: {
auth: null,
headers: null,
}
if (!data.auth) {
data.auth = {
authType: "inherit",
authActive: true,
}
auth.parentID = [...path.slice(0, i + 1)].join("/")
auth.parentName = parentFolder.title
}
if (!data.headers) data.headers = []
const parentFolderAuth = data.auth
const parentFolderHeaders = data.headers
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.title,
inheritedAuth: auth.inheritedAuth,
}
}
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.title,
inheritedAuth: parentFolderAuth,
}
}
// Update headers, overwriting duplicates by key
if (parentFolderHeaders) {
const activeHeaders = parentFolderHeaders.filter((h) => h.active)
activeHeaders.forEach((header) => {
const index = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
if (index !== -1) {
// Replace the existing header with the same key
headers[index] = {
parentID: currentPath,
parentName: parentFolder.title,
inheritedHeader: header,
}
} else {
headers.push({
parentID: currentPath,
parentName: parentFolder.title,
inheritedHeader: header,
})
}
})
}
}
return { auth, headers }
}
}

View File

@@ -1,19 +0,0 @@
import {
GQLHeader,
HoppGQLAuth,
HoppRESTHeader,
HoppRESTAuth,
} from "@hoppscotch/data"
export type HoppInheritedProperty = {
auth: {
parentID: string
parentName: string
inheritedAuth: HoppRESTAuth | HoppGQLAuth
}
headers: {
parentID: string
parentName: string
inheritedHeader: HoppRESTHeader | GQLHeader
}[]
}

View File

@@ -42,26 +42,22 @@ export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
* @param envVars Currently active environment variables
* @returns The list of headers
*/
export const getComputedAuthHeaders = (
envVars: Environment["variables"],
req?: HoppRESTRequest,
auth?: HoppRESTRequest["auth"]
const getComputedAuthHeaders = (
req: HoppRESTRequest,
envVars: Environment["variables"]
) => {
const request = auth ? { auth: auth ?? { authActive: false } } : req
// If Authorization header is also being user-defined, that takes priority
if (req && req.headers.find((h) => h.key.toLowerCase() === "authorization"))
if (req.headers.find((h) => h.key.toLowerCase() === "authorization"))
return []
if (!request) return []
if (!request.auth || !request.auth.authActive) return []
if (!req.auth.authActive) return []
const headers: HoppRESTHeader[] = []
// TODO: Support a better b64 implementation than btoa ?
if (request.auth.authType === "basic") {
const username = parseTemplateString(request.auth.username, envVars)
const password = parseTemplateString(request.auth.password, envVars)
if (req.auth.authType === "basic") {
const username = parseTemplateString(req.auth.username, envVars)
const password = parseTemplateString(req.auth.password, envVars)
headers.push({
active: true,
@@ -69,21 +65,22 @@ export const getComputedAuthHeaders = (
value: `Basic ${btoa(`${username}:${password}`)}`,
})
} else if (
request.auth.authType === "bearer" ||
request.auth.authType === "oauth-2"
req.auth.authType === "bearer" ||
req.auth.authType === "oauth-2"
) {
headers.push({
active: true,
key: "Authorization",
value: `Bearer ${parseTemplateString(request.auth.token, envVars)}`,
value: `Bearer ${parseTemplateString(req.auth.token, envVars)}`,
})
} else if (request.auth.authType === "api-key") {
const { key, addTo } = request.auth
if (addTo === "Headers" && key) {
} else if (req.auth.authType === "api-key") {
const { key, value, addTo } = req.auth
if (addTo === "Headers") {
headers.push({
active: true,
key: parseTemplateString(key, envVars),
value: parseTemplateString(request.auth.value ?? "", envVars),
value: parseTemplateString(value, envVars),
})
}
}
@@ -134,18 +131,16 @@ export type ComputedHeader = {
export const getComputedHeaders = (
req: HoppRESTRequest,
envVars: Environment["variables"]
): ComputedHeader[] => {
return [
...getComputedAuthHeaders(envVars, req).map((header) => ({
source: "auth" as const,
header,
})),
...getComputedBodyHeaders(req).map((header) => ({
source: "body" as const,
header,
})),
]
}
): ComputedHeader[] => [
...getComputedAuthHeaders(req, envVars).map((header) => ({
source: "auth" as const,
header,
})),
...getComputedBodyHeaders(req).map((header) => ({
source: "body" as const,
header,
})),
]
export type ComputedParam = {
source: "auth"
@@ -165,7 +160,7 @@ export const getComputedParams = (
): ComputedParam[] => {
// When this gets complex, its best to split this function off (like with getComputedHeaders)
// API-key auth can be added to query params
if (!req.auth || !req.auth.authActive) return []
if (!req.auth.authActive) return []
if (req.auth.authType !== "api-key") return []
if (req.auth.addTo !== "Query params") return []

View File

@@ -10,34 +10,23 @@ import { cloneDeep } from "lodash-es"
import { resolveSaveContextOnRequestReorder } from "~/helpers/collection/request"
import { getService } from "~/modules/dioc"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
const defaultRESTCollectionState = {
state: [
makeCollection({
makeCollection<HoppRESTRequest>({
name: "My Collection",
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
}),
],
}
const defaultGraphqlCollectionState = {
state: [
makeCollection({
makeCollection<HoppGQLRequest>({
name: "My GraphQL Collection",
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: false,
},
headers: [],
}),
],
}
@@ -50,7 +39,7 @@ type GraphqlCollectionStoreType = typeof defaultGraphqlCollectionState
* Not removing this behaviour because i'm not sure if we utilize this behaviour anywhere and i found this on a tight time crunch.
*/
export function navigateToFolderWithIndexPath(
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
indexPaths: number[]
) {
if (indexPaths.length === 0) return null
@@ -63,94 +52,6 @@ export function navigateToFolderWithIndexPath(
return target !== undefined ? target : null
}
export function cascadeParentCollectionForHeaderAuth(
folderPath: string | undefined,
type: "rest" | "graphql"
) {
const collectionStore =
type === "rest" ? restCollectionStore : graphqlCollectionStore
let auth: HoppInheritedProperty["auth"] = {
parentID: folderPath ?? "",
parentName: "",
inheritedAuth: {
authType: "none",
authActive: true,
},
}
const headers: HoppInheritedProperty["headers"] = []
if (!folderPath) return { auth, headers }
const path = folderPath.split("/").map((i) => parseInt(i))
// Check if the path is empty or invalid
if (!path || path.length === 0) {
console.error("Invalid path:", folderPath)
return { auth, headers }
}
// Loop through the path and get the last parent folder with authType other than 'inherit'
for (let i = 0; i < path.length; i++) {
const parentFolder = navigateToFolderWithIndexPath(
collectionStore.value.state,
[...path.slice(0, i + 1)] // Create a copy of the path array
)
// Check if parentFolder is undefined or null
if (!parentFolder) {
console.error("Parent folder not found for path:", path)
return { auth, headers }
}
const parentFolderAuth = parentFolder.auth
const parentFolderHeaders = parentFolder.headers
// check if the parent folder has authType 'inherit' and if it is the root folder
if (parentFolderAuth?.authType === "inherit" && path.length === 1) {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.name,
inheritedAuth: auth.inheritedAuth,
}
}
if (parentFolderAuth?.authType !== "inherit") {
auth = {
parentID: [...path.slice(0, i + 1)].join("/"),
parentName: parentFolder.name,
inheritedAuth: parentFolderAuth,
}
}
// Update headers, overwriting duplicates by key
if (parentFolderHeaders) {
const activeHeaders = parentFolderHeaders.filter((h) => h.active)
activeHeaders.forEach((header) => {
const index = headers.findIndex(
(h) => h.inheritedHeader?.key === header.key
)
const currentPath = [...path.slice(0, i + 1)].join("/")
if (index !== -1) {
// Replace the existing header with the same key
headers[index] = {
parentID: currentPath,
parentName: parentFolder.name,
inheritedHeader: header,
}
} else {
headers.push({
parentID: currentPath,
parentName: parentFolder.name,
inheritedHeader: header,
})
}
})
}
}
return { auth, headers }
}
function reorderItems(array: unknown[], from: number, to: number) {
const item = array.splice(from, 1)[0]
if (from < to) {
@@ -163,7 +64,7 @@ function reorderItems(array: unknown[], from: number, to: number) {
const restCollectionDispatchers = defineDispatchers({
setCollections(
_: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
) {
return {
state: entries,
@@ -172,7 +73,7 @@ const restCollectionDispatchers = defineDispatchers({
appendCollections(
{ state }: RESTCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
) {
return {
state: [...state, ...entries],
@@ -181,7 +82,7 @@ const restCollectionDispatchers = defineDispatchers({
addCollection(
{ state }: RESTCollectionStoreType,
{ collection }: { collection: HoppCollection }
{ collection }: { collection: HoppCollection<any> }
) {
return {
state: [...state, collection],
@@ -211,7 +112,7 @@ const restCollectionDispatchers = defineDispatchers({
partialCollection,
}: {
collectionIndex: number
partialCollection: Partial<HoppCollection>
partialCollection: Partial<HoppCollection<any>>
}
) {
return {
@@ -227,15 +128,10 @@ const restCollectionDispatchers = defineDispatchers({
{ state }: RESTCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection = makeCollection({
const newFolder: HoppCollection<HoppRESTRequest> = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
const newState = state
@@ -262,7 +158,7 @@ const restCollectionDispatchers = defineDispatchers({
folder,
}: {
path: string
folder: Partial<HoppCollection>
folder: Partial<HoppCollection<HoppRESTRequest>>
}
) {
const newState = state
@@ -353,7 +249,7 @@ const restCollectionDispatchers = defineDispatchers({
}
const theFolder = containingFolder.folders.splice(folderIndex, 1)
newState.push(theFolder[0] as HoppCollection)
newState.push(theFolder[0] as HoppCollection<HoppRESTRequest>)
return {
state: newState,
@@ -716,7 +612,7 @@ const restCollectionDispatchers = defineDispatchers({
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath(
const after = removeDuplicateCollectionsFromPath<HoppRESTRequest>(
id,
collectionPath,
state,
@@ -732,7 +628,7 @@ const restCollectionDispatchers = defineDispatchers({
const gqlCollectionDispatchers = defineDispatchers({
setCollections(
_: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
{ entries }: { entries: HoppCollection<any>[] }
) {
return {
state: entries,
@@ -741,7 +637,7 @@ const gqlCollectionDispatchers = defineDispatchers({
appendCollections(
{ state }: GraphqlCollectionStoreType,
{ entries }: { entries: HoppCollection[] }
{ entries }: { entries: HoppCollection<any>[] }
) {
return {
state: [...state, ...entries],
@@ -750,7 +646,7 @@ const gqlCollectionDispatchers = defineDispatchers({
addCollection(
{ state }: GraphqlCollectionStoreType,
{ collection }: { collection: HoppCollection }
{ collection }: { collection: HoppCollection<any> }
) {
return {
state: [...state, collection],
@@ -777,7 +673,7 @@ const gqlCollectionDispatchers = defineDispatchers({
{
collectionIndex,
collection,
}: { collectionIndex: number; collection: Partial<HoppCollection> }
}: { collectionIndex: number; collection: Partial<HoppCollection<any>> }
) {
return {
state: state.map((col, index) =>
@@ -790,16 +686,12 @@ const gqlCollectionDispatchers = defineDispatchers({
{ state }: GraphqlCollectionStoreType,
{ name, path }: { name: string; path: string }
) {
const newFolder: HoppCollection = makeCollection({
const newFolder: HoppCollection<HoppGQLRequest> = makeCollection({
name,
folders: [],
requests: [],
auth: {
authType: "inherit",
authActive: true,
},
headers: [],
})
const newState = state
const indexPaths = path.split("/").map((x) => parseInt(x))
@@ -819,7 +711,10 @@ const gqlCollectionDispatchers = defineDispatchers({
editFolder(
{ state }: GraphqlCollectionStoreType,
{ path, folder }: { path: string; folder: Partial<HoppCollection> }
{
path,
folder,
}: { path: string; folder: Partial<HoppCollection<HoppGQLRequest>> }
) {
const newState = state
@@ -1018,7 +913,7 @@ const gqlCollectionDispatchers = defineDispatchers({
type: "collection" | "request"
}
) {
const after = removeDuplicateCollectionsFromPath(
const after = removeDuplicateCollectionsFromPath<HoppGQLRequest>(
id,
collectionPath,
state,
@@ -1041,7 +936,7 @@ export const graphqlCollectionStore = new DispatchingStore(
gqlCollectionDispatchers
)
export function setRESTCollections(entries: HoppCollection[]) {
export function setRESTCollections(entries: HoppCollection<HoppRESTRequest>[]) {
restCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
@@ -1058,7 +953,9 @@ export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
pluck("state")
)
export function appendRESTCollections(entries: HoppCollection[]) {
export function appendRESTCollections(
entries: HoppCollection<HoppRESTRequest>[]
) {
restCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
@@ -1067,7 +964,7 @@ export function appendRESTCollections(entries: HoppCollection[]) {
})
}
export function addRESTCollection(collection: HoppCollection) {
export function addRESTCollection(collection: HoppCollection<HoppRESTRequest>) {
restCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
@@ -1095,7 +992,7 @@ export function getRESTCollection(collectionIndex: number) {
export function editRESTCollection(
collectionIndex: number,
partialCollection: Partial<HoppCollection>
partialCollection: Partial<HoppCollection<HoppRESTRequest>>
) {
restCollectionStore.dispatch({
dispatcher: "editCollection",
@@ -1116,7 +1013,10 @@ export function addRESTFolder(name: string, path: string) {
})
}
export function editRESTFolder(path: string, folder: Partial<HoppCollection>) {
export function editRESTFolder(
path: string,
folder: Partial<HoppCollection<HoppRESTRequest>>
) {
restCollectionStore.dispatch({
dispatcher: "editFolder",
payload: {
@@ -1260,7 +1160,9 @@ export function updateRESTCollectionOrder(
})
}
export function setGraphqlCollections(entries: HoppCollection[]) {
export function setGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({
dispatcher: "setCollections",
payload: {
@@ -1269,7 +1171,9 @@ export function setGraphqlCollections(entries: HoppCollection[]) {
})
}
export function appendGraphqlCollections(entries: HoppCollection[]) {
export function appendGraphqlCollections(
entries: HoppCollection<HoppGQLRequest>[]
) {
graphqlCollectionStore.dispatch({
dispatcher: "appendCollections",
payload: {
@@ -1278,7 +1182,9 @@ export function appendGraphqlCollections(entries: HoppCollection[]) {
})
}
export function addGraphqlCollection(collection: HoppCollection) {
export function addGraphqlCollection(
collection: HoppCollection<HoppGQLRequest>
) {
graphqlCollectionStore.dispatch({
dispatcher: "addCollection",
payload: {
@@ -1302,7 +1208,7 @@ export function removeGraphqlCollection(
export function editGraphqlCollection(
collectionIndex: number,
collection: Partial<HoppCollection>
collection: Partial<HoppCollection<HoppGQLRequest>>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editCollection",
@@ -1325,7 +1231,7 @@ export function addGraphqlFolder(name: string, path: string) {
export function editGraphqlFolder(
path: string,
folder: Partial<HoppCollection>
folder: Partial<HoppCollection<HoppGQLRequest>>
) {
graphqlCollectionStore.dispatch({
dispatcher: "editFolder",
@@ -1416,12 +1322,14 @@ export function moveGraphqlRequest(
})
}
function removeDuplicateCollectionsFromPath(
function removeDuplicateCollectionsFromPath<
T extends HoppRESTRequest | HoppGQLRequest,
>(
idToRemove: string,
collectionPath: string | null,
collections: HoppCollection[],
collections: HoppCollection<T>[],
type: "collection" | "request"
): HoppCollection[] {
): HoppCollection<T>[] {
const indexes = collectionPath?.split("/").map((x) => parseInt(x))
indexes && indexes.pop()
const parentPath = indexes?.join("/")

View File

@@ -12,7 +12,7 @@ import { onMounted } from "vue"
import { useRoute, useRouter } from "vue-router"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"
import { HoppCollection } from "@hoppscotch/data"
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
import { appendRESTCollections } from "~/newstore/collections"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
@@ -110,7 +110,9 @@ const handleImportFailure = (error: ImportCollectionsError) => {
toast.error(t(IMPORT_ERROR_MAP[error]).toString())
}
const handleImportSuccess = (collections: HoppCollection[]) => {
const handleImportSuccess = (
collections: HoppCollection<HoppRESTRequest>[]
) => {
appendRESTCollections(collections)
toast.success(t("import.import_from_url_success").toString())
}

View File

@@ -2,7 +2,6 @@ import { ClientOptions } from "@urql/core"
import { Observable } from "rxjs"
import { Component } from "vue"
import { getI18n } from "~/modules/i18n"
import * as E from "fp-ts/Either"
/**
* A common (and required) set of fields that describe a user.
@@ -223,11 +222,6 @@ export type AuthPlatformDef = {
*/
setDisplayName: (name: string) => Promise<void>
/**
* Returns the list of allowed auth providers for the platform ( the currently supported ones are GOOGLE, GITHUB, EMAIL, MICROSOFT, SAML )
*/
getAllowedAuthProviders: () => Promise<E.Either<string, string[]>>
/**
* Defines the additional login items that should be shown in the login screen
*/

View File

@@ -1,4 +1,9 @@
import { Environment, HoppCollection } from "@hoppscotch/data"
import {
Environment,
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppRESTDocument } from "~/helpers/rest/document"
@@ -9,15 +14,15 @@ import { PersistableTabState } from "~/services/tab"
type VUEX_DATA = {
postwoman: {
settings?: SettingsDef
collections?: HoppCollection[]
collectionsGraphql?: HoppCollection[]
collections?: HoppCollection<HoppRESTRequest>[]
collectionsGraphql?: HoppCollection<HoppGQLRequest>[]
environments?: Environment[]
}
}
const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
export const REST_COLLECTIONS_MOCK: HoppCollection<HoppRESTRequest>[] = [
{
v: 1,
name: "Echo",
@@ -39,7 +44,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [
},
]
export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [
export const GQL_COLLECTIONS_MOCK: HoppCollection<HoppGQLRequest>[] = [
{
v: 1,
name: "Echo",

View File

@@ -3,9 +3,7 @@ import {
GQLHeader,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTAuth,
HoppRESTRequest,
HoppRESTHeaders,
} from "@hoppscotch/data"
import { entityReference } from "verzod"
import { z } from "zod"
@@ -64,18 +62,12 @@ const HoppGQLRequestSchema = entityReference(HoppGQLRequest)
const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppRESTCollectionSchema)),
requests: z.optional(z.array(HoppRESTRequestSchema)),
auth: z.optional(HoppRESTAuth),
headers: z.optional(HoppRESTHeaders),
}).strict()
// @ts-expect-error recursive schema
const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppGQLCollectionSchema)),
requests: z.optional(z.array(HoppGQLRequestSchema)),
auth: z.optional(HoppGQLAuth),
headers: z.optional(z.array(GQLHeader)),
}).strict()
export const VUEX_SCHEMA = z.object({
@@ -284,23 +276,6 @@ const validGqlOperations = [
"authorization",
] as const
const HoppInheritedPropertySchema = z
.object({
auth: z.object({
parentID: z.string(),
parentName: z.string(),
inheritedAuth: z.union([HoppRESTAuth, HoppGQLAuth]),
}),
headers: z.array(
z.object({
parentID: z.string(),
parentName: z.string(),
inheritedHeader: z.union([HoppRESTHeaders, GQLHeader]),
})
),
})
.strict()
export const GQL_TAB_STATE_SCHEMA = z
.object({
lastActiveTabID: z.string(),
@@ -316,7 +291,6 @@ export const GQL_TAB_STATE_SCHEMA = z
response: z.optional(z.nullable(GQLResponseEventSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validGqlOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
})
.strict(),
})
@@ -488,7 +462,6 @@ export const REST_TAB_STATE_SCHEMA = z
testResults: z.optional(z.nullable(HoppTestResultSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validRestOperations)),
inheritedProperties: z.optional(HoppInheritedPropertySchema),
})
.strict(),
})

View File

@@ -10,7 +10,6 @@ import { Ref, computed, effectScope, markRaw, ref, watch } from "vue"
import { getI18n } from "~/modules/i18n"
import MiniSearch from "minisearch"
import {
cascadeParentCollectionForHeaderAuth,
graphqlCollectionStore,
restCollectionStore,
} from "~/newstore/collections"
@@ -230,7 +229,7 @@ export class CollectionsSpotlightSearcherService
private getRESTFolderFromFolderPath(
folderPath: string
): HoppCollection | undefined {
): HoppCollection<HoppRESTRequest> | undefined {
try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
@@ -254,7 +253,7 @@ export class CollectionsSpotlightSearcherService
private getGQLFolderFromFolderPath(
folderPath: string
): HoppCollection | undefined {
): HoppCollection<HoppGQLRequest> | undefined {
try {
const folderIndicies = folderPath.split("/").map((x) => parseInt(x))
@@ -301,15 +300,10 @@ export class CollectionsSpotlightSearcherService
this.restTab.setActiveTab(possibleTab.value.id)
} else {
const req = this.getRESTFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex] as HoppRESTRequest
?.requests[reqIndex]
if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"rest"
)
this.restTab.createNewTab(
{
request: req,
@@ -319,10 +313,6 @@ export class CollectionsSpotlightSearcherService
folderPath: folderPath.join("/"),
requestIndex: reqIndex,
},
inheritedProperties: {
auth,
headers,
},
},
true
)
@@ -332,14 +322,10 @@ export class CollectionsSpotlightSearcherService
const reqIndex = folderPath.pop()!
const req = this.getGQLFolderFromFolderPath(folderPath.join("/"))
?.requests[reqIndex] as HoppGQLRequest
?.requests[reqIndex]
if (!req) return
const { auth, headers } = cascadeParentCollectionForHeaderAuth(
folderPath.join("/"),
"graphql"
)
this.gqlTab.createNewTab({
saveContext: {
originLocation: "user-collection",
@@ -348,10 +334,6 @@ export class CollectionsSpotlightSearcherService
},
request: req,
isDirty: false,
inheritedProperties: {
auth,
headers,
},
})
}
}

View File

@@ -1,48 +1,33 @@
import { InferredEntity, createVersionedEntity } from "verzod"
import { GQL_REQ_SCHEMA_VERSION, HoppGQLRequest, translateToGQLRequest } from "../graphql";
import { HoppRESTRequest, translateToNewRequest } from "../rest";
import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
const CURRENT_COLL_SCHEMA_VER = 1
import { z } from "zod"
import { translateToNewRequest } from "../rest"
import { translateToGQLRequest } from "../graphql"
type SupportedReqTypes =
| HoppRESTRequest
| HoppGQLRequest
const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})
export type HoppCollection<T extends SupportedReqTypes> = {
v: number
name: string
folders: HoppCollection<T>[]
requests: T[]
export const HoppCollection = createVersionedEntity({
latestVersion: 2,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(data) {
const versionCheck = versionedObject.safeParse(data)
if (versionCheck.success) return versionCheck.data.v
// For V1 we have to check the schema
const result = V1_VERSION.schema.safeParse(data)
return result.success ? 0 : null
},
})
export type HoppCollection = InferredEntity<typeof HoppCollection>
export const CollectionSchemaVersion = 2
id?: string // For Firestore ID data
}
/**
* Generates a Collection object. This ignores the version number object
* so it can be incremented independently without updating it everywhere
* @param x The Collection Data
* @returns The final collection
*/
export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
export function makeCollection<T extends SupportedReqTypes>(
x: Omit<HoppCollection<T>, "v">
): HoppCollection<T> {
return {
v: CollectionSchemaVersion,
...x,
v: CURRENT_COLL_SCHEMA_VER,
...x
}
}
@@ -51,23 +36,20 @@ export function makeCollection(x: Omit<HoppCollection, "v">): HoppCollection {
* @param x The collection object to load
* @returns The proper new collection format
*/
export function translateToNewRESTCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
export function translateToNewRESTCollection(
x: any
): HoppCollection<HoppRESTRequest> {
if (x.v && x.v === 1) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewRESTCollection)
const requests = (x.requests ?? []).map(translateToNewRequest)
const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const obj = makeCollection({
const obj = makeCollection<HoppRESTRequest>({
name,
folders,
requests,
auth,
headers,
})
if (x.id) obj.id = x.id
@@ -80,26 +62,24 @@ export function translateToNewRESTCollection(x: any): HoppCollection {
* @param x The collection object to load
* @returns The proper new collection format
*/
export function translateToNewGQLCollection(x: any): HoppCollection {
if (x.v && x.v === CollectionSchemaVersion) return x
export function translateToNewGQLCollection(
x: any
): HoppCollection<HoppGQLRequest> {
if (x.v && x.v === GQL_REQ_SCHEMA_VERSION) return x
// Legacy
const name = x.name ?? "Untitled"
const folders = (x.folders ?? []).map(translateToNewGQLCollection)
const requests = (x.requests ?? []).map(translateToGQLRequest)
const auth = x.auth ?? { authType: "inherit", authActive: true }
const headers = x.headers ?? []
const obj = makeCollection({
const obj = makeCollection<HoppGQLRequest>({
name,
folders,
requests,
auth,
headers,
})
if (x.id) obj.id = x.id
return obj
}

View File

@@ -1,36 +0,0 @@
import { defineVersion, entityReference } from "verzod"
import { z } from "zod"
import { HoppRESTRequest } from "../../rest"
import { HoppGQLRequest } from "../../graphql"
const baseCollectionSchema = z.object({
v: z.literal(1),
id: z.optional(z.string()), // For Firestore ID data
name: z.string(),
requests: z.array(
z.lazy(() =>
z.union([
entityReference(HoppRESTRequest),
entityReference(HoppGQLRequest),
])
)
),
})
type Input = z.input<typeof baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof baseCollectionSchema> & {
folders: Output[]
}
export const V1_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> = baseCollectionSchema.extend({
folders: z.lazy(() => z.array(V1_SCHEMA)),
})
export default defineVersion({
initial: true,
schema: V1_SCHEMA,
})

View File

@@ -1,57 +0,0 @@
import { defineVersion, entityReference } from "verzod"
import { z } from "zod"
import { HoppRESTRequest, HoppRESTAuth } from "../../rest"
import { HoppGQLRequest, HoppGQLAuth, GQLHeader } from "../../graphql"
import { V1_SCHEMA } from "./1"
import { HoppRESTHeaders } from "../../rest/v/1"
const baseCollectionSchema = z.object({
v: z.literal(2),
id: z.optional(z.string()), // For Firestore ID data
name: z.string(),
requests: z.array(
z.lazy(() =>
z.union([
entityReference(HoppRESTRequest),
entityReference(HoppGQLRequest),
])
)
),
auth: z.union([HoppRESTAuth, HoppGQLAuth]),
headers: z.union([HoppRESTHeaders, z.array(GQLHeader)]),
})
type Input = z.input<typeof baseCollectionSchema> & {
folders: Input[]
}
type Output = z.output<typeof baseCollectionSchema> & {
folders: Output[]
}
export const V2_SCHEMA: z.ZodType<Output, z.ZodTypeDef, Input> = baseCollectionSchema.extend({
folders: z.lazy(() => z.array(V2_SCHEMA)),
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
// @ts-expect-error
const result: z.infer<typeof V2_SCHEMA> = {
...old,
v: 2,
auth: {
authActive: true,
authType: "inherit",
},
headers: [],
}
if (old.id) result.id = old.id
return result
},
})

View File

@@ -11,7 +11,6 @@ export {
HoppGQLAuthBearer,
HoppGQLAuthNone,
HoppGQLAuthOAuth2,
HoppGQLAuthInherit,
} from "./v/2"
export const GQL_REQ_SCHEMA_VERSION = 2

View File

@@ -3,7 +3,7 @@ import { defineVersion } from "verzod"
import { GQLHeader, V1_SCHEMA } from "./1"
export const HoppGQLAuthNone = z.object({
authType: z.literal("none"),
authType: z.literal("none")
})
export type HoppGQLAuthNone = z.infer<typeof HoppGQLAuthNone>
@@ -12,7 +12,7 @@ export const HoppGQLAuthBasic = z.object({
authType: z.literal("basic"),
username: z.string().catch(""),
password: z.string().catch(""),
password: z.string().catch("")
})
export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic>
@@ -20,7 +20,7 @@ export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic>
export const HoppGQLAuthBearer = z.object({
authType: z.literal("bearer"),
token: z.string().catch(""),
token: z.string().catch("")
})
export type HoppGQLAuthBearer = z.infer<typeof HoppGQLAuthBearer>
@@ -33,7 +33,7 @@ export const HoppGQLAuthOAuth2 = z.object({
authURL: z.string().catch(""),
accessTokenURL: z.string().catch(""),
clientID: z.string().catch(""),
scope: z.string().catch(""),
scope: z.string().catch("")
})
export type HoppGQLAuthOAuth2 = z.infer<typeof HoppGQLAuthOAuth2>
@@ -43,31 +43,22 @@ export const HoppGQLAuthAPIKey = z.object({
key: z.string().catch(""),
value: z.string().catch(""),
addTo: z.string().catch("Headers"),
addTo: z.string().catch("Headers")
})
export type HoppGQLAuthAPIKey = z.infer<typeof HoppGQLAuthAPIKey>
export const HoppGQLAuthInherit = z.object({
authType: z.literal("inherit"),
})
export type HoppGQLAuthInherit = z.infer<typeof HoppGQLAuthInherit>
export const HoppGQLAuth = z
.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
HoppGQLAuthAPIKey,
HoppGQLAuthInherit,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export const HoppGQLAuth = z.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
HoppGQLAuthAPIKey
]).and(
z.object({
authActive: z.boolean()
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
@@ -81,7 +72,7 @@ const V2_SCHEMA = z.object({
query: z.string(),
variables: z.string(),
auth: HoppGQLAuth,
auth: HoppGQLAuth
})
export default defineVersion({
@@ -94,7 +85,7 @@ export default defineVersion({
auth: {
authActive: true,
authType: "none",
},
}
}
},
}
})

View File

@@ -20,12 +20,10 @@ export {
HoppRESTAuth,
HoppRESTAuthAPIKey,
HoppRESTAuthBasic,
HoppRESTAuthInherit,
HoppRESTAuthBearer,
HoppRESTAuthNone,
HoppRESTAuthOAuth2,
HoppRESTReqBody,
HoppRESTHeaders,
} from "./v/1"
const versionedObject = z.object({

View File

@@ -3,29 +3,27 @@ import { z } from "zod"
import { V0_SCHEMA } from "./0"
export const FormDataKeyValue = z
.object({
key: z.string(),
active: z.boolean(),
})
.and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob).nullable()),
}),
z.object({
isFile: z.literal(false),
value: z.string(),
}),
])
)
export const FormDataKeyValue = z.object({
key: z.string(),
active: z.boolean()
}).and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob).nullable())
}),
z.object({
isFile: z.literal(false),
value: z.string()
})
])
)
export type FormDataKeyValue = z.infer<typeof FormDataKeyValue>
export const HoppRESTReqBodyFormData = z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue),
body: z.array(FormDataKeyValue)
})
export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
@@ -33,11 +31,11 @@ export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
export const HoppRESTReqBody = z.union([
z.object({
contentType: z.literal(null),
body: z.literal(null).catch(null),
body: z.literal(null).catch(null)
}),
z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue).catch([]),
body: z.array(FormDataKeyValue).catch([])
}),
z.object({
contentType: z.union([
@@ -50,14 +48,14 @@ export const HoppRESTReqBody = z.union([
z.literal("text/html"),
z.literal("text/plain"),
]),
body: z.string().catch(""),
}),
body: z.string().catch("")
})
])
export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody>
export const HoppRESTAuthNone = z.object({
authType: z.literal("none"),
authType: z.literal("none")
})
export type HoppRESTAuthNone = z.infer<typeof HoppRESTAuthNone>
@@ -98,26 +96,17 @@ export const HoppRESTAuthAPIKey = z.object({
export type HoppRESTAuthAPIKey = z.infer<typeof HoppRESTAuthAPIKey>
export const HoppRESTAuthInherit = z.object({
authType: z.literal("inherit"),
})
export type HoppRESTAuthInherit = z.infer<typeof HoppRESTAuthInherit>
export const HoppRESTAuth = z
.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthInherit,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey,
])
.and(
z.object({
authActive: z.boolean(),
})
)
export const HoppRESTAuth = z.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey
]).and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
@@ -125,7 +114,7 @@ export const HoppRESTParams = z.array(
z.object({
key: z.string().catch(""),
value: z.string().catch(""),
active: z.boolean().catch(true),
active: z.boolean().catch(true)
})
)
@@ -135,7 +124,7 @@ export const HoppRESTHeaders = z.array(
z.object({
key: z.string().catch(""),
value: z.string().catch(""),
active: z.boolean().catch(true),
active: z.boolean().catch(true)
})
)
@@ -155,21 +144,17 @@ const V1_SCHEMA = z.object({
auth: HoppRESTAuth,
body: HoppRESTReqBody,
body: HoppRESTReqBody
})
function parseRequestBody(
x: z.infer<typeof V0_SCHEMA>
): z.infer<typeof V1_SCHEMA>["body"] {
function parseRequestBody(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["body"] {
return {
contentType: "application/json",
body: x.contentType === "application/json" ? x.rawParams ?? "" : "",
}
}
export function parseOldAuth(
x: z.infer<typeof V0_SCHEMA>
): z.infer<typeof V1_SCHEMA>["auth"] {
export function parseOldAuth(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["auth"] {
if (!x.auth || x.auth === "None")
return {
authType: "none",
@@ -198,16 +183,7 @@ export default defineVersion({
initial: false,
schema: V1_SCHEMA,
up(old: z.infer<typeof V0_SCHEMA>) {
const {
url,
path,
headers,
params,
name,
method,
preRequestScript,
testScript,
} = old
const { url, path, headers, params, name, method, preRequestScript, testScript } = old
const endpoint = `${url}${path}`
const body = parseRequestBody(old)

View File

@@ -13,5 +13,5 @@
"emitDeclarationOnly": true,
"declarationDir": "./dist"
},
"include": ["src/**/*.ts"]
"include": ["src/*.ts"]
}

View File

@@ -10,5 +10,5 @@
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts"]
"include": ["src/*.ts"]
}

View File

@@ -101,7 +101,7 @@ type ExportedUserCollectionGQL = {
function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL"
): HoppCollection {
): HoppCollection<HoppRESTRequest | HoppGQLRequest> {
if (collectionType == "REST") {
const restCollection = collection as ExportedUserCollectionREST
@@ -186,7 +186,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection(
collection,
"REST"
) as HoppCollection
) as HoppCollection<HoppRESTRequest>
)
)
: setGraphqlCollections(
@@ -195,7 +195,7 @@ async function loadUserCollections(collectionType: "REST" | "GQL") {
exportedCollectionToHoppCollection(
collection,
"GQL"
) as HoppCollection
) as HoppCollection<HoppGQLRequest>
)
)
})
@@ -718,7 +718,7 @@ export const def: CollectionsPlatformDef = {
function getCollectionPathFromCollectionID(
collectionID: string,
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
parentPath?: string
): string | null {
for (const collectionIndex in collections) {
@@ -742,7 +742,7 @@ function getCollectionPathFromCollectionID(
function getRequestPathFromRequestID(
requestID: string,
collections: HoppCollection[],
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
parentPath?: string
): { collectionPath: string; requestIndex: number } | null {
for (const collectionIndex in collections) {
@@ -774,7 +774,7 @@ function getRequestPathFromRequestID(
function getRequestIndex(
requestID: string,
parentCollectionPath: string,
collections: HoppCollection[]
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[]
) {
const collection = navigateToFolderWithIndexPath(
collections,

View File

@@ -39,7 +39,7 @@ export const restRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection,
collection: HoppCollection<HoppRESTRequest>,
collectionPath: string,
parentUserCollectionID?: string
) => {

View File

@@ -36,7 +36,7 @@ export const gqlRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection,
collection: HoppCollection<HoppRESTRequest>,
collectionPath: string,
parentUserCollectionID?: string
) => {

View File

@@ -37,8 +37,7 @@
"stream-browserify": "^3.0.0",
"util": "^0.12.5",
"vue": "^3.3.8",
"workbox-window": "^7.0.0",
"zod": "^3.22.4"
"workbox-window": "^7.0.0"
},
"devDependencies": {
"@graphql-codegen/add": "^5.0.0",

View File

@@ -1,14 +1,11 @@
mutation CreateGQLChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
$data: String
) {
createGQLChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
data: $data
) {
id
data
}
}

View File

@@ -1,6 +1,5 @@
mutation CreateGQLRootUserCollection($title: String!, $data: String) {
createGQLRootUserCollection(title: $title, data: $data) {
mutation CreateGQLRootUserCollection($title: String!) {
createGQLRootUserCollection(title: $title) {
id
data
}
}

View File

@@ -1,14 +1,11 @@
mutation CreateRESTChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
$data: String
) {
createRESTChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
data: $data
) {
id
data
}
}

View File

@@ -1,6 +1,5 @@
mutation CreateRESTRootUserCollection($title: String!, $data: String) {
createRESTRootUserCollection(title: $title, data: $data) {
mutation CreateRESTRootUserCollection($title: String!) {
createRESTRootUserCollection(title: $title) {
id
data
}
}

View File

@@ -1,15 +0,0 @@
mutation UpdateUserCollection(
$userCollectionID: ID!
$newTitle: String
$data: String
) {
updateUserCollection(
userCollectionID: $userCollectionID
newTitle: $newTitle
data: $data
) {
id
title
data
}
}

View File

@@ -4,12 +4,10 @@ query GetGQLRootUserCollections {
id
title
type
data
childrenGQL {
id
title
type
data
}
}
}

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