Compare commits
25 Commits
feat/colle
...
improve/pl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
019c2cec46 | ||
|
|
40b9508361 | ||
|
|
85285a5204 | ||
|
|
c0c0c37a67 | ||
|
|
4ac8a117ef | ||
|
|
c1bc430ee6 | ||
|
|
9201aa7d7d | ||
|
|
87395a4553 | ||
|
|
6063c633ee | ||
|
|
7481feb366 | ||
|
|
bdfa14fa54 | ||
|
|
0a61ec2bfe | ||
|
|
2bf0106aa2 | ||
|
|
ab7c29d228 | ||
|
|
d9c75ed79e | ||
|
|
6fa722df7b | ||
|
|
18864bfecf | ||
|
|
95754cb2b4 | ||
|
|
ed2a461dc5 | ||
|
|
8d5a456dbd | ||
|
|
2528bbb92f | ||
|
|
259cd48dbb | ||
|
|
b43531f200 | ||
|
|
26da3e18a9 | ||
|
|
bb4b640e58 |
@@ -32,6 +32,9 @@
|
|||||||
"lint-staged": "12.4.0"
|
"lint-staged": "12.4.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"vue": "3.3.9"
|
||||||
|
},
|
||||||
"packageExtensions": {
|
"packageExtensions": {
|
||||||
"httpsnippet@^3.0.1": {
|
"httpsnippet@^3.0.1": {
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ module.exports = {
|
|||||||
// The glob patterns Jest uses to detect test files
|
// The glob patterns Jest uses to detect test files
|
||||||
testMatch: [
|
testMatch: [
|
||||||
// "**/__tests__/**/*.[jt]s?(x)",
|
// "**/__tests__/**/*.[jt]s?(x)",
|
||||||
"**/src/__tests__/**/*.*.ts",
|
"**/src/__tests__/commands/**/*.*.ts",
|
||||||
],
|
],
|
||||||
|
|
||||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||||
|
|||||||
@@ -19,8 +19,9 @@
|
|||||||
"debugger": "node debugger.js 9999",
|
"debugger": "node debugger.js 9999",
|
||||||
"prepublish": "pnpm exec tsup",
|
"prepublish": "pnpm exec tsup",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
||||||
|
"test": "pnpm run build && jest && rm -rf dist",
|
||||||
"do-typecheck": "pnpm exec tsc --noEmit",
|
"do-typecheck": "pnpm exec tsc --noEmit",
|
||||||
"test": "pnpm run build && jest && rm -rf dist"
|
"do-test": "pnpm test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cli",
|
"cli",
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
|
|||||||
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
|
||||||
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
|
||||||
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
const cmd = `node ./bin/hopp test ${TESTS_PATH} --env ${ENV_PATH}`;
|
||||||
const { error } = await execAsync(cmd);
|
const { error, stdout } = await execAsync(cmd);
|
||||||
|
|
||||||
expect(error).toBeNull();
|
expect(error).toBeNull();
|
||||||
});
|
});
|
||||||
@@ -129,7 +129,6 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
|
|||||||
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
|
||||||
const { stderr } = await execAsync(cmd);
|
const { stderr } = await execAsync(cmd);
|
||||||
const out = getErrorCode(stderr);
|
const out = getErrorCode(stderr);
|
||||||
console.log("invalid value thing", out)
|
|
||||||
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"URL": "https://echo.hoppscotch.io",
|
"URL": "https://echo.hoppscotch.io",
|
||||||
"HOST": "echo.hoppscotch.io",
|
"HOST": "echo.hoppscotch.io",
|
||||||
"X-COUNTRY": "IN",
|
|
||||||
"BODY_VALUE": "body_value",
|
"BODY_VALUE": "body_value",
|
||||||
"BODY_KEY": "body_key"
|
"BODY_KEY": "body_key"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"method": "POST",
|
"method": "POST",
|
||||||
"auth": { "authType": "none", "authActive": true },
|
"auth": { "authType": "none", "authActive": true },
|
||||||
"preRequestScript": "",
|
"preRequestScript": "",
|
||||||
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst X_COUNTRY = pw.env.get(\"X-COUNTRY\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n\t pw.expect(pw.response.body.headers[\"x-country\"]).toBe(X_COUNTRY); \n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
"testScript": "const HOST = pw.env.get(\"HOST\");\nconst UNSET_ENV = pw.env.get(\"UNSET_ENV\");\nconst EXPECTED_URL = \"https://echo.hoppscotch.io\";\nconst URL = pw.env.get(\"URL\");\nconst BODY_VALUE = pw.env.get(\"BODY_VALUE\");\n\n// Check JSON response property\npw.test(\"Check headers properties.\", ()=> {\n pw.expect(pw.response.body.headers.host).toBe(HOST);\n});\n\npw.test(\"Check data properties.\", () => {\n\tconst DATA = pw.response.body.data;\n \n pw.expect(DATA).toBeType(\"string\");\n pw.expect(JSON.parse(DATA).body_key).toBe(BODY_VALUE);\n});\n\npw.test(\"Check request URL.\", () => {\n pw.expect(URL).toBe(EXPECTED_URL);\n})\n\npw.test(\"Check unset ENV.\", () => {\n pw.expect(UNSET_ENV).toBeType(\"undefined\");\n})",
|
||||||
"body": {
|
"body": {
|
||||||
"contentType": "application/json",
|
"contentType": "application/json",
|
||||||
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"
|
||||||
|
|||||||
@@ -6,23 +6,24 @@ import {
|
|||||||
parseTemplateString,
|
parseTemplateString,
|
||||||
parseTemplateStringE,
|
parseTemplateStringE,
|
||||||
} from "@hoppscotch/data";
|
} from "@hoppscotch/data";
|
||||||
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
|
import { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
|
||||||
import { flow, pipe } from "fp-ts/function";
|
|
||||||
import * as TE from "fp-ts/TaskEither";
|
|
||||||
import * as E from "fp-ts/Either";
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray";
|
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
|
import * as E from "fp-ts/Either";
|
||||||
import * as O from "fp-ts/Option";
|
import * as O from "fp-ts/Option";
|
||||||
|
import * as RA from "fp-ts/ReadonlyArray";
|
||||||
|
import * as TE from "fp-ts/TaskEither";
|
||||||
|
import { flow, pipe } from "fp-ts/function";
|
||||||
import * as S from "fp-ts/string";
|
import * as S from "fp-ts/string";
|
||||||
import qs from "qs";
|
import qs from "qs";
|
||||||
|
|
||||||
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
import { EffectiveHoppRESTRequest } from "../interfaces/request";
|
||||||
import { error, HoppCLIError } from "../types/errors";
|
import { HoppCLIError, error } from "../types/errors";
|
||||||
import { HoppEnvs } from "../types/request";
|
import { HoppEnvs } from "../types/request";
|
||||||
import { isHoppCLIError } from "./checks";
|
|
||||||
import { tupleToRecord, arraySort, arrayFlatMap } from "./functions/array";
|
|
||||||
import { toFormData } from "./mutators";
|
|
||||||
import { getEffectiveFinalMetaData } from "./getters";
|
|
||||||
import { PreRequestMetrics } from "../types/response";
|
import { PreRequestMetrics } from "../types/response";
|
||||||
|
import { isHoppCLIError } from "./checks";
|
||||||
|
import { arrayFlatMap, arraySort, tupleToRecord } from "./functions/array";
|
||||||
|
import { getEffectiveFinalMetaData } from "./getters";
|
||||||
|
import { toFormData } from "./mutators";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs pre-request-script runner over given request which extracts set ENVs and
|
* Runs pre-request-script runner over given request which extracts set ENVs and
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { HoppRESTRequest } from "@hoppscotch/data";
|
import { HoppRESTRequest } from "@hoppscotch/data";
|
||||||
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
|
import { TestDescriptor } from "@hoppscotch/js-sandbox";
|
||||||
import { hrtime } from "process";
|
import { runTestScript } from "@hoppscotch/js-sandbox/node";
|
||||||
import { flow, pipe } from "fp-ts/function";
|
|
||||||
import * as RA from "fp-ts/ReadonlyArray";
|
|
||||||
import * as A from "fp-ts/Array";
|
import * as A from "fp-ts/Array";
|
||||||
import * as TE from "fp-ts/TaskEither";
|
import * as RA from "fp-ts/ReadonlyArray";
|
||||||
import * as T from "fp-ts/Task";
|
import * as T from "fp-ts/Task";
|
||||||
|
import * as TE from "fp-ts/TaskEither";
|
||||||
|
import { flow, pipe } from "fp-ts/function";
|
||||||
|
import { hrtime } from "process";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RequestRunnerResponse,
|
RequestRunnerResponse,
|
||||||
TestReport,
|
TestReport,
|
||||||
TestScriptParams,
|
TestScriptParams,
|
||||||
} from "../interfaces/response";
|
} from "../interfaces/response";
|
||||||
import { error, HoppCLIError } from "../types/errors";
|
import { HoppCLIError, error } from "../types/errors";
|
||||||
import { HoppEnvs } from "../types/request";
|
import { HoppEnvs } from "../types/request";
|
||||||
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
|
||||||
import { getDurationInSeconds } from "./getters";
|
import { getDurationInSeconds } from "./getters";
|
||||||
@@ -36,7 +38,7 @@ export const testRunner = (
|
|||||||
pipe(
|
pipe(
|
||||||
TE.of(testScriptData),
|
TE.of(testScriptData),
|
||||||
TE.chain(({ testScript, response, envs }) =>
|
TE.chain(({ testScript, response, envs }) =>
|
||||||
execTestScript(testScript, envs, response)
|
runTestScript(testScript, envs, response)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -69,5 +69,7 @@ module.exports = {
|
|||||||
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
"Do not use 'localStorage' directly. Please use the PersistenceService",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
eqeqeq: 1,
|
||||||
|
"no-else-return": 1,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,4 @@ module.exports = {
|
|||||||
printWidth: 80,
|
printWidth: 80,
|
||||||
useTabs: false,
|
useTabs: false,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
plugins: ["prettier-plugin-tailwindcss"],
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24" class="iconify iconify--fontisto"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width=".88em" height="1em" viewBox="0 0 21 24"><path fill="currentColor" d="M12.731 2.751 17.666 5.6a2.138 2.138 0 1 1 2.07 3.548l-.015.003v5.7a2.14 2.14 0 1 1-2.098 3.502l-.002-.002-4.905 2.832a2.14 2.14 0 1 1-4.079.054l-.004.015-4.941-2.844a2.14 2.14 0 1 1-2.067-3.556l.015-.003V9.15a2.14 2.14 0 1 1 1.58-3.926l-.01-.005c.184.106.342.231.479.376l.001.001 4.938-2.85a2.14 2.14 0 1 1 4.096.021l.004-.015zm-.515.877a.766.766 0 0 1-.057.057l-.001.001 6.461 11.19c.026-.009.056-.016.082-.023V9.146a2.14 2.14 0 0 1-1.555-2.603l-.003.015.019-.072zm-3.015.059-.06-.06-4.946 2.852A2.137 2.137 0 0 1 2.749 9.12l-.015.004-.076.021v5.708l.084.023 6.461-11.19zm2.076.507a2.164 2.164 0 0 1-1.207-.004l.015.004-6.46 11.189c.286.276.496.629.597 1.026l.003.015h12.911c.102-.413.313-.768.599-1.043l.001-.001L11.28 4.194zm.986 16.227 4.917-2.838a1.748 1.748 0 0 1-.038-.142H4.222l-.021.083 4.939 2.852c.39-.403.936-.653 1.54-.653.626 0 1.189.268 1.581.696l.001.002z"/></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1017 B |
1
packages/hoppscotch-common/assets/icons/mqtt.svg
Normal file
1
packages/hoppscotch-common/assets/icons/mqtt.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M10.133 1h4.409a.5.5 0 0 1 .5.5v4.422c0 .026-.035.033-.045.01l-.048-.112a9.095 9.095 0 0 0-4.825-4.776c-.023-.01-.016-.044.01-.044Zm-8.588.275h-.5v1h.5c7.027 0 12.229 5.199 12.229 12.226v.5h1v-.5c0-7.58-5.65-13.226-13.229-13.226Zm.034 4.22h-.5v1h.5c2.361 0 4.348.837 5.744 2.238 1.395 1.401 2.227 3.395 2.227 5.758v.5h1v-.5c0-2.604-.921-4.859-2.52-6.463-1.596-1.605-3.845-2.532-6.45-2.532Zm-.528 8.996v-4.423c0-.041.033-.074.074-.074a4.923 4.923 0 0 1 4.923 4.922.074.074 0 0 1-.074.074H1.551a.5.5 0 0 1-.5-.5Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 684 B |
1
packages/hoppscotch-common/assets/icons/socketio.svg
Normal file
1
packages/hoppscotch-common/assets/icons/socketio.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" fill-rule="evenodd" d="M9.277 2.084a.5.5 0 0 1 .185.607l-2.269 5.5a.5.5 0 0 1-.462.309H3.5a.5.5 0 0 1-.354-.854l5.5-5.5a.5.5 0 0 1 .631-.062ZM4.707 7.5h1.69l1.186-2.875L4.707 7.5Zm2.016 6.416a.5.5 0 0 1-.185-.607l2.269-5.5a.5.5 0 0 1 .462-.309H12.5a.5.5 0 0 1 .354.854l-5.5 5.5a.5.5 0 0 1-.631.062Zm4.57-5.416h-1.69l-1.186 2.875L11.293 8.5Z" clip-rule="evenodd"/><path fill="currentColor" fill-rule="evenodd" d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0Zm-1 0A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 633 B |
1
packages/hoppscotch-common/assets/icons/websocket.svg
Normal file
1
packages/hoppscotch-common/assets/icons/websocket.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 16 16"><path fill="currentColor" d="M1 2h4.257a2.5 2.5 0 0 1 1.768.732L9.293 5 5 9.293 3.732 8.025A2.5 2.5 0 0 1 3 6.257V4H2v2.257a3.5 3.5 0 0 0 1.025 2.475L5 10.707l1.25-1.25 2.396 2.397.708-.708L6.957 8.75 8.75 6.957l2.396 2.397.708-.708L9.457 6.25 10.707 5 7.732 2.025A3.5 3.5 0 0 0 5.257 1H1v1ZM10.646 2.354l2.622 2.62A2.5 2.5 0 0 1 14 6.744V12h1V6.743a3.5 3.5 0 0 0-1.025-2.475l-2.621-2.622-.707.708ZM4.268 13.975l-2.622-2.621.708-.708 2.62 2.622A2.5 2.5 0 0 0 6.744 14H15v1H6.743a3.5 3.5 0 0 1-2.475-1.025Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 610 B |
@@ -29,14 +29,6 @@
|
|||||||
@apply antialiased;
|
@apply antialiased;
|
||||||
accent-color: var(--accent-color);
|
accent-color: var(--accent-color);
|
||||||
font-variant-ligatures: common-ligatures;
|
font-variant-ligatures: common-ligatures;
|
||||||
|
|
||||||
// Colors
|
|
||||||
--info-color: #ec4899;
|
|
||||||
--success-color: #10b981;
|
|
||||||
--blue-color: #3b82f6;
|
|
||||||
--warning-color: #f59e0b;
|
|
||||||
--cl-error-color: #ef4444;
|
|
||||||
--sv-error-color: #dc2626;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -65,7 +57,7 @@ input::placeholder,
|
|||||||
textarea::placeholder,
|
textarea::placeholder,
|
||||||
.cm-placeholder {
|
.cm-placeholder {
|
||||||
@apply text-secondary;
|
@apply text-secondary;
|
||||||
@apply opacity-50;
|
@apply opacity-50 #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
@@ -84,7 +76,7 @@ body {
|
|||||||
@apply font-medium;
|
@apply font-medium;
|
||||||
@apply select-none;
|
@apply select-none;
|
||||||
@apply overflow-x-hidden;
|
@apply overflow-x-hidden;
|
||||||
@apply leading-body;
|
@apply leading-body #{!important};
|
||||||
animation: fade 300ms forwards;
|
animation: fade 300ms forwards;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
@@ -182,7 +174,7 @@ a {
|
|||||||
@apply font-semibold;
|
@apply font-semibold;
|
||||||
@apply px-2 py-1;
|
@apply px-2 py-1;
|
||||||
@apply truncate;
|
@apply truncate;
|
||||||
@apply leading-normal;
|
@apply leading-body;
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
|
|
||||||
kbd {
|
kbd {
|
||||||
@@ -229,7 +221,7 @@ a {
|
|||||||
@apply overflow-y-auto;
|
@apply overflow-y-auto;
|
||||||
@apply text-body text-secondary;
|
@apply text-body text-secondary;
|
||||||
@apply p-2;
|
@apply p-2;
|
||||||
@apply leading-normal;
|
@apply leading-body;
|
||||||
@apply focus:outline-none;
|
@apply focus:outline-none;
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
|
|
||||||
@@ -261,7 +253,7 @@ a {
|
|||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply border-b border-dividerLight;
|
@apply border-b border-dividerLight;
|
||||||
@apply my-2;
|
@apply my-2 #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.heading {
|
||||||
@@ -350,44 +342,28 @@ pre.ace_editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-wrapper {
|
|
||||||
@apply flex flex-1;
|
|
||||||
@apply relative;
|
|
||||||
@apply after:absolute;
|
|
||||||
@apply after:flex;
|
|
||||||
@apply after:inset-y-0;
|
|
||||||
@apply after:items-center;
|
|
||||||
@apply after:justify-center;
|
|
||||||
@apply after:pointer-events-none;
|
|
||||||
@apply after:font-icon;
|
|
||||||
@apply after:text-current;
|
|
||||||
@apply after:right-3;
|
|
||||||
@apply after:content-["\e5cf"];
|
|
||||||
@apply after:text-lg;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-response {
|
.info-response {
|
||||||
color: var(--info-color);
|
color: var(--status-info-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-response {
|
.success-response {
|
||||||
color: var(--success-color);
|
color: var(--status-success-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.redir-response {
|
.redirect-response {
|
||||||
color: var(--warning-color);
|
color: var(--status-redirect-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cl-error-response {
|
.critical-error-response {
|
||||||
color: var(--cl-error-color);
|
color: var(--status-critical-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sv-error-response {
|
.server-error-response {
|
||||||
color: var(--sv-error-color);
|
color: var(--status-server-error-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-data-response {
|
.missing-data-response {
|
||||||
@apply text-secondaryLight;
|
color: var(--status-missing-data-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toasted-container {
|
.toasted-container {
|
||||||
@@ -537,12 +513,12 @@ pre.ace_editor {
|
|||||||
@apply inline-flex;
|
@apply inline-flex;
|
||||||
@apply font-sans;
|
@apply font-sans;
|
||||||
@apply text-tiny;
|
@apply text-tiny;
|
||||||
@apply bg-divider;
|
@apply bg-dividerLight;
|
||||||
@apply rounded;
|
@apply rounded;
|
||||||
@apply ml-2;
|
@apply ml-2;
|
||||||
@apply px-1;
|
@apply px-1;
|
||||||
@apply min-w-5;
|
@apply min-w-[1.25rem];
|
||||||
@apply min-h-5;
|
@apply min-h-[1.25rem];
|
||||||
@apply items-center;
|
@apply items-center;
|
||||||
@apply justify-center;
|
@apply justify-center;
|
||||||
@apply border border-dividerDark;
|
@apply border border-dividerDark;
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
@mixin green-theme {
|
@mixin green-theme {
|
||||||
--accent-color: #10b981;
|
--accent-color: theme("colors.emerald.500");
|
||||||
--accent-light-color: #34d399;
|
--accent-light-color: theme("colors.emerald.400");
|
||||||
--accent-dark-color: #059669;
|
--accent-dark-color: theme("colors.emerald.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #a7f3d0;
|
--gradient-from-color: theme("colors.emerald.400");
|
||||||
--gradient-via-color: #34d399;
|
--gradient-via-color: theme("colors.emerald.500");
|
||||||
--gradient-to-color: #059669;
|
--gradient-to-color: theme("colors.emerald.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin teal-theme {
|
@mixin teal-theme {
|
||||||
--accent-color: #14b8a6;
|
--accent-color: theme("colors.teal.500");
|
||||||
--accent-light-color: #2dd4bf;
|
--accent-light-color: theme("colors.teal.400");
|
||||||
--accent-dark-color: #0d9488;
|
--accent-dark-color: theme("colors.teal.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #99f6e4;
|
--gradient-from-color: theme("colors.teal.400");
|
||||||
--gradient-via-color: #2dd4bf;
|
--gradient-via-color: theme("colors.teal.500");
|
||||||
--gradient-to-color: #0d9488;
|
--gradient-to-color: theme("colors.teal.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin blue-theme {
|
@mixin blue-theme {
|
||||||
--accent-color: #3b82f6;
|
--accent-color: theme("colors.blue.500");
|
||||||
--accent-light-color: #60a5fa;
|
--accent-light-color: theme("colors.blue.400");
|
||||||
--accent-dark-color: #2563eb;
|
--accent-dark-color: theme("colors.blue.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #bfdbfe;
|
--gradient-from-color: theme("colors.blue.400");
|
||||||
--gradient-via-color: #60a5fa;
|
--gradient-via-color: theme("colors.blue.500");
|
||||||
--gradient-to-color: #2563eb;
|
--gradient-to-color: theme("colors.blue.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin indigo-theme {
|
@mixin indigo-theme {
|
||||||
--accent-color: #6366f1;
|
--accent-color: theme("colors.indigo.500");
|
||||||
--accent-light-color: #818cf8;
|
--accent-light-color: theme("colors.indigo.400");
|
||||||
--accent-dark-color: #4f46e5;
|
--accent-dark-color: theme("colors.indigo.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #c7d2fe;
|
--gradient-from-color: theme("colors.indigo.400");
|
||||||
--gradient-via-color: #818cf8;
|
--gradient-via-color: theme("colors.indigo.500");
|
||||||
--gradient-to-color: #4f46e5;
|
--gradient-to-color: theme("colors.indigo.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin purple-theme {
|
@mixin purple-theme {
|
||||||
--accent-color: #8b5cf6;
|
--accent-color: theme("colors.purple.500");
|
||||||
--accent-light-color: #a78bfa;
|
--accent-light-color: theme("colors.purple.400");
|
||||||
--accent-dark-color: #7c3aed;
|
--accent-dark-color: theme("colors.purple.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #ddd6fe;
|
--gradient-from-color: theme("colors.purple.400");
|
||||||
--gradient-via-color: #a78bfa;
|
--gradient-via-color: theme("colors.purple.500");
|
||||||
--gradient-to-color: #7c3aed;
|
--gradient-to-color: theme("colors.purple.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin yellow-theme {
|
@mixin yellow-theme {
|
||||||
--accent-color: #f59e0b;
|
--accent-color: theme("colors.amber.500");
|
||||||
--accent-light-color: #fbbf24;
|
--accent-light-color: theme("colors.amber.400");
|
||||||
--accent-dark-color: #d97706;
|
--accent-dark-color: theme("colors.amber.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #fde68a;
|
--gradient-from-color: theme("colors.amber.400");
|
||||||
--gradient-via-color: #fbbf24;
|
--gradient-via-color: theme("colors.amber.500");
|
||||||
--gradient-to-color: #d97706;
|
--gradient-to-color: theme("colors.amber.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin orange-theme {
|
@mixin orange-theme {
|
||||||
--accent-color: #f97316;
|
--accent-color: theme("colors.orange.500");
|
||||||
--accent-light-color: #fb923c;
|
--accent-light-color: theme("colors.orange.400");
|
||||||
--accent-dark-color: #ea580c;
|
--accent-dark-color: theme("colors.orange.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #fed7aa;
|
--gradient-from-color: theme("colors.orange.400");
|
||||||
--gradient-via-color: #fb923c;
|
--gradient-via-color: theme("colors.orange.500");
|
||||||
--gradient-to-color: #ea580c;
|
--gradient-to-color: theme("colors.orange.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin red-theme {
|
@mixin red-theme {
|
||||||
--accent-color: #ef4444;
|
--accent-color: theme("colors.red.500");
|
||||||
--accent-light-color: #f87171;
|
--accent-light-color: theme("colors.red.400");
|
||||||
--accent-dark-color: #dc2626;
|
--accent-dark-color: theme("colors.red.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #fecaca;
|
--gradient-from-color: theme("colors.red.400");
|
||||||
--gradient-via-color: #f87171;
|
--gradient-via-color: theme("colors.red.500");
|
||||||
--gradient-to-color: #dc2626;
|
--gradient-to-color: theme("colors.red.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin pink-theme {
|
@mixin pink-theme {
|
||||||
--accent-color: #ec4899;
|
--accent-color: theme("colors.pink.500");
|
||||||
--accent-light-color: #f472b6;
|
--accent-light-color: theme("colors.pink.400");
|
||||||
--accent-dark-color: #db2777;
|
--accent-dark-color: theme("colors.pink.600");
|
||||||
--accent-contrast-color: #fff;
|
--accent-contrast-color: theme("colors.white");
|
||||||
--gradient-from-color: #fbcfe8;
|
--gradient-from-color: theme("colors.pink.400");
|
||||||
--gradient-via-color: #f472b6;
|
--gradient-via-color: theme("colors.pink.500");
|
||||||
--gradient-to-color: #db2777;
|
--gradient-to-color: theme("colors.pink.600");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
@mixin base-theme {
|
@mixin base-theme {
|
||||||
--font-sans: "Inter Variable", sans-serif;
|
--font-sans: "Inter Variable", sans-serif;
|
||||||
--font-icon: "Material Symbols Rounded Variable";
|
|
||||||
--font-mono: "Roboto Mono Variable", monospace;
|
--font-mono: "Roboto Mono Variable", monospace;
|
||||||
--font-size-body: 0.75rem;
|
--font-size-body: 0.75rem;
|
||||||
--font-size-tiny: 0.688rem;
|
--font-size-tiny: 0.625rem;
|
||||||
--line-height-body: 1rem;
|
--line-height-body: 1rem;
|
||||||
--upper-primary-sticky-fold: 4.125rem;
|
--upper-primary-sticky-fold: 4.125rem;
|
||||||
--upper-secondary-sticky-fold: 6.188rem;
|
--upper-secondary-sticky-fold: 6.188rem;
|
||||||
--upper-tertiary-sticky-fold: 8.25rem;
|
--upper-tertiary-sticky-fold: 8.25rem;
|
||||||
--upper-fourth-sticky-fold: 10.2rem;
|
--upper-fourth-sticky-fold: 10.2rem;
|
||||||
--upper-mobile-primary-sticky-fold: 6.625rem;
|
--upper-mobile-primary-sticky-fold: 6.75rem;
|
||||||
--upper-mobile-secondary-sticky-fold: 8.688rem;
|
--upper-mobile-secondary-sticky-fold: 8.813rem;
|
||||||
--upper-mobile-sticky-fold: 10.75rem;
|
--upper-mobile-sticky-fold: 10.875rem;
|
||||||
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
--upper-mobile-tertiary-sticky-fold: 8.25rem;
|
||||||
--lower-primary-sticky-fold: 3rem;
|
--lower-primary-sticky-fold: 3rem;
|
||||||
--lower-secondary-sticky-fold: 5.063rem;
|
--lower-secondary-sticky-fold: 5.063rem;
|
||||||
@@ -20,62 +19,122 @@
|
|||||||
--sidebar-primary-sticky-fold: 2rem;
|
--sidebar-primary-sticky-fold: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin light-theme {
|
||||||
|
--primary-color: theme("colors.white");
|
||||||
|
--primary-light-color: theme("colors.gray.50");
|
||||||
|
--primary-dark-color: theme("colors.gray.100");
|
||||||
|
--primary-contrast-color: #fdfdfd;
|
||||||
|
|
||||||
|
--secondary-color: theme("colors.gray.500");
|
||||||
|
--secondary-light-color: theme("colors.gray.400");
|
||||||
|
--secondary-dark-color: theme("colors.gray.900");
|
||||||
|
|
||||||
|
--divider-color: theme("colors.gray.100");
|
||||||
|
--divider-light-color: theme("colors.gray.100");
|
||||||
|
--divider-dark-color: theme("colors.gray.300");
|
||||||
|
|
||||||
|
--banner-info-color: theme("colors.stone.100");
|
||||||
|
--banner-warning-color: theme("colors.yellow.100");
|
||||||
|
--banner-error-color: theme("colors.red.100");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.800");
|
||||||
|
--popover-color: theme("colors.white");
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.green.500");
|
||||||
|
--method-post-color: theme("colors.amber.500");
|
||||||
|
--method-put-color: theme("colors.blue.500");
|
||||||
|
--method-patch-color: theme("colors.purple.500");
|
||||||
|
--method-delete-color: theme("colors.red.500");
|
||||||
|
--method-head-color: theme("colors.lime.500");
|
||||||
|
--method-options-color: theme("colors.pink.500");
|
||||||
|
--method-default-color: theme("colors.gray.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
|
--editor-theme: "textmate";
|
||||||
|
}
|
||||||
|
|
||||||
@mixin dark-theme {
|
@mixin dark-theme {
|
||||||
--primary-color: #181818;
|
--primary-color: #181818;
|
||||||
--primary-light-color: #1c1c1e;
|
--primary-light-color: #1c1c1e;
|
||||||
--primary-dark-color: #262626;
|
--primary-dark-color: theme("colors.neutral.800");
|
||||||
--primary-contrast-color: #171717;
|
--primary-contrast-color: theme("colors.neutral.900");
|
||||||
|
|
||||||
--secondary-color: #a3a3a3;
|
--secondary-color: theme("colors.neutral.400");
|
||||||
--secondary-light-color: #737373;
|
--secondary-light-color: theme("colors.neutral.500");
|
||||||
--secondary-dark-color: #fafafa;
|
--secondary-dark-color: theme("colors.zinc.50");
|
||||||
|
|
||||||
--divider-color: #262626;
|
--divider-color: #1f1f1f;
|
||||||
--divider-light-color: #1f1f1f;
|
--divider-light-color: #1f1f1f;
|
||||||
--divider-dark-color: #2d2d2d;
|
--divider-dark-color: theme("colors.zinc.800");
|
||||||
|
|
||||||
--error-color: #292524;
|
--banner-info-color: theme("colors.stone.800");
|
||||||
--tooltip-color: #f5f5f5;
|
--banner-warning-color: theme("colors.yellow.800");
|
||||||
|
--banner-error-color: theme("colors.red.800");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.100");
|
||||||
--popover-color: #1b1b1b;
|
--popover-color: #1b1b1b;
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.emerald.500");
|
||||||
|
--method-post-color: theme("colors.yellow.500");
|
||||||
|
--method-put-color: theme("colors.sky.500");
|
||||||
|
--method-patch-color: theme("colors.violet.500");
|
||||||
|
--method-delete-color: theme("colors.rose.500");
|
||||||
|
--method-head-color: theme("colors.teal.500");
|
||||||
|
--method-options-color: theme("colors.indigo.500");
|
||||||
|
--method-default-color: theme("colors.neutral.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
--editor-theme: "merbivore_soft";
|
--editor-theme: "merbivore_soft";
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin light-theme {
|
|
||||||
--primary-color: #ffffff;
|
|
||||||
--primary-light-color: #f9fafb;
|
|
||||||
--primary-dark-color: #f3f4f6;
|
|
||||||
--primary-contrast-color: #fdfdfd;
|
|
||||||
|
|
||||||
--secondary-color: #6b7280;
|
|
||||||
--secondary-light-color: #9ca3af;
|
|
||||||
--secondary-dark-color: #111827;
|
|
||||||
|
|
||||||
--divider-color: #f3f4f6;
|
|
||||||
--divider-light-color: #f3f4f6;
|
|
||||||
--divider-dark-color: #d1d5db;
|
|
||||||
|
|
||||||
--error-color: #fef3c7;
|
|
||||||
--tooltip-color: #262626;
|
|
||||||
--popover-color: #ffffff;
|
|
||||||
--editor-theme: "textmate";
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin black-theme {
|
@mixin black-theme {
|
||||||
--primary-color: #0f0f0f;
|
--primary-color: #0f0f0f;
|
||||||
--primary-light-color: #171717;
|
--primary-light-color: theme("colors.neutral.900");
|
||||||
--primary-dark-color: #181818;
|
--primary-dark-color: #181818;
|
||||||
--primary-contrast-color: #0f0f0f;
|
--primary-contrast-color: #0f0f0f;
|
||||||
|
|
||||||
--secondary-color: #a3a3a3;
|
--secondary-color: theme("colors.neutral.400");
|
||||||
--secondary-light-color: #737373;
|
--secondary-light-color: theme("colors.neutral.500");
|
||||||
--secondary-dark-color: #f5f5f5;
|
--secondary-dark-color: theme("colors.neutral.50");
|
||||||
|
|
||||||
--divider-color: #1c1c1e;
|
--divider-color: theme("colors.neutral.900");
|
||||||
--divider-light-color: #181818;
|
--divider-light-color: theme("colors.neutral.900");
|
||||||
--divider-dark-color: #323232;
|
--divider-dark-color: theme("colors.zinc.800");
|
||||||
|
|
||||||
|
--banner-info-color: theme("colors.stone.900");
|
||||||
|
--banner-warning-color: theme("colors.yellow.900");
|
||||||
|
--banner-error-color: theme("colors.red.900");
|
||||||
|
|
||||||
|
--tooltip-color: theme("colors.neutral.100");
|
||||||
|
--popover-color: theme("colors.stone.950");
|
||||||
|
|
||||||
|
--method-get-color: theme("colors.emerald.500");
|
||||||
|
--method-post-color: theme("colors.yellow.500");
|
||||||
|
--method-put-color: theme("colors.sky.500");
|
||||||
|
--method-patch-color: theme("colors.violet.500");
|
||||||
|
--method-delete-color: theme("colors.rose.500");
|
||||||
|
--method-head-color: theme("colors.teal.500");
|
||||||
|
--method-options-color: theme("colors.indigo.500");
|
||||||
|
--method-default-color: theme("colors.zinc.500");
|
||||||
|
|
||||||
|
--status-info-color: theme("colors.blue.500");
|
||||||
|
--status-success-color: theme("colors.green.500");
|
||||||
|
--status-redirect-color: theme("colors.amber.500");
|
||||||
|
--status-critical-error-color: theme("colors.red.500");
|
||||||
|
--status-server-error-color: theme("colors.rose.500");
|
||||||
|
--status-missing-data-color: theme("colors.slate.500");
|
||||||
|
|
||||||
--error-color: #1c1917;
|
|
||||||
--tooltip-color: #f5f5f5;
|
|
||||||
--popover-color: #0f0f0f;
|
|
||||||
--editor-theme: "twilight";
|
--editor-theme: "twilight";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
@mixin dark-editor-theme {
|
@mixin light-editor-theme {
|
||||||
--editor-type-color: #a78bfa;
|
--editor-type-color: theme("colors.violet.600");
|
||||||
--editor-name-color: #60a5fa;
|
--editor-name-color: theme("colors.red.600");
|
||||||
--editor-operator-color: #818cf8;
|
--editor-operator-color: theme("colors.indigo.600");
|
||||||
--editor-invalid-color: #f87171;
|
--editor-invalid-color: theme("colors.red.600");
|
||||||
--editor-separator-color: #9ca3af;
|
--editor-separator-color: theme("colors.gray.600");
|
||||||
--editor-meta-color: #9ca3af;
|
--editor-meta-color: theme("colors.gray.600");
|
||||||
--editor-variable-color: #34d399;
|
--editor-variable-color: theme("colors.emerald.600");
|
||||||
--editor-link-color: #22d3ee;
|
--editor-link-color: theme("colors.cyan.600");
|
||||||
--editor-process-color: #e879f9;
|
--editor-process-color: theme("colors.blue.600");
|
||||||
--editor-constant-color: #a78bfa;
|
--editor-constant-color: theme("colors.fuchsia.600");
|
||||||
--editor-keyword-color: #f472b6;
|
--editor-keyword-color: theme("colors.pink.600");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin light-editor-theme {
|
@mixin dark-editor-theme {
|
||||||
--editor-type-color: #7c3aed;
|
--editor-type-color: theme("colors.violet.400");
|
||||||
--editor-name-color: #dc2626;
|
--editor-name-color: theme("colors.blue.400");
|
||||||
--editor-operator-color: #4f46e5;
|
--editor-operator-color: theme("colors.indigo.400");
|
||||||
--editor-invalid-color: #dc2626;
|
--editor-invalid-color: theme("colors.red.400");
|
||||||
--editor-separator-color: #4b5563;
|
--editor-separator-color: theme("colors.gray.400");
|
||||||
--editor-meta-color: #4b5563;
|
--editor-meta-color: theme("colors.gray.400");
|
||||||
--editor-variable-color: #059669;
|
--editor-variable-color: theme("colors.emerald.400");
|
||||||
--editor-link-color: #0891b2;
|
--editor-link-color: theme("colors.cyan.400");
|
||||||
--editor-process-color: #2563eb;
|
--editor-process-color: theme("colors.fuchsia.400");
|
||||||
--editor-constant-color: #c026d3;
|
--editor-constant-color: theme("colors.violet.400");
|
||||||
--editor-keyword-color: #db2777;
|
--editor-keyword-color: theme("colors.pink.400");
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin black-editor-theme {
|
@mixin black-editor-theme {
|
||||||
--editor-type-color: #a78bfa;
|
--editor-type-color: theme("colors.violet.400");
|
||||||
--editor-name-color: #e879f9;
|
--editor-name-color: theme("colors.fuchsia.400");
|
||||||
--editor-operator-color: #818cf8;
|
--editor-operator-color: theme("colors.indigo.400");
|
||||||
--editor-invalid-color: #f87171;
|
--editor-invalid-color: theme("colors.red.400");
|
||||||
--editor-separator-color: #9ca3af;
|
--editor-separator-color: theme("colors.gray.400");
|
||||||
--editor-meta-color: #9ca3af;
|
--editor-meta-color: theme("colors.gray.400");
|
||||||
--editor-variable-color: #34d399;
|
--editor-variable-color: theme("colors.emerald.400");
|
||||||
--editor-link-color: #22d3ee;
|
--editor-link-color: theme("colors.cyan.400");
|
||||||
--editor-process-color: #a78bfa;
|
--editor-process-color: theme("colors.violet.400");
|
||||||
--editor-constant-color: #60a5fa;
|
--editor-constant-color: theme("colors.blue.400");
|
||||||
--editor-keyword-color: #f472b6;
|
--editor-keyword-color: theme("colors.pink.400");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
"connecting": "Connecting",
|
"connecting": "Connecting",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"create": "Create",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"disconnect": "Disconnect",
|
"disconnect": "Disconnect",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"scroll_to_top": "Scroll to top",
|
"scroll_to_top": "Scroll to top",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
|
"share": "Share",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
"starting": "Starting",
|
"starting": "Starting",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
@@ -78,6 +80,7 @@
|
|||||||
"contact_us": "Contact us",
|
"contact_us": "Contact us",
|
||||||
"cookies": "Cookies",
|
"cookies": "Cookies",
|
||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
|
"copy_interface_type": "Copy interface type",
|
||||||
"copy_user_id": "Copy User Auth Token",
|
"copy_user_id": "Copy User Auth Token",
|
||||||
"developer_option": "Developer options",
|
"developer_option": "Developer options",
|
||||||
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
|
||||||
@@ -93,6 +96,7 @@
|
|||||||
"keyboard_shortcuts": "Keyboard shortcuts",
|
"keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"name": "Hoppscotch",
|
"name": "Hoppscotch",
|
||||||
"new_version_found": "New version found. Refresh to update.",
|
"new_version_found": "New version found. Refresh to update.",
|
||||||
|
"open_in_hoppscotch": "Open in Hoppscotch",
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"proxy_privacy_policy": "Proxy privacy policy",
|
"proxy_privacy_policy": "Proxy privacy policy",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
@@ -187,6 +191,7 @@
|
|||||||
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
"remove_folder": "Are you sure you want to permanently delete this folder?",
|
||||||
"remove_history": "Are you sure you want to permanently delete all history?",
|
"remove_history": "Are you sure you want to permanently delete all history?",
|
||||||
"remove_request": "Are you sure you want to permanently delete this request?",
|
"remove_request": "Are you sure you want to permanently delete this request?",
|
||||||
|
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
|
||||||
"remove_team": "Are you sure you want to delete this team?",
|
"remove_team": "Are you sure you want to delete this team?",
|
||||||
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
"remove_telemetry": "Are you sure you want to opt-out of Telemetry?",
|
||||||
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
"request_change": "Are you sure you want to discard current request, unsaved changes will be lost.",
|
||||||
@@ -228,7 +233,8 @@
|
|||||||
"profile": "Login to view your profile",
|
"profile": "Login to view your profile",
|
||||||
"protocols": "Protocols are empty",
|
"protocols": "Protocols are empty",
|
||||||
"schema": "Connect to a GraphQL endpoint to view schema",
|
"schema": "Connect to a GraphQL endpoint to view schema",
|
||||||
"shortcodes": "Shortcodes are empty",
|
"shared_requests_logout": "Login to view your shared requests or create a new one",
|
||||||
|
"shared_requests": "Shared requests are empty",
|
||||||
"subscription": "Subscriptions are empty",
|
"subscription": "Subscriptions are empty",
|
||||||
"team_name": "Team name empty",
|
"team_name": "Team name empty",
|
||||||
"teams": "You don't belong to any teams",
|
"teams": "You don't belong to any teams",
|
||||||
@@ -268,6 +274,9 @@
|
|||||||
"variable": "Variable",
|
"variable": "Variable",
|
||||||
"variable_list": "Variable List"
|
"variable_list": "Variable List"
|
||||||
},
|
},
|
||||||
|
"graphql_collections": {
|
||||||
|
"title": "GraphQL Collections"
|
||||||
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
"browser_support_sse": "This browser doesn't seems to have Server Sent Events support.",
|
||||||
"check_console_details": "Check console log for details.",
|
"check_console_details": "Check console log for details.",
|
||||||
@@ -303,7 +312,8 @@
|
|||||||
"create_secret_gist": "Create secret Gist",
|
"create_secret_gist": "Create secret Gist",
|
||||||
"gist_created": "Gist created",
|
"gist_created": "Gist created",
|
||||||
"require_github": "Login with GitHub to create secret gist",
|
"require_github": "Login with GitHub to create secret gist",
|
||||||
"title": "Export"
|
"title": "Export",
|
||||||
|
"failed": "Something went wrong while exporting"
|
||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"all": "All",
|
"all": "All",
|
||||||
@@ -340,8 +350,8 @@
|
|||||||
"authorization": "The authorization header will be automatically generated when you send the request.",
|
"authorization": "The authorization header will be automatically generated when you send the request.",
|
||||||
"generate_documentation_first": "Generate documentation first",
|
"generate_documentation_first": "Generate documentation first",
|
||||||
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
"network_fail": "Unable to reach the API endpoint. Check your network connection or select a different Interceptor and try again.",
|
||||||
"offline": "You seem to be offline. Data in this workspace might not be up to date.",
|
"offline": "You're using Hoppscotch offline. Updates will sync when you're online, based on workspace settings.",
|
||||||
"offline_short": "You seem to be offline.",
|
"offline_short": "You're using Hoppscotch offline.",
|
||||||
"post_request_tests": "Test scripts are written in JavaScript, and are run after the response is received.",
|
"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.",
|
"pre_request_script": "Pre-request scripts are written in JavaScript, and are run before the request is sent.",
|
||||||
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
"script_fail": "It seems there is a glitch in the pre-request script. Check the error below and fix the script accordingly.",
|
||||||
@@ -370,6 +380,7 @@
|
|||||||
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
"from_openapi_description": "Import from OpenAPI specification file (YML/JSON)",
|
||||||
"from_postman": "Import from Postman",
|
"from_postman": "Import from Postman",
|
||||||
"from_postman_description": "Import from Postman collection",
|
"from_postman_description": "Import from Postman collection",
|
||||||
|
"from_file": "Import from File",
|
||||||
"from_url": "Import from URL",
|
"from_url": "Import from URL",
|
||||||
"gist_url": "Enter Gist URL",
|
"gist_url": "Enter Gist URL",
|
||||||
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
"import_from_url_invalid_fetch": "Couldn't get data from the url",
|
||||||
@@ -377,7 +388,14 @@
|
|||||||
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
"import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'",
|
||||||
"import_from_url_success": "Collections Imported",
|
"import_from_url_success": "Collections Imported",
|
||||||
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
"json_description": "Import collections from a Hoppscotch Collections JSON file",
|
||||||
"title": "Import"
|
"title": "Import",
|
||||||
|
"hoppscotch_environment": "Hoppscotch Environment",
|
||||||
|
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
|
||||||
|
"postman_environment": "Postman Environment",
|
||||||
|
"postman_environment_description": "Import Postman Environment JSON file",
|
||||||
|
"environments_from_gist": "Import From Gist",
|
||||||
|
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
|
||||||
|
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist"
|
||||||
},
|
},
|
||||||
"inspections": {
|
"inspections": {
|
||||||
"description": "Inspect possible errors",
|
"description": "Inspect possible errors",
|
||||||
@@ -414,7 +432,9 @@
|
|||||||
"close_unsaved_tab": "You have unsaved changes",
|
"close_unsaved_tab": "You have unsaved changes",
|
||||||
"collections": "Collections",
|
"collections": "Collections",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
|
"customize_request": "Customize Request",
|
||||||
"edit_request": "Edit Request",
|
"edit_request": "Edit Request",
|
||||||
|
"share_request": "Share Request",
|
||||||
"import_export": "Import / Export"
|
"import_export": "Import / Export"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
@@ -490,7 +510,6 @@
|
|||||||
"structured": "Structured",
|
"structured": "Structured",
|
||||||
"text": "Text"
|
"text": "Text"
|
||||||
},
|
},
|
||||||
"copy_link": "Copy link",
|
|
||||||
"different_collection": "Cannot reorder requests from different collections",
|
"different_collection": "Cannot reorder requests from different collections",
|
||||||
"duplicated": "Request duplicated",
|
"duplicated": "Request duplicated",
|
||||||
"duration": "Duration",
|
"duration": "Duration",
|
||||||
@@ -523,6 +542,7 @@
|
|||||||
"saved": "Request saved",
|
"saved": "Request saved",
|
||||||
"share": "Share",
|
"share": "Share",
|
||||||
"share_description": "Share Hoppscotch with your friends",
|
"share_description": "Share Hoppscotch with your friends",
|
||||||
|
"share_request": "Share Request",
|
||||||
"stop": "Stop",
|
"stop": "Stop",
|
||||||
"title": "Request",
|
"title": "Request",
|
||||||
"type": "Request type",
|
"type": "Request type",
|
||||||
@@ -603,14 +623,31 @@
|
|||||||
"additional": "Additional Settings",
|
"additional": "Additional Settings",
|
||||||
"verify_email": "Verify email"
|
"verify_email": "Verify email"
|
||||||
},
|
},
|
||||||
"shortcodes": {
|
"shared_requests": {
|
||||||
"actions": "Actions",
|
"button": "Button",
|
||||||
"created_on": "Created on",
|
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
|
||||||
"deleted": "Shortcode deleted",
|
"customize": "Customize",
|
||||||
"method": "Method",
|
"creating_widget": "Creating widget",
|
||||||
"not_found": "Shortcode not found",
|
"copy_html": "Copy HTML",
|
||||||
"short_code": "Short code",
|
"copy_link": "Copy Link",
|
||||||
"url": "URL"
|
"copy_markdown": "Copy Markdown",
|
||||||
|
"deleted": "Shared request deleted",
|
||||||
|
"description": "Select a widget, you can change and customize this later",
|
||||||
|
"embed": "Embed",
|
||||||
|
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
|
||||||
|
"link": "Link",
|
||||||
|
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
|
||||||
|
"modified": "Shared request modified",
|
||||||
|
"not_found": "Shared request not found",
|
||||||
|
"open_new_tab": "Open in new tab",
|
||||||
|
"preview": "Preview",
|
||||||
|
"run_in_hoppscotch": "Run in Hoppscotch",
|
||||||
|
"theme": {
|
||||||
|
"dark": "Dark",
|
||||||
|
"light": "Light",
|
||||||
|
"system": "System",
|
||||||
|
"title": "Theme"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"shortcut": {
|
"shortcut": {
|
||||||
"general": {
|
"general": {
|
||||||
@@ -640,7 +677,6 @@
|
|||||||
"title": "Others"
|
"title": "Others"
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"copy_request_link": "Copy Request Link",
|
|
||||||
"delete_method": "Select DELETE method",
|
"delete_method": "Select DELETE method",
|
||||||
"get_method": "Select GET method",
|
"get_method": "Select GET method",
|
||||||
"head_method": "Select HEAD method",
|
"head_method": "Select HEAD method",
|
||||||
@@ -656,6 +692,7 @@
|
|||||||
"save_to_collections": "Save to Collections",
|
"save_to_collections": "Save to Collections",
|
||||||
"send_request": "Send Request",
|
"send_request": "Send Request",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "Generate code snippet",
|
||||||
|
"share_request": "Share Request",
|
||||||
"title": "Request"
|
"title": "Request"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
@@ -780,6 +817,7 @@
|
|||||||
"connection_failed": "Connection failed",
|
"connection_failed": "Connection failed",
|
||||||
"connection_lost": "Connection lost",
|
"connection_lost": "Connection lost",
|
||||||
"copied_to_clipboard": "Copied to clipboard",
|
"copied_to_clipboard": "Copied to clipboard",
|
||||||
|
"copied_interface_to_clipboard": "Copied {language} interface type to clipboard",
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
"deprecated": "DEPRECATED",
|
"deprecated": "DEPRECATED",
|
||||||
"disabled": "Disabled",
|
"disabled": "Disabled",
|
||||||
@@ -838,6 +876,7 @@
|
|||||||
"queries": "Queries",
|
"queries": "Queries",
|
||||||
"query": "Query",
|
"query": "Query",
|
||||||
"schema": "Schema",
|
"schema": "Schema",
|
||||||
|
"shared_requests": "Shared Requests",
|
||||||
"socketio": "Socket.IO",
|
"socketio": "Socket.IO",
|
||||||
"sse": "SSE",
|
"sse": "SSE",
|
||||||
"tests": "Tests",
|
"tests": "Tests",
|
||||||
|
|||||||
@@ -22,45 +22,41 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^10.1.0",
|
"@apidevtools/swagger-parser": "^10.1.0",
|
||||||
"@codemirror/autocomplete": "^6.10.2",
|
"@codemirror/autocomplete": "^6.11.0",
|
||||||
"@codemirror/commands": "^6.3.0",
|
"@codemirror/commands": "^6.3.0",
|
||||||
"@codemirror/lang-javascript": "^6.2.1",
|
"@codemirror/lang-javascript": "^6.2.1",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "6.9.0",
|
"@codemirror/language": "6.9.2",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.3.3",
|
||||||
"@codemirror/lint": "^6.4.2",
|
"@codemirror/lint": "^6.4.2",
|
||||||
"@codemirror/search": "^6.5.4",
|
"@codemirror/search": "^6.5.4",
|
||||||
"@codemirror/state": "^6.3.1",
|
"@codemirror/state": "^6.3.1",
|
||||||
"@codemirror/view": "^6.22.0",
|
"@codemirror/view": "^6.22.0",
|
||||||
"@fontsource-variable/inter": "^5.0.8",
|
|
||||||
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
|
|
||||||
"@fontsource-variable/roboto-mono": "^5.0.9",
|
|
||||||
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
"@hoppscotch/codemirror-lang-graphql": "workspace:^",
|
||||||
"@hoppscotch/data": "workspace:^",
|
"@hoppscotch/data": "workspace:^",
|
||||||
"@hoppscotch/js-sandbox": "workspace:^",
|
"@hoppscotch/js-sandbox": "workspace:^",
|
||||||
"@hoppscotch/ui": "workspace:^",
|
"@hoppscotch/ui": "workspace:^",
|
||||||
"@hoppscotch/vue-toasted": "^0.1.0",
|
"@hoppscotch/vue-toasted": "^0.1.0",
|
||||||
"@lezer/highlight": "1.1.4",
|
"@lezer/highlight": "1.2.0",
|
||||||
"@urql/core": "^4.1.1",
|
"@unhead/vue": "^1.8.8",
|
||||||
|
"@urql/core": "^4.2.0",
|
||||||
"@urql/devtools": "^2.0.3",
|
"@urql/devtools": "^2.0.3",
|
||||||
"@urql/exchange-auth": "^2.1.6",
|
"@urql/exchange-auth": "^2.1.6",
|
||||||
"@urql/exchange-graphcache": "^6.3.2",
|
"@urql/exchange-graphcache": "^6.3.3",
|
||||||
"@vitejs/plugin-legacy": "^4.1.1",
|
"@vitejs/plugin-legacy": "^4.1.1",
|
||||||
"@vueuse/core": "^10.3.0",
|
"@vueuse/core": "^10.6.1",
|
||||||
"@vueuse/head": "^1.3.1",
|
"acorn-walk": "^8.3.0",
|
||||||
"acorn-walk": "^8.2.0",
|
"axios": "^1.6.2",
|
||||||
"axios": "^1.4.0",
|
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"cookie-es": "^1.0.0",
|
"cookie-es": "^1.0.0",
|
||||||
"dioc": "workspace:^",
|
"dioc": "workspace:^",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"fp-ts": "^2.16.1",
|
"fp-ts": "^2.16.1",
|
||||||
"fuse.js": "^6.6.2",
|
|
||||||
"globalthis": "^1.0.3",
|
"globalthis": "^1.0.3",
|
||||||
"graphql": "^16.8.0",
|
"graphql": "^16.8.1",
|
||||||
"graphql-language-service-interface": "^2.9.1",
|
"graphql-language-service-interface": "^2.10.2",
|
||||||
"graphql-tag": "^2.12.6",
|
"graphql-tag": "^2.12.6",
|
||||||
"httpsnippet": "^3.0.1",
|
"httpsnippet": "^3.0.1",
|
||||||
"insomnia-importers": "^3.6.0",
|
"insomnia-importers": "^3.6.0",
|
||||||
@@ -68,14 +64,15 @@
|
|||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonpath-plus": "^7.2.0",
|
"jsonpath-plus": "^7.2.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lossless-json": "^2.0.11",
|
"lossless-json": "^3.0.2",
|
||||||
"minisearch": "^6.1.0",
|
"minisearch": "^6.3.0",
|
||||||
"nprogress": "^0.2.0",
|
"nprogress": "^0.2.0",
|
||||||
"paho-mqtt": "^1.1.0",
|
"paho-mqtt": "^1.1.0",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"postman-collection": "^4.2.0",
|
"postman-collection": "^4.3.0",
|
||||||
"process": "^0.11.10",
|
"process": "^0.11.10",
|
||||||
"qs": "^6.11.2",
|
"qs": "^6.11.2",
|
||||||
|
"quicktype-core": "^23.0.79",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"set-cookie-parser": "^2.6.0",
|
"set-cookie-parser": "^2.6.0",
|
||||||
"set-cookie-parser-es": "^1.0.5",
|
"set-cookie-parser-es": "^1.0.5",
|
||||||
@@ -89,19 +86,19 @@
|
|||||||
"tern": "^0.24.3",
|
"tern": "^0.24.3",
|
||||||
"timers": "^0.1.1",
|
"timers": "^0.1.1",
|
||||||
"tippy.js": "^6.3.7",
|
"tippy.js": "^6.3.7",
|
||||||
"url": "^0.11.1",
|
"url": "^0.11.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"uuid": "^9.0.0",
|
|
||||||
"verzod": "^0.2.0",
|
"verzod": "^0.2.0",
|
||||||
"vue": "^3.3.4",
|
"uuid": "^9.0.1",
|
||||||
"vue-i18n": "^9.2.2",
|
"vue": "^3.3.8",
|
||||||
"vue-pdf-embed": "^1.1.6",
|
"vue-i18n": "^9.7.1",
|
||||||
"vue-router": "^4.2.4",
|
"vue-pdf-embed": "^1.2.1",
|
||||||
|
"vue-router": "^4.2.5",
|
||||||
"vue-tippy": "6.3.1",
|
"vue-tippy": "6.3.1",
|
||||||
"vuedraggable-es": "^4.1.1",
|
"vuedraggable-es": "^4.1.1",
|
||||||
"wonka": "^6.3.4",
|
"wonka": "^6.3.4",
|
||||||
"workbox-window": "^7.0.0",
|
"workbox-window": "^7.0.0",
|
||||||
"xml-formatter": "^3.5.0",
|
"xml-formatter": "^3.6.0",
|
||||||
"yargs-parser": "^21.1.1",
|
"yargs-parser": "^21.1.1",
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
@@ -113,57 +110,58 @@
|
|||||||
"@graphql-codegen/typed-document-node": "^5.0.1",
|
"@graphql-codegen/typed-document-node": "^5.0.1",
|
||||||
"@graphql-codegen/typescript": "^4.0.1",
|
"@graphql-codegen/typescript": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||||
"@graphql-codegen/typescript-urql-graphcache": "^2.4.5",
|
"@graphql-codegen/typescript-urql-graphcache": "^3.0.0",
|
||||||
"@graphql-codegen/urql-introspection": "^2.2.1",
|
"@graphql-codegen/urql-introspection": "^3.0.0",
|
||||||
"@graphql-typed-document-node/core": "^3.2.0",
|
"@graphql-typed-document-node/core": "^3.2.0",
|
||||||
"@iconify-json/lucide": "^1.1.119",
|
"@iconify-json/lucide": "^1.1.141",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^7.0.0",
|
||||||
"@relmify/jest-fp-ts": "^2.1.1",
|
"@relmify/jest-fp-ts": "^2.1.1",
|
||||||
"@rushstack/eslint-patch": "^1.3.3",
|
"@rushstack/eslint-patch": "^1.6.0",
|
||||||
"@types/har-format": "^1.2.12",
|
"@types/har-format": "^1.2.15",
|
||||||
"@types/js-yaml": "^4.0.5",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/lodash-es": "^4.17.8",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/lossless-json": "^1.0.1",
|
"@types/lossless-json": "^1.0.4",
|
||||||
"@types/nprogress": "^0.2.0",
|
"@types/nprogress": "^0.2.3",
|
||||||
"@types/paho-mqtt": "^1.0.7",
|
"@types/paho-mqtt": "^1.0.10",
|
||||||
"@types/postman-collection": "^3.5.7",
|
"@types/postman-collection": "^3.5.10",
|
||||||
"@types/splitpanes": "^2.2.1",
|
"@types/splitpanes": "^2.2.6",
|
||||||
"@types/uuid": "^9.0.2",
|
"@types/uuid": "^9.0.7",
|
||||||
"@types/yargs-parser": "^21.0.0",
|
"@types/yargs-parser": "^21.0.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.0",
|
"@typescript-eslint/eslint-plugin": "^6.12.0",
|
||||||
"@typescript-eslint/parser": "^6.4.0",
|
"@typescript-eslint/parser": "^6.12.0",
|
||||||
"@vitejs/plugin-vue": "^4.3.1",
|
"@vitejs/plugin-vue": "^4.5.0",
|
||||||
"@vue/compiler-sfc": "^3.3.4",
|
"@vue/compiler-sfc": "^3.3.8",
|
||||||
"@vue/eslint-config-typescript": "^11.0.3",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/runtime-core": "^3.3.4",
|
"@vue/runtime-core": "^3.3.8",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.54.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.1",
|
||||||
"eslint-plugin-vue": "^9.17.0",
|
"eslint-plugin-vue": "^9.18.1",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.6",
|
"prettier": "^3.1.0",
|
||||||
"rollup-plugin-polyfill-node": "^0.12.0",
|
"prettier-plugin-tailwindcss": "^0.5.7",
|
||||||
"sass": "^1.66.0",
|
"rollup-plugin-polyfill-node": "^0.13.0",
|
||||||
|
"sass": "^1.69.5",
|
||||||
"tailwindcss": "^3.3.2",
|
"tailwindcss": "^3.3.2",
|
||||||
"typescript": "^5.1.6",
|
"typescript": "^5.3.2",
|
||||||
"unplugin-fonts": "^1.0.3",
|
"unplugin-fonts": "^1.1.1",
|
||||||
"unplugin-icons": "^0.16.5",
|
"unplugin-icons": "^0.17.4",
|
||||||
"unplugin-vue-components": "^0.25.1",
|
"unplugin-vue-components": "^0.25.2",
|
||||||
"vite": "^4.4.9",
|
"vite": "^4.5.0",
|
||||||
"vite-plugin-checker": "^0.6.1",
|
"vite-plugin-checker": "^0.6.2",
|
||||||
"vite-plugin-fonts": "^0.6.0",
|
"vite-plugin-fonts": "^0.7.0",
|
||||||
"vite-plugin-html-config": "^1.0.11",
|
"vite-plugin-html-config": "^1.0.11",
|
||||||
"vite-plugin-inspect": "^0.7.38",
|
"vite-plugin-inspect": "^0.7.42",
|
||||||
"vite-plugin-pages": "^0.31.0",
|
"vite-plugin-pages": "^0.31.0",
|
||||||
"vite-plugin-pages-sitemap": "^1.6.1",
|
"vite-plugin-pages-sitemap": "^1.6.1",
|
||||||
"vite-plugin-pwa": "^0.16.4",
|
"vite-plugin-pwa": "^0.17.0",
|
||||||
"vite-plugin-vue-layouts": "^0.8.0",
|
"vite-plugin-vue-layouts": "^0.8.0",
|
||||||
"vitest": "^0.34.2",
|
"vitest": "^0.34.6",
|
||||||
"vue-tsc": "^1.8.8"
|
"vue-tsc": "^1.8.22"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
21
packages/hoppscotch-common/src/components.d.ts
vendored
21
packages/hoppscotch-common/src/components.d.ts
vendored
@@ -61,6 +61,7 @@ declare module 'vue' {
|
|||||||
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
|
||||||
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
|
||||||
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
|
||||||
|
Embeds: typeof import('./components/embeds/index.vue')['default']
|
||||||
Environments: typeof import('./components/environments/index.vue')['default']
|
Environments: typeof import('./components/environments/index.vue')['default']
|
||||||
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
|
||||||
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
|
||||||
@@ -108,6 +109,7 @@ declare module 'vue' {
|
|||||||
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
HoppSmartProgressRing: typeof import('@hoppscotch/ui')['HoppSmartProgressRing']
|
||||||
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
HoppSmartRadio: typeof import('@hoppscotch/ui')['HoppSmartRadio']
|
||||||
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
HoppSmartRadioGroup: typeof import('@hoppscotch/ui')['HoppSmartRadioGroup']
|
||||||
|
HoppSmartSelectWrapper: typeof import('@hoppscotch/ui')['HoppSmartSelectWrapper']
|
||||||
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
HoppSmartSlideOver: typeof import('@hoppscotch/ui')['HoppSmartSlideOver']
|
||||||
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
HoppSmartSpinner: typeof import('@hoppscotch/ui')['HoppSmartSpinner']
|
||||||
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
HoppSmartTab: typeof import('@hoppscotch/ui')['HoppSmartTab']
|
||||||
@@ -160,6 +162,14 @@ declare module 'vue' {
|
|||||||
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
|
IconLucideVerified: typeof import('~icons/lucide/verified')['default']
|
||||||
|
IconLucideX: typeof import('~icons/lucide/x')['default']
|
||||||
|
ImportExportBase: typeof import('./components/importExport/Base.vue')['default']
|
||||||
|
ImportExportImportExportList: typeof import('./components/importExport/ImportExportList.vue')['default']
|
||||||
|
ImportExportImportExportSourcesList: typeof import('./components/importExport/ImportExportSourcesList.vue')['default']
|
||||||
|
ImportExportImportExportStepsFileImport: typeof import('./components/importExport/ImportExportSteps/FileImport.vue')['default']
|
||||||
|
ImportExportImportExportStepsMyCollectionImport: typeof import('./components/importExport/ImportExportSteps/MyCollectionImport.vue')['default']
|
||||||
|
ImportExportImportExportStepsUrlImport: typeof import('./components/importExport/ImportExportSteps/UrlImport.vue')['default']
|
||||||
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
InterceptorsErrorPlaceholder: typeof import('./components/interceptors/ErrorPlaceholder.vue')['default']
|
||||||
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
|
||||||
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']
|
||||||
@@ -183,6 +193,16 @@ declare module 'vue' {
|
|||||||
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
SettingsExtension: typeof import('./components/settings/Extension.vue')['default']
|
||||||
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
SettingsProxy: typeof import('./components/settings/Proxy.vue')['default']
|
||||||
|
Share: typeof import('./components/share/index.vue')['default']
|
||||||
|
ShareCreateModal: typeof import('./components/share/CreateModal.vue')['default']
|
||||||
|
ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default']
|
||||||
|
ShareModal: typeof import('./components/share/Modal.vue')['default']
|
||||||
|
ShareRequest: typeof import('./components/share/Request.vue')['default']
|
||||||
|
ShareRequestModal: typeof import('./components/share/RequestModal.vue')['default']
|
||||||
|
ShareShareRequestModal: typeof import('./components/share/ShareRequestModal.vue')['default']
|
||||||
|
ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default']
|
||||||
|
ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default']
|
||||||
|
ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default']
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./../../hoppscotch-ui/src/components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./../../hoppscotch-ui/src/components/smart/AutoComplete.vue')['default']
|
||||||
@@ -203,6 +223,7 @@ declare module 'vue' {
|
|||||||
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
SmartProgressRing: typeof import('./../../hoppscotch-ui/src/components/smart/ProgressRing.vue')['default']
|
||||||
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
SmartRadio: typeof import('./../../hoppscotch-ui/src/components/smart/Radio.vue')['default']
|
||||||
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
SmartRadioGroup: typeof import('./../../hoppscotch-ui/src/components/smart/RadioGroup.vue')['default']
|
||||||
|
SmartSelectWrapper: typeof import('./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue')['default']
|
||||||
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
SmartSlideOver: typeof import('./../../hoppscotch-ui/src/components/smart/SlideOver.vue')['default']
|
||||||
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
SmartSpinner: typeof import('./../../hoppscotch-ui/src/components/smart/Spinner.vue')['default']
|
||||||
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./../../hoppscotch-ui/src/components/smart/Tab.vue')['default']
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:role="bannerRole"
|
:role="bannerRole"
|
||||||
class="flex items-center px-4 py-2 text-tiny"
|
class="flex items-center justify-between px-4 py-2 text-tiny text-secondaryDark"
|
||||||
:class="bannerColor"
|
:class="bannerColor"
|
||||||
>
|
>
|
||||||
<component :is="bannerIcon" class="mr-2 text-white" />
|
<div class="flex items-center">
|
||||||
|
<component :is="bannerIcon" class="mr-2" />
|
||||||
<span class="text-white">
|
<span :class="{ 'hidden sm:inline-flex': banner.alternateText }">
|
||||||
<span v-if="banner.alternateText" class="md:hidden">
|
|
||||||
{{ banner.alternateText(t) }}
|
|
||||||
</span>
|
|
||||||
<span :class="banner.alternateText ? '<md:hidden' : ''">
|
|
||||||
{{ banner.text(t) }}
|
{{ banner.text(t) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
<span v-if="banner.alternateText" class="inline-flex sm:hidden">
|
||||||
|
{{ banner.alternateText(t) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<icon-lucide-x
|
||||||
|
v-if="dismissible"
|
||||||
|
class="opacity-50 hover:cursor-pointer hover:opacity-100"
|
||||||
|
@click="emit('dismiss')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -26,22 +30,32 @@ import IconAlertCircle from "~icons/lucide/alert-circle"
|
|||||||
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
import IconAlertTriangle from "~icons/lucide/alert-triangle"
|
||||||
import IconInfo from "~icons/lucide/info"
|
import IconInfo from "~icons/lucide/info"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
banner: BannerContent
|
defineProps<{
|
||||||
}>()
|
banner: BannerContent
|
||||||
|
dismissible?: boolean
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
dismissible: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "dismiss"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const ariaRoles: Record<BannerType, string> = {
|
const ariaRoles: Record<BannerType, string> = {
|
||||||
error: "alert",
|
|
||||||
warning: "status",
|
|
||||||
info: "status",
|
info: "status",
|
||||||
|
warning: "status",
|
||||||
|
error: "alert",
|
||||||
}
|
}
|
||||||
|
|
||||||
const bgColors: Record<BannerType, string> = {
|
const bgColors: Record<BannerType, string> = {
|
||||||
error: "bg-red-700",
|
info: "bg-bannerInfo",
|
||||||
warning: "bg-yellow-700",
|
warning: "bg-bannerWarning",
|
||||||
info: "bg-stone-800",
|
error: "bg-bannerError",
|
||||||
}
|
}
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
|
|||||||
@@ -2,25 +2,27 @@
|
|||||||
<div>
|
<div>
|
||||||
<header
|
<header
|
||||||
ref="headerRef"
|
ref="headerRef"
|
||||||
class="flex flex-1 flex-shrink-0 items-center justify-between space-x-2 overflow-x-auto overflow-y-hidden px-2 py-2"
|
class="grid grid-cols-5 grid-rows-1 gap-2 overflow-x-auto overflow-y-hidden p-2"
|
||||||
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
|
@mousedown.prevent="platform.ui?.appHeader?.onHeaderAreaClick?.()"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="inline-flex flex-1 items-center justify-start space-x-2"
|
class="col-span-2 flex items-center justify-between space-x-2"
|
||||||
:style="{
|
:style="{
|
||||||
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
paddingTop: platform.ui?.appHeader?.paddingTop?.value,
|
||||||
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
paddingLeft: platform.ui?.appHeader?.paddingLeft?.value,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<div class="flex">
|
||||||
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
<HoppButtonSecondary
|
||||||
:label="t('app.name')"
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
to="/"
|
:label="t('app.name')"
|
||||||
/>
|
to="/"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex flex-1 items-center justify-center space-x-2">
|
<div class="col-span-1 flex items-center justify-between space-x-2">
|
||||||
<button
|
<button
|
||||||
class="flex max-w-[15rem] flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 py-1 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
class="flex h-full flex-1 cursor-text items-center justify-between self-stretch rounded border border-dividerDark bg-primaryDark px-2 text-secondaryLight transition hover:border-dividerDark hover:bg-primaryLight hover:text-secondary focus-visible:border-dividerDark focus-visible:bg-primaryLight focus-visible:text-secondary"
|
||||||
@click="invokeAction('modals.search.toggle')"
|
@click="invokeAction('modals.search.toggle')"
|
||||||
>
|
>
|
||||||
<span class="inline-flex flex-1 items-center">
|
<span class="inline-flex flex-1 items-center">
|
||||||
@@ -32,192 +34,189 @@
|
|||||||
<kbd class="shortcut-key">K</kbd>
|
<kbd class="shortcut-key">K</kbd>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="showInstallButton"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('header.install_pwa')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="installPWA()"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
|
||||||
:title="`${
|
|
||||||
mdAndLarger ? t('support.title') : t('app.options')
|
|
||||||
} <kbd>?</kbd>`"
|
|
||||||
:icon="IconLifeBuoy"
|
|
||||||
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
|
||||||
@click="invokeAction('modals.support.toggle')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="inline-flex flex-1 items-center justify-end space-x-2">
|
<div class="col-span-2 flex items-center justify-between space-x-2">
|
||||||
<div
|
<div class="flex">
|
||||||
v-if="currentUser === null"
|
|
||||||
class="inline-flex items-center space-x-2"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconUploadCloud"
|
v-if="showInstallButton"
|
||||||
:label="t('header.save_workspace')"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 hidden border border-green-600/25 bg-green-500/[.15] !text-green-500 hover:border-green-800/50 hover:bg-green-400/10 focus-visible:border-green-800/50 focus-visible:bg-green-400/10 md:flex"
|
:title="t('header.install_pwa')"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
:icon="IconDownload"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="installPWA()"
|
||||||
/>
|
/>
|
||||||
<HoppButtonPrimary
|
<HoppButtonSecondary
|
||||||
:label="t('header.login')"
|
v-tippy="{ theme: 'tooltip', allowHTML: true }"
|
||||||
@click="invokeAction('modals.login.toggle')"
|
:title="`${
|
||||||
|
mdAndLarger ? t('support.title') : t('app.options')
|
||||||
|
} <kbd>?</kbd>`"
|
||||||
|
:icon="IconLifeBuoy"
|
||||||
|
class="rounded hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
@click="invokeAction('modals.support.toggle')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="inline-flex items-center space-x-2">
|
<div class="flex">
|
||||||
<TeamsMemberStack
|
|
||||||
v-if="
|
|
||||||
workspace.type === 'team' &&
|
|
||||||
selectedTeam &&
|
|
||||||
selectedTeam.teamMembers.length > 1
|
|
||||||
"
|
|
||||||
:team-members="selectedTeam.teamMembers"
|
|
||||||
show-count
|
|
||||||
class="mx-2"
|
|
||||||
@handle-click="handleTeamEdit()"
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
class="flex divide-x divide-green-600/25 rounded border border-green-600/25 bg-green-500/[.15] focus-within:divide-green-800/50 focus-within:border-green-800/50 focus-within:bg-green-400/10 hover:divide-green-800/50 hover:border-green-800/50 hover:bg-green-400/10"
|
v-if="currentUser === null"
|
||||||
|
class="inline-flex items-center space-x-2"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:icon="IconUploadCloud"
|
||||||
:title="t('team.invite_tooltip')"
|
:label="t('header.save_workspace')"
|
||||||
:icon="IconUserPlus"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex"
|
||||||
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500"
|
@click="invokeAction('modals.login.toggle')"
|
||||||
@click="handleInvite()"
|
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonPrimary
|
||||||
|
:label="t('header.login')"
|
||||||
|
class="h-8"
|
||||||
|
@click="invokeAction('modals.login.toggle')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else class="inline-flex items-center space-x-2">
|
||||||
|
<TeamsMemberStack
|
||||||
v-if="
|
v-if="
|
||||||
workspace.type === 'team' &&
|
workspace.type === 'team' &&
|
||||||
selectedTeam &&
|
selectedTeam &&
|
||||||
selectedTeam?.myRole === 'OWNER'
|
selectedTeam.teamMembers.length > 1
|
||||||
"
|
"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
:team-members="selectedTeam.teamMembers"
|
||||||
:title="t('team.edit')"
|
show-count
|
||||||
:icon="IconSettings"
|
class="mx-2"
|
||||||
class="py-1.75 !focus-visible:text-green-600 !hover:text-green-600 !text-green-500"
|
@handle-click="handleTeamEdit()"
|
||||||
@click="handleTeamEdit()"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
<div
|
||||||
<tippy
|
class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20"
|
||||||
interactive
|
>
|
||||||
trigger="click"
|
<HoppButtonSecondary
|
||||||
theme="popover"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:on-shown="() => accountActions.focus()"
|
:title="t('team.invite_tooltip')"
|
||||||
>
|
:icon="IconUserPlus"
|
||||||
<HoppButtonSecondary
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
@click="handleInvite()"
|
||||||
:title="t('workspace.change')"
|
/>
|
||||||
:label="mdAndLarger ? workspaceName : ``"
|
<HoppButtonSecondary
|
||||||
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
v-if="
|
||||||
class="select-wrapper !focus-visible:text-blue-600 !hover:text-blue-600 rounded border border-blue-600/25 bg-blue-500/[.15] py-[0.4375rem] pr-8 !text-blue-500 hover:border-blue-800/50 hover:bg-blue-400/10 focus-visible:border-blue-800/50 focus-visible:bg-blue-400/10"
|
workspace.type === 'team' &&
|
||||||
/>
|
selectedTeam &&
|
||||||
<template #content="{ hide }">
|
selectedTeam?.myRole === 'OWNER'
|
||||||
<div
|
"
|
||||||
ref="accountActions"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
class="flex flex-col focus:outline-none"
|
:title="t('team.edit')"
|
||||||
tabindex="0"
|
:icon="IconSettings"
|
||||||
@keyup.escape="hide()"
|
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 !text-emerald-500"
|
||||||
@click="hide()"
|
@click="handleTeamEdit()"
|
||||||
>
|
/>
|
||||||
<WorkspaceSelector />
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
<span class="px-2">
|
|
||||||
<tippy
|
<tippy
|
||||||
interactive
|
interactive
|
||||||
trigger="click"
|
trigger="click"
|
||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => accountActions.focus()"
|
||||||
>
|
>
|
||||||
<HoppSmartPicture
|
<HoppSmartSelectWrapper
|
||||||
v-if="currentUser.photoURL"
|
class="!text-blue-500 !focus-visible:text-blue-600 !hover:text-blue-600"
|
||||||
v-tippy="{
|
>
|
||||||
theme: 'tooltip',
|
<HoppButtonSecondary
|
||||||
}"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:url="currentUser.photoURL"
|
:title="t('workspace.change')"
|
||||||
:alt="
|
:label="mdAndLarger ? workspaceName : ``"
|
||||||
currentUser.displayName ||
|
:icon="workspace.type === 'personal' ? IconUser : IconUsers"
|
||||||
t('profile.default_hopp_displayname')
|
class="!focus-visible:text-blue-600 !hover:text-blue-600 h-8 rounded border border-blue-600/25 bg-blue-500/10 pr-8 !text-blue-500 hover:border-blue-600/20 hover:bg-blue-600/20 focus-visible:border-blue-600/20 focus-visible:bg-blue-600/20"
|
||||||
"
|
/>
|
||||||
:title="
|
</HoppSmartSelectWrapper>
|
||||||
currentUser.displayName ||
|
|
||||||
currentUser.email ||
|
|
||||||
t('profile.default_hopp_displayname')
|
|
||||||
"
|
|
||||||
indicator
|
|
||||||
:indicator-styles="
|
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<HoppSmartPicture
|
|
||||||
v-else
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
currentUser.displayName ||
|
|
||||||
currentUser.email ||
|
|
||||||
t('profile.default_hopp_displayname')
|
|
||||||
"
|
|
||||||
:initial="currentUser.displayName || currentUser.email"
|
|
||||||
indicator
|
|
||||||
:indicator-styles="
|
|
||||||
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="accountActions"
|
||||||
class="flex flex-col focus:outline-none"
|
class="flex flex-col focus:outline-none"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keyup.p="profile.$el.click()"
|
|
||||||
@keyup.s="settings.$el.click()"
|
|
||||||
@keyup.l="logout.$el.click()"
|
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
|
@click="hide()"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col px-2 text-tiny">
|
<WorkspaceSelector />
|
||||||
<span class="inline-flex truncate font-semibold">
|
|
||||||
{{
|
|
||||||
currentUser.displayName ||
|
|
||||||
t("profile.default_hopp_displayname")
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span class="inline-flex truncate text-secondaryLight">
|
|
||||||
{{ currentUser.email }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="profile"
|
|
||||||
to="/profile"
|
|
||||||
:icon="IconUser"
|
|
||||||
:label="t('navigation.profile')"
|
|
||||||
:shortcut="['P']"
|
|
||||||
@click="hide()"
|
|
||||||
/>
|
|
||||||
<HoppSmartItem
|
|
||||||
ref="settings"
|
|
||||||
to="/settings"
|
|
||||||
:icon="IconSettings"
|
|
||||||
:label="t('navigation.settings')"
|
|
||||||
:shortcut="['S']"
|
|
||||||
@click="hide()"
|
|
||||||
/>
|
|
||||||
<FirebaseLogout
|
|
||||||
ref="logout"
|
|
||||||
:shortcut="['L']"
|
|
||||||
@confirm-logout="hide()"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
</span>
|
<span class="px-2">
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppSmartPicture
|
||||||
|
v-tippy="{
|
||||||
|
theme: 'tooltip',
|
||||||
|
}"
|
||||||
|
:name="currentUser.uid"
|
||||||
|
:title="
|
||||||
|
currentUser.displayName ||
|
||||||
|
currentUser.email ||
|
||||||
|
t('profile.default_hopp_displayname')
|
||||||
|
"
|
||||||
|
indicator
|
||||||
|
:indicator-styles="
|
||||||
|
network.isOnline ? 'bg-green-500' : 'bg-red-500'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.p="profile.$el.click()"
|
||||||
|
@keyup.s="settings.$el.click()"
|
||||||
|
@keyup.l="logout.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col px-2">
|
||||||
|
<span class="inline-flex truncate font-semibold">
|
||||||
|
{{
|
||||||
|
currentUser.displayName ||
|
||||||
|
t("profile.default_hopp_displayname")
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex truncate text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ currentUser.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="profile"
|
||||||
|
to="/profile"
|
||||||
|
:icon="IconUser"
|
||||||
|
:label="t('navigation.profile')"
|
||||||
|
:shortcut="['P']"
|
||||||
|
@click="hide()"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="settings"
|
||||||
|
to="/settings"
|
||||||
|
:icon="IconSettings"
|
||||||
|
:label="t('navigation.settings')"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="hide()"
|
||||||
|
/>
|
||||||
|
<FirebaseLogout
|
||||||
|
ref="logout"
|
||||||
|
:shortcut="['L']"
|
||||||
|
@confirm-logout="hide()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<AppBanner v-if="bannerContent" :banner="bannerContent" />
|
<AppBanner
|
||||||
|
v-if="bannerContent"
|
||||||
|
:banner="bannerContent"
|
||||||
|
:dismissible="true"
|
||||||
|
@dismiss="dismissOfflineBanner"
|
||||||
|
/>
|
||||||
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
<TeamsModal :show="showTeamsModal" @hide-modal="showTeamsModal = false" />
|
||||||
<TeamsInvite
|
<TeamsInvite
|
||||||
v-if="workspace.type === 'team' && workspace.teamID"
|
v-if="workspace.type === 'team' && workspace.teamID"
|
||||||
@@ -233,7 +232,6 @@
|
|||||||
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
@invite-team="inviteTeam(editingTeamName, editingTeamID)"
|
||||||
@refetch-teams="refetchTeams"
|
@refetch-teams="refetchTeams"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
:title="t('confirm.remove_team')"
|
:title="t('confirm.remove_team')"
|
||||||
@@ -293,7 +291,7 @@ const bannerContent = computed(() => banner.content.value?.content)
|
|||||||
let bannerID: number | null = null
|
let bannerID: number | null = null
|
||||||
|
|
||||||
const offlineBanner: BannerContent = {
|
const offlineBanner: BannerContent = {
|
||||||
type: "info",
|
type: "warning",
|
||||||
text: (t) => t("helpers.offline"),
|
text: (t) => t("helpers.offline"),
|
||||||
alternateText: (t) => t("helpers.offline_short"),
|
alternateText: (t) => t("helpers.offline_short"),
|
||||||
score: BANNER_PRIORITY_HIGH,
|
score: BANNER_PRIORITY_HIGH,
|
||||||
@@ -307,13 +305,14 @@ watch(isOnline, () => {
|
|||||||
if (!isOnline.value) {
|
if (!isOnline.value) {
|
||||||
bannerID = banner.showBanner(offlineBanner)
|
bannerID = banner.showBanner(offlineBanner)
|
||||||
return
|
return
|
||||||
} else {
|
}
|
||||||
if (banner.content && bannerID) {
|
if (banner.content && bannerID) {
|
||||||
banner.removeBanner(bannerID)
|
banner.removeBanner(bannerID)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const dismissOfflineBanner = () => banner.removeBanner(bannerID!)
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getProbableUserStream(),
|
platform.auth.getProbableUserStream(),
|
||||||
platform.auth.getProbableUser()
|
platform.auth.getProbableUser()
|
||||||
|
|||||||
@@ -92,9 +92,8 @@ const getHighestSeverity = computed(() => {
|
|||||||
},
|
},
|
||||||
{ severity: 0 }
|
{ severity: 0 }
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return { severity: 0 }
|
|
||||||
}
|
}
|
||||||
|
return { severity: 0 }
|
||||||
})
|
})
|
||||||
|
|
||||||
const severityColor = (severity: number) => {
|
const severityColor = (severity: number) => {
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
v-if="isEmpty(shortcutsResults)"
|
v-if="isEmpty(shortcutsResults)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<template #icon>
|
||||||
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
<details
|
<details
|
||||||
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
v-for="(sectionResults, sectionTitle) in shortcutsResults"
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="entry.icon"
|
:is="entry.icon"
|
||||||
class="svg-icons opacity-50"
|
class="svg-icons opacity-80"
|
||||||
:class="{ 'opacity-100': active }"
|
:class="{ 'opacity-25': active }"
|
||||||
/>
|
/>
|
||||||
<template
|
<template
|
||||||
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
v-if="entry.text.type === 'text' && typeof entry.text.text === 'string'"
|
||||||
@@ -82,9 +82,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const formattedShortcutKeys = computed(
|
const formattedShortcutKeys = computed(
|
||||||
() =>
|
() =>
|
||||||
props.entry.meta?.keyboardShortcut?.map((key) => {
|
props.entry.meta?.keyboardShortcut?.map(
|
||||||
return SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
(key) => SPECIAL_KEY_CHARS[key] ?? capitalize(key)
|
||||||
})
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
name="command"
|
name="command"
|
||||||
:placeholder="`${t('app.type_a_command_search')}`"
|
:placeholder="`${t('app.type_a_command_search')}`"
|
||||||
class="flex flex-1 bg-transparent px-6 py-5 text-base text-secondaryDark"
|
class="flex flex-1 bg-transparent px-6 pt-5 pb-3 text-base text-secondaryDark"
|
||||||
/>
|
/>
|
||||||
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
<HoppSmartSpinner v-if="searchSession?.loading" class="mr-6" />
|
||||||
</div>
|
</div>
|
||||||
@@ -49,13 +49,15 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${search}”`"
|
:text="`${t('state.nothing_found')} ‟${search}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.clear')"
|
||||||
|
outline
|
||||||
|
@click="search = ''"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="search = ''"
|
|
||||||
/>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -112,6 +114,7 @@ import {
|
|||||||
WorkspaceSpotlightSearcherService,
|
WorkspaceSpotlightSearcherService,
|
||||||
} from "~/services/spotlight/searchers/workspace.searcher"
|
} from "~/services/spotlight/searchers/workspace.searcher"
|
||||||
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -141,6 +144,10 @@ useService(WorkspaceSpotlightSearcherService)
|
|||||||
useService(SwitchWorkspaceSpotlightSearcherService)
|
useService(SwitchWorkspaceSpotlightSearcherService)
|
||||||
useService(InterceptorSpotlightSearcherService)
|
useService(InterceptorSpotlightSearcherService)
|
||||||
|
|
||||||
|
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
|
||||||
|
useService(searcher)
|
||||||
|
)
|
||||||
|
|
||||||
const search = ref("")
|
const search = ref("")
|
||||||
|
|
||||||
const searchSession = ref<SpotlightSearchState>()
|
const searchSession = ref<SpotlightSearchState>()
|
||||||
|
|||||||
@@ -14,14 +14,14 @@
|
|||||||
></div>
|
></div>
|
||||||
<div class="relative flex flex-col">
|
<div class="relative flex flex-col">
|
||||||
<div
|
<div
|
||||||
class="z-1 pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
|
class="z-[1] pointer-events-none absolute inset-0 bg-accent opacity-0 transition"
|
||||||
:class="{
|
:class="{
|
||||||
'opacity-25':
|
'opacity-25':
|
||||||
dragging && notSameDestination && notSameParentDestination,
|
dragging && notSameDestination && notSameParentDestination,
|
||||||
}"
|
}"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
class="z-3 group pointer-events-auto relative flex cursor-pointer items-stretch"
|
class="z-[3] group pointer-events-auto relative flex cursor-pointer items-stretch"
|
||||||
:draggable="!hasNoTeamAccess"
|
:draggable="!hasNoTeamAccess"
|
||||||
@dragstart="dragStart"
|
@dragstart="dragStart"
|
||||||
@drop="handelDrop($event)"
|
@drop="handelDrop($event)"
|
||||||
@@ -290,13 +290,13 @@ const collectionIcon = computed(() => {
|
|||||||
if (props.isSelected) return IconCheckCircle
|
if (props.isSelected) return IconCheckCircle
|
||||||
else if (!props.isOpen) return IconFolder
|
else if (!props.isOpen) return IconFolder
|
||||||
else if (props.isOpen) return IconFolderOpen
|
else if (props.isOpen) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const collectionName = computed(() => {
|
const collectionName = computed(() => {
|
||||||
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
if ((props.data as HoppCollection<HoppRESTRequest>).name)
|
||||||
return (props.data as HoppCollection<HoppRESTRequest>).name
|
return (props.data as HoppCollection<HoppRESTRequest>).name
|
||||||
else return (props.data as TeamCollection).title
|
return (props.data as TeamCollection).title
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -424,9 +424,8 @@ const isCollLoading = computed(() => {
|
|||||||
props.data.id
|
props.data.id
|
||||||
) {
|
) {
|
||||||
return collectionMoveLoading.includes(props.data.id)
|
return collectionMoveLoading.includes(props.data.id)
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetDragState = () => {
|
const resetDragState = () => {
|
||||||
|
|||||||
@@ -1,361 +1,568 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="modal.collections"
|
||||||
:title="t('modal.collections')"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-if="importerType !== null"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.go_back')"
|
|
||||||
:icon="IconArrowLeft"
|
|
||||||
@click="resetImport"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="importerType !== null" class="flex flex-col">
|
|
||||||
<div class="flex flex-col pb-4">
|
|
||||||
<div
|
|
||||||
v-for="(step, index) in importerSteps"
|
|
||||||
:key="`step-${index}`"
|
|
||||||
class="flex flex-col space-y-8"
|
|
||||||
>
|
|
||||||
<div v-if="step.name === 'FILE_IMPORT'" class="space-y-4">
|
|
||||||
<p class="flex items-center">
|
|
||||||
<span
|
|
||||||
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
|
|
||||||
:class="{
|
|
||||||
'!text-green-500': hasFile,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<icon-lucide-check-circle class="svg-icons" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ t(`${step.metadata.caption}`) }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
class="ml-10 flex flex-col rounded border border-dashed border-dividerDark"
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
id="inputChooseFileToImportFrom"
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
name="inputChooseFileToImportFrom"
|
|
||||||
type="file"
|
|
||||||
class="cursor-pointer p-4 text-secondary transition file:mr-2 file:cursor-pointer file:rounded file:border-0 file:bg-primaryLight file:px-4 file:py-2 file:text-secondary file:transition hover:text-secondaryDark hover:file:bg-primaryDark hover:file:text-secondaryDark"
|
|
||||||
:accept="step.metadata.acceptedFileTypes"
|
|
||||||
@change="onFileChange"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="step.name === 'URL_IMPORT'" class="space-y-4">
|
|
||||||
<p class="flex items-center">
|
|
||||||
<span
|
|
||||||
class="mr-4 inline-flex flex-shrink-0 items-center justify-center rounded-full border-4 border-primary text-dividerDark"
|
|
||||||
:class="{
|
|
||||||
'!text-green-500': hasGist,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<icon-lucide-check-circle class="svg-icons" />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{{ t(`${step.metadata.caption}`) }}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
<p class="ml-10 flex flex-col">
|
|
||||||
<input
|
|
||||||
v-model="inputChooseGistToImportFrom"
|
|
||||||
type="url"
|
|
||||||
class="input"
|
|
||||||
:placeholder="`${t('import.gist_url')}`"
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="step.name === 'TARGET_MY_COLLECTION'"
|
|
||||||
class="flex flex-col"
|
|
||||||
>
|
|
||||||
<div class="select-wrapper">
|
|
||||||
<select
|
|
||||||
v-model="mySelectedCollectionID"
|
|
||||||
autocomplete="off"
|
|
||||||
class="select"
|
|
||||||
autofocus
|
|
||||||
>
|
|
||||||
<option :key="undefined" :value="undefined" disabled selected>
|
|
||||||
{{ t("collection.select") }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="(collection, collectionIndex) in myCollections"
|
|
||||||
:key="`collection-${collectionIndex}`"
|
|
||||||
:value="collectionIndex"
|
|
||||||
class="bg-primary"
|
|
||||||
>
|
|
||||||
{{ collection.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<HoppButtonPrimary
|
|
||||||
:label="t('import.title')"
|
|
||||||
:disabled="enableImportButton"
|
|
||||||
:loading="importingMyCollections"
|
|
||||||
@click="finishImport"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col">
|
|
||||||
<HoppSmartExpand>
|
|
||||||
<template #body>
|
|
||||||
<HoppSmartItem
|
|
||||||
v-for="(importer, index) in importerModules"
|
|
||||||
:key="`importer-${index}`"
|
|
||||||
:icon="importer.icon"
|
|
||||||
:label="t(`${importer.name}`)"
|
|
||||||
@click="importerType = index"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</HoppSmartExpand>
|
|
||||||
<hr />
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:loading="exportingTeamCollections"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="emit('export-json-collection')"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-if="platform.platformFeatureFlags.exportAsGIST"
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
class="flex"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:loading="creatingGistCollection"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="emit('create-collection-gist')"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconArrowLeft from "~icons/lucide/arrow-left"
|
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
|
||||||
import { computed, PropType, ref, watch } from "vue"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { HoppRESTRequest, HoppCollection } from "@hoppscotch/data"
|
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { UrlSource } from "~/helpers/import-export/import/import-sources/UrlSource"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
import IconFile from "~icons/lucide/file"
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
import {
|
||||||
|
hoppRESTImporter,
|
||||||
|
hoppInsomniaImporter,
|
||||||
|
hoppPostmanImporter,
|
||||||
|
toTeamsImporter,
|
||||||
|
hoppOpenAPIImporter,
|
||||||
|
} from "~/helpers/import-export/import/importers"
|
||||||
|
|
||||||
|
import { defineStep } from "~/composables/step-components"
|
||||||
|
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 { appendRESTCollections, restCollections$ } from "~/newstore/collections"
|
||||||
import { RESTCollectionImporters } from "~/helpers/import-export/import/importers"
|
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
|
||||||
import { StepReturnValue } from "~/helpers/import-export/steps"
|
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
|
||||||
|
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconOpenAPI from "~icons/lucide/file"
|
||||||
|
import IconPostman from "~icons/hopp/postman"
|
||||||
|
import IconInsomnia from "~icons/hopp/insomnia"
|
||||||
|
import IconGithub from "~icons/lucide/github"
|
||||||
|
import IconLink from "~icons/lucide/link"
|
||||||
|
|
||||||
|
import IconUser from "~icons/lucide/user"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
|
||||||
|
import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
|
||||||
|
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
|
import { collectionsGistExporter } from "~/helpers/import-export/export/gistExport"
|
||||||
|
import { myCollectionsExporter } from "~/helpers/import-export/export/myCollections"
|
||||||
|
import { teamCollectionsExporter } from "~/helpers/import-export/export/teamCollections"
|
||||||
|
|
||||||
|
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||||
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
type CollectionType = "team-collections" | "my-collections"
|
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
|
||||||
|
|
||||||
|
type CollectionType =
|
||||||
|
| {
|
||||||
|
type: "team-collections"
|
||||||
|
selectedTeam: SelectedTeam
|
||||||
|
}
|
||||||
|
| { type: "my-collections" }
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
collectionsType: {
|
collectionsType: {
|
||||||
type: String as PropType<CollectionType>,
|
type: Object as PropType<CollectionType>,
|
||||||
default: "my-collections",
|
default: () => ({
|
||||||
|
type: "my-collections",
|
||||||
|
selectedTeam: undefined,
|
||||||
|
}),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
exportingTeamCollections: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
creatingGistCollection: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
importingMyCollections: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
(e: "update-team-collections"): void
|
|
||||||
(e: "export-json-collection"): void
|
|
||||||
(e: "create-collection-gist"): void
|
|
||||||
(e: "import-to-teams", payload: HoppCollection<HoppRESTRequest>[]): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const hasFile = ref(false)
|
|
||||||
const hasGist = ref(false)
|
|
||||||
|
|
||||||
const importerType = ref<number | null>(null)
|
|
||||||
|
|
||||||
const stepResults = ref<StepReturnValue[]>([])
|
|
||||||
|
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
|
||||||
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
|
||||||
const inputChooseGistToImportFrom = ref<string>("")
|
|
||||||
|
|
||||||
const importerModules = computed(() =>
|
|
||||||
RESTCollectionImporters.filter(
|
|
||||||
(i) => i.applicableTo?.includes(props.collectionsType) ?? true
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const importerModule = computed(() => {
|
|
||||||
if (importerType.value === null) return null
|
|
||||||
return importerModules.value[importerType.value]
|
|
||||||
})
|
|
||||||
|
|
||||||
const importerSteps = computed(() => importerModule.value?.steps ?? null)
|
|
||||||
|
|
||||||
const enableImportButton = computed(
|
|
||||||
() => !(stepResults.value.length === importerSteps.value?.length)
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(mySelectedCollectionID, (newValue) => {
|
|
||||||
if (newValue === undefined) return
|
|
||||||
stepResults.value = []
|
|
||||||
stepResults.value.push(newValue)
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(inputChooseGistToImportFrom, (url) => {
|
|
||||||
stepResults.value = []
|
|
||||||
if (url === "") {
|
|
||||||
hasGist.value = false
|
|
||||||
} else {
|
|
||||||
hasGist.value = true
|
|
||||||
stepResults.value.push(inputChooseGistToImportFrom.value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const myCollections = useReadonlyStream(restCollections$, [])
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
const importerAction = async (stepResults: StepReturnValue[]) => {
|
const showImportFailedError = () => {
|
||||||
if (!importerModule.value) return
|
toast.error(t("import.failed"))
|
||||||
|
}
|
||||||
|
|
||||||
pipe(
|
const handleImportToStore = async (
|
||||||
await importerModule.value.importer(stepResults as any)(),
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
E.match(
|
) => {
|
||||||
(err) => {
|
const importResult =
|
||||||
failedImport()
|
props.collectionsType.type === "my-collections"
|
||||||
console.error("error", err)
|
? await importToPersonalWorkspace(collections)
|
||||||
},
|
: await importToTeamsWorkspace(collections)
|
||||||
(result) => {
|
|
||||||
if (props.collectionsType === "team-collections") {
|
|
||||||
emit("import-to-teams", result)
|
|
||||||
} else {
|
|
||||||
appendRESTCollections(result)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
if (E.isRight(importResult)) {
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
toast.success(t("state.file_imported"))
|
||||||
importer: importerModule.value!.name,
|
emit("hide-modal")
|
||||||
platform: "rest",
|
} else {
|
||||||
workspaceType: "personal",
|
toast.error(t("import.failed"))
|
||||||
})
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fileImported()
|
const importToPersonalWorkspace = (
|
||||||
}
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
|
) => {
|
||||||
|
appendRESTCollections(collections)
|
||||||
|
return E.right({
|
||||||
|
success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const importToTeamsWorkspace = async (
|
||||||
|
collections: HoppCollection<HoppRESTRequest>[]
|
||||||
|
) => {
|
||||||
|
if (!hasTeamWriteAccess.value || !selectedTeamID.value) {
|
||||||
|
return E.left({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await toTeamsImporter(
|
||||||
|
JSON.stringify(collections),
|
||||||
|
selectedTeamID.value
|
||||||
|
)()
|
||||||
|
|
||||||
|
return E.isRight(res)
|
||||||
|
? E.right({ success: true })
|
||||||
|
: E.left({
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const isHoppMyCollectionExporterInProgress = ref(false)
|
||||||
|
const isHoppTeamCollectionExporterInProgress = ref(false)
|
||||||
|
const isHoppGistCollectionExporterInProgress = ref(false)
|
||||||
|
|
||||||
|
const isTeamWorkspace = computed(() => {
|
||||||
|
return props.collectionsType.type === "team-collections"
|
||||||
|
})
|
||||||
|
|
||||||
|
const HoppRESTImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_rest",
|
||||||
|
name: "import.from_json",
|
||||||
|
title: "import.from_json_description",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppRESTImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_json",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppMyCollectionImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_my_collection",
|
||||||
|
name: "import.from_my_collections",
|
||||||
|
title: "import.from_my_collections_description",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["team-workspace"],
|
||||||
|
},
|
||||||
|
component: defineStep("my_collection_import", MyCollectionImport, () => ({
|
||||||
|
async onImportFromMyCollection(content) {
|
||||||
|
handleImportToStore([content])
|
||||||
|
|
||||||
|
// our analytics consider this as an export event, so keeping compatibility with that
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "import_to_teams",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppOpenAPIImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_openapi",
|
||||||
|
name: "import.from_openapi",
|
||||||
|
title: "import.from_openapi_description",
|
||||||
|
icon: IconOpenAPI,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
supported_sources: [
|
||||||
|
{
|
||||||
|
id: "file_import",
|
||||||
|
name: "import.from_file",
|
||||||
|
icon: IconFile,
|
||||||
|
step: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json, .yaml, .yml",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppOpenAPIImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_openapi",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "url_import",
|
||||||
|
name: "import.from_url",
|
||||||
|
icon: IconLink,
|
||||||
|
step: UrlSource({
|
||||||
|
caption: "import.from_url",
|
||||||
|
onImportFromURL: async (content) => {
|
||||||
|
const res = await hoppOpenAPIImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_openapi",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppPostmanImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_postman",
|
||||||
|
name: "import.from_postman",
|
||||||
|
title: "import.from_postman_description",
|
||||||
|
icon: IconPostman,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppPostmanImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_postman",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppInsomniaImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_insomnia",
|
||||||
|
name: "import.from_insomnia",
|
||||||
|
title: "import.from_insomnia_description",
|
||||||
|
icon: IconInsomnia,
|
||||||
|
disabled: true,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
caption: "import.from_file",
|
||||||
|
acceptedFileTypes: ".json",
|
||||||
|
onImportFromFile: async (content) => {
|
||||||
|
const res = await hoppInsomniaImporter(content)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_insomnia",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppGistImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_gist",
|
||||||
|
name: "import.from_gist",
|
||||||
|
title: "import.from_gist_description",
|
||||||
|
icon: IconGithub,
|
||||||
|
disabled: true,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace", "url-import"],
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.from_url",
|
||||||
|
onImportFromGist: async (content) => {
|
||||||
|
if (E.isLeft(content)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppRESTImporter(content.right)()
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
platform: "rest",
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
importer: "import.from_gist",
|
||||||
|
workspaceType: isTeamWorkspace.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
showImportFailedError()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppMyCollectionsExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "hopp_my_collections",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
isLoading: isHoppMyCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!myCollections.value.length) {
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppMyCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
myCollectionsExporter(myCollections.value),
|
||||||
|
"Collections"
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
if (E.isRight(message)) {
|
||||||
|
toast.success(t(message.right))
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "json",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppMyCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const finishImport = async () => {
|
const HoppTeamCollectionsExporter: ImporterOrExporter = {
|
||||||
await importerAction(stepResults.value)
|
metadata: {
|
||||||
|
id: "hopp_team_collections",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "export.as_json_description",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["team-workspace"],
|
||||||
|
isLoading: isHoppTeamCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
|
if (
|
||||||
|
props.collectionsType.type === "team-collections" &&
|
||||||
|
props.collectionsType.selectedTeam
|
||||||
|
) {
|
||||||
|
const res = await teamCollectionsExporter(
|
||||||
|
props.collectionsType.selectedTeam.id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (E.isRight(res)) {
|
||||||
|
const { exportCollectionsToJSON } = res.right
|
||||||
|
|
||||||
|
if (!JSON.parse(exportCollectionsToJSON).length) {
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = false
|
||||||
|
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeDownloadCollection(
|
||||||
|
exportCollectionsToJSON,
|
||||||
|
"team-collections"
|
||||||
|
)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "json",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(res.left.error.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppTeamCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const onFileChange = () => {
|
const HoppGistCollectionsExporter: ImporterOrExporter = {
|
||||||
stepResults.value = []
|
metadata: {
|
||||||
|
id: "create_secret_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
|
icon: IconGithub,
|
||||||
|
disabled: !currentUser.value
|
||||||
|
? true
|
||||||
|
: currentUser.value.provider !== "github.com",
|
||||||
|
title: t("export.create_secret_gist"),
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
isLoading: isHoppGistCollectionExporterInProgress,
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
isHoppGistCollectionExporterInProgress.value = true
|
||||||
|
|
||||||
const inputFileToImport = inputChooseFileToImportFrom.value[0]
|
const collectionJSON = await getCollectionJSON()
|
||||||
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
if (!inputFileToImport) {
|
if (!accessToken) {
|
||||||
hasFile.value = false
|
toast.error(t("error.something_went_wrong"))
|
||||||
return
|
isHoppGistCollectionExporterInProgress.value = false
|
||||||
}
|
|
||||||
|
|
||||||
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
|
||||||
inputChooseFileToImportFrom.value[0].value = ""
|
|
||||||
hasFile.value = false
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
if (!content) {
|
|
||||||
hasFile.value = false
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
stepResults.value.push(content)
|
if (E.isRight(collectionJSON)) {
|
||||||
hasFile.value = !!content?.length
|
collectionsGistExporter(collectionJSON.right, accessToken)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
exporter: "gist",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
isHoppGistCollectionExporterInProgress.value = false
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const importerModules = computed(() => {
|
||||||
|
const enabledImporters = [
|
||||||
|
HoppRESTImporter,
|
||||||
|
HoppMyCollectionImporter,
|
||||||
|
HoppOpenAPIImporter,
|
||||||
|
HoppPostmanImporter,
|
||||||
|
HoppInsomniaImporter,
|
||||||
|
HoppGistImporter,
|
||||||
|
]
|
||||||
|
|
||||||
|
const isTeams = props.collectionsType.type === "team-collections"
|
||||||
|
|
||||||
|
return enabledImporters.filter((importer) => {
|
||||||
|
return isTeams
|
||||||
|
? importer.metadata.applicableTo.includes("team-workspace")
|
||||||
|
: importer.metadata.applicableTo.includes("personal-workspace")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const enabledExporters = [
|
||||||
|
HoppMyCollectionsExporter,
|
||||||
|
HoppTeamCollectionsExporter,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
enabledExporters.push(HoppGistCollectionsExporter)
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.readAsText(inputFileToImport.files[0])
|
return enabledExporters.filter((exporter) => {
|
||||||
}
|
return exporter.metadata.applicableTo.includes(
|
||||||
|
props.collectionsType.type === "my-collections"
|
||||||
|
? "personal-workspace"
|
||||||
|
: "team-workspace"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const fileImported = () => {
|
const hasTeamWriteAccess = computed(() => {
|
||||||
toast.success(t("state.file_imported").toString())
|
const { collectionsType } = props
|
||||||
hideModal()
|
|
||||||
}
|
|
||||||
const failedImport = () => {
|
|
||||||
toast.error(t("import.failed").toString())
|
|
||||||
}
|
|
||||||
const hideModal = () => {
|
|
||||||
resetImport()
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetImport = () => {
|
const isTeamCollection = collectionsType.type === "team-collections"
|
||||||
importerType.value = null
|
|
||||||
hasFile.value = false
|
if (!isTeamCollection || !collectionsType.selectedTeam) {
|
||||||
hasGist.value = false
|
return false
|
||||||
stepResults.value = []
|
}
|
||||||
inputChooseFileToImportFrom.value = ""
|
|
||||||
inputChooseGistToImportFrom.value = ""
|
return (
|
||||||
mySelectedCollectionID.value = undefined
|
collectionsType.selectedTeam.myRole === "EDITOR" ||
|
||||||
|
collectionsType.selectedTeam.myRole === "OWNER"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedTeamID = computed(() => {
|
||||||
|
const { collectionsType } = props
|
||||||
|
|
||||||
|
return collectionsType.type === "team-collections"
|
||||||
|
? collectionsType.selectedTeam?.id
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const myCollections = useReadonlyStream(restCollections$, [])
|
||||||
|
|
||||||
|
const getCollectionJSON = async () => {
|
||||||
|
if (
|
||||||
|
props.collectionsType.type === "team-collections" &&
|
||||||
|
props.collectionsType.selectedTeam?.id
|
||||||
|
) {
|
||||||
|
const res = await getTeamCollectionJSON(
|
||||||
|
props.collectionsType.selectedTeam?.id
|
||||||
|
)
|
||||||
|
|
||||||
|
return E.isRight(res)
|
||||||
|
? E.right(res.right.exportCollectionsToJSON)
|
||||||
|
: E.left(res.left)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.collectionsType.type === "my-collections") {
|
||||||
|
return E.right(JSON.stringify(myCollections.value, null, 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return E.left("INVALID_SELECTED_TEAM_OR_INVALID_COLLECTION_TYPE")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -222,6 +222,12 @@
|
|||||||
requestIndex: pathToIndex(node.id),
|
requestIndex: pathToIndex(node.id),
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@share-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('share-request', {
|
||||||
|
request: node.data.data.data,
|
||||||
|
})
|
||||||
|
"
|
||||||
@drag-request="
|
@drag-request="
|
||||||
dragRequest($event, {
|
dragRequest($event, {
|
||||||
folderPath: node.data.data.parentIndex,
|
folderPath: node.data.data.parentIndex,
|
||||||
@@ -248,7 +254,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
@@ -257,27 +263,29 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<template #body>
|
||||||
<span class="text-center text-secondaryLight">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{{ t("collection.import_or_create") }}
|
<span class="text-center text-secondaryLight">
|
||||||
</span>
|
{{ t("collection.import_or_create") }}
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
</span>
|
||||||
<HoppButtonPrimary
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
:icon="IconImport"
|
<HoppButtonPrimary
|
||||||
:label="t('import.title')"
|
:icon="IconImport"
|
||||||
filled
|
:label="t('import.title')"
|
||||||
outline
|
filled
|
||||||
@click="emit('display-modal-import-export')"
|
outline
|
||||||
/>
|
@click="emit('display-modal-import-export')"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
:icon="IconPlus"
|
<HoppButtonSecondary
|
||||||
:label="t('add.new')"
|
:icon="IconPlus"
|
||||||
filled
|
:label="t('add.new')"
|
||||||
outline
|
filled
|
||||||
@click="emit('display-modal-add')"
|
outline
|
||||||
/>
|
@click="emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
@@ -285,18 +293,20 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="t('add.new')"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="t('add.new')"
|
||||||
outline
|
filled
|
||||||
@click="
|
outline
|
||||||
node.data.type === 'collections' &&
|
@click="
|
||||||
emit('add-folder', {
|
node.data.type === 'collections' &&
|
||||||
path: node.id,
|
emit('add-folder', {
|
||||||
folder: node.data.data.data,
|
path: node.id,
|
||||||
})
|
folder: node.data.data.data,
|
||||||
"
|
})
|
||||||
/>
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
@@ -460,6 +470,12 @@ const emit = defineEmits<{
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "share-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "drop-request",
|
event: "drop-request",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -526,13 +542,12 @@ const isSelected = ({
|
|||||||
props.picked.folderPath === folderPath &&
|
props.picked.folderPath === folderPath &&
|
||||||
props.picked.requestIndex === requestIndex
|
props.picked.requestIndex === requestIndex
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "my-folder" &&
|
|
||||||
props.picked.folderPath === folderPath
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "my-folder" &&
|
||||||
|
props.picked.folderPath === folderPath
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const tabs = useService(RESTTabService)
|
const tabs = useService(RESTTabService)
|
||||||
@@ -729,11 +744,10 @@ class MyCollectionsAdapter implements SmartTreeAdapter<MyCollectionNode> {
|
|||||||
status: "loaded",
|
status: "loaded",
|
||||||
data: data,
|
data: data,
|
||||||
} as ChildrenResult<Folder | Requests>
|
} as ChildrenResult<Folder | Requests>
|
||||||
} else {
|
}
|
||||||
return {
|
return {
|
||||||
status: "loaded",
|
status: "loaded",
|
||||||
data: [],
|
data: [],
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
|
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
|
||||||
:class="requestLabelColor"
|
:style="{ color: getMethodLabelColorClassOf(request) }"
|
||||||
:style="{ color: requestLabelColor }"
|
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
:is="IconCheckCircle"
|
:is="IconCheckCircle"
|
||||||
@@ -94,6 +93,7 @@
|
|||||||
@keyup.e="edit?.$el.click()"
|
@keyup.e="edit?.$el.click()"
|
||||||
@keyup.d="duplicate?.$el.click()"
|
@keyup.d="duplicate?.$el.click()"
|
||||||
@keyup.delete="deleteAction?.$el.click()"
|
@keyup.delete="deleteAction?.$el.click()"
|
||||||
|
@keyup.s="shareAction?.$el.click()"
|
||||||
@keyup.escape="hide()"
|
@keyup.escape="hide()"
|
||||||
>
|
>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
@@ -133,6 +133,18 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="shareAction"
|
||||||
|
:icon="IconShare2"
|
||||||
|
:label="t('action.share')"
|
||||||
|
:shortcut="['S']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
emit('share-request')
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</tippy>
|
</tippy>
|
||||||
@@ -162,6 +174,7 @@ import IconEdit from "~icons/lucide/edit"
|
|||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import IconTrash2 from "~icons/lucide/trash-2"
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { ref, PropType, watch, computed } from "vue"
|
import { ref, PropType, watch, computed } from "vue"
|
||||||
import { HoppRESTRequest } from "@hoppscotch/data"
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
@@ -240,6 +253,7 @@ const emit = defineEmits<{
|
|||||||
(event: "duplicate-request"): void
|
(event: "duplicate-request"): void
|
||||||
(event: "remove-request"): void
|
(event: "remove-request"): void
|
||||||
(event: "select-request"): void
|
(event: "select-request"): void
|
||||||
|
(event: "share-request"): void
|
||||||
(event: "drag-request", payload: DataTransfer): void
|
(event: "drag-request", payload: DataTransfer): void
|
||||||
(event: "update-request-order", payload: DataTransfer): void
|
(event: "update-request-order", payload: DataTransfer): void
|
||||||
(event: "update-last-request-order", payload: DataTransfer): void
|
(event: "update-last-request-order", payload: DataTransfer): void
|
||||||
@@ -250,6 +264,7 @@ const edit = ref<HTMLButtonElement | null>(null)
|
|||||||
const deleteAction = ref<HTMLButtonElement | null>(null)
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
const options = ref<TippyComponent | null>(null)
|
const options = ref<TippyComponent | null>(null)
|
||||||
const duplicate = ref<HTMLButtonElement | null>(null)
|
const duplicate = ref<HTMLButtonElement | null>(null)
|
||||||
|
const shareAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
const ordering = ref(false)
|
const ordering = ref(false)
|
||||||
@@ -261,10 +276,6 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
parentID: "",
|
parentID: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestLabelColor = computed(() =>
|
|
||||||
getMethodLabelColorClassOf(props.request)
|
|
||||||
)
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.duplicateLoading,
|
() => props.duplicateLoading,
|
||||||
(val) => {
|
(val) => {
|
||||||
@@ -363,9 +374,8 @@ const updateLastItemOrder = (e: DragEvent) => {
|
|||||||
const isRequestLoading = computed(() => {
|
const isRequestLoading = computed(() => {
|
||||||
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
if (props.requestMoveLoading.length > 0 && props.requestID) {
|
||||||
return props.requestMoveLoading.includes(props.requestID)
|
return props.requestMoveLoading.includes(props.requestID)
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
})
|
})
|
||||||
|
|
||||||
const resetDragState = () => {
|
const resetDragState = () => {
|
||||||
|
|||||||
@@ -141,9 +141,8 @@ const reqName = computed(() => {
|
|||||||
return props.request.name
|
return props.request.name
|
||||||
} else if (props.mode === "rest") {
|
} else if (props.mode === "rest") {
|
||||||
return restRequestName.value
|
return restRequestName.value
|
||||||
} else {
|
|
||||||
return gqlRequestName.value
|
|
||||||
}
|
}
|
||||||
|
return gqlRequestName.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const requestName = ref(reqName.value)
|
const requestName = ref(reqName.value)
|
||||||
@@ -480,21 +479,20 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_coll/short_title":
|
case "team_coll/short_title":
|
||||||
return t("collection.name_length_insufficient")
|
return t("collection.name_length_insufficient")
|
||||||
case "team/invalid_coll_id":
|
case "team/invalid_coll_id":
|
||||||
return t("team.invalid_id")
|
return t("team.invalid_id")
|
||||||
case "team/not_required_role":
|
case "team/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_required_role":
|
case "team_req/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
class="!rounded-none"
|
class="!rounded-none"
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
:title="t('team.no_access')"
|
:title="t('team.no_access')"
|
||||||
:label="t('add.new')"
|
:label="t('action.new')"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
v-else
|
v-else
|
||||||
:icon="IconPlus"
|
:icon="IconPlus"
|
||||||
:label="t('add.new')"
|
:label="t('action.new')"
|
||||||
class="!rounded-none"
|
class="!rounded-none"
|
||||||
@click="emit('display-modal-add')"
|
@click="emit('display-modal-add')"
|
||||||
/>
|
/>
|
||||||
@@ -240,6 +240,12 @@
|
|||||||
requestIndex: node.data.data.data.id,
|
requestIndex: node.data.data.data.id,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@share-request="
|
||||||
|
node.data.type === 'requests' &&
|
||||||
|
emit('share-request', {
|
||||||
|
request: node.data.data.data.request,
|
||||||
|
})
|
||||||
|
"
|
||||||
@drag-request="
|
@drag-request="
|
||||||
dragRequest($event, {
|
dragRequest($event, {
|
||||||
folderPath: node.data.data.parentIndex,
|
folderPath: node.data.data.parentIndex,
|
||||||
@@ -268,33 +274,37 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
@drop.stop
|
@drop.stop
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<template #body>
|
||||||
<span class="text-center text-secondaryLight">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{{ t("collection.import_or_create") }}
|
<span class="text-center text-secondaryLight">
|
||||||
</span>
|
{{ t("collection.import_or_create") }}
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
</span>
|
||||||
<HoppButtonPrimary
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
:icon="IconImport"
|
<HoppButtonPrimary
|
||||||
:label="t('import.title')"
|
:icon="IconImport"
|
||||||
filled
|
:label="t('import.title')"
|
||||||
outline
|
filled
|
||||||
:disabled="hasNoTeamAccess"
|
outline
|
||||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
:disabled="hasNoTeamAccess"
|
||||||
@click="
|
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||||
hasNoTeamAccess ? null : emit('display-modal-import-export')
|
@click="
|
||||||
"
|
hasNoTeamAccess
|
||||||
/>
|
? null
|
||||||
<HoppButtonSecondary
|
: emit('display-modal-import-export')
|
||||||
:icon="IconPlus"
|
"
|
||||||
:label="t('add.new')"
|
/>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
outline
|
:icon="IconPlus"
|
||||||
:disabled="hasNoTeamAccess"
|
:label="t('add.new')"
|
||||||
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
filled
|
||||||
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
outline
|
||||||
/>
|
:disabled="hasNoTeamAccess"
|
||||||
|
:title="hasNoTeamAccess ? t('team.no_access') : ''"
|
||||||
|
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'collections'"
|
v-else-if="node.data.type === 'collections'"
|
||||||
@@ -303,18 +313,20 @@
|
|||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
@drop.stop
|
@drop.stop
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="t('add.new')"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="t('add.new')"
|
||||||
outline
|
filled
|
||||||
@click="
|
outline
|
||||||
node.data.type === 'collections' &&
|
@click="
|
||||||
emit('add-folder', {
|
node.data.type === 'collections' &&
|
||||||
path: node.id,
|
emit('add-folder', {
|
||||||
folder: node.data.data.data,
|
path: node.id,
|
||||||
})
|
folder: node.data.data.data,
|
||||||
"
|
})
|
||||||
/>
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="node.data.type === 'folders'"
|
v-else-if="node.data.type === 'folders'"
|
||||||
@@ -473,6 +485,12 @@ const emit = defineEmits<{
|
|||||||
folderPath?: string | undefined
|
folderPath?: string | undefined
|
||||||
}
|
}
|
||||||
): void
|
): void
|
||||||
|
(
|
||||||
|
event: "share-request",
|
||||||
|
payload: {
|
||||||
|
request: HoppRESTRequest
|
||||||
|
}
|
||||||
|
): void
|
||||||
(
|
(
|
||||||
event: "drop-request",
|
event: "drop-request",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -542,13 +560,12 @@ const isSelected = ({
|
|||||||
props.picked.pickedType === "teams-request" &&
|
props.picked.pickedType === "teams-request" &&
|
||||||
props.picked.requestID === requestID
|
props.picked.requestID === requestID
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
props.picked &&
|
|
||||||
props.picked.pickedType === "teams-folder" &&
|
|
||||||
props.picked.folderID === folderID
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
props.picked &&
|
||||||
|
props.picked.pickedType === "teams-folder" &&
|
||||||
|
props.picked.folderID === folderID
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
const active = computed(() => tabs.currentActiveTab.value.document.saveContext)
|
||||||
@@ -714,82 +731,78 @@ class TeamCollectionsAdapter implements SmartTreeAdapter<TeamCollectionNode> {
|
|||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
const data = this.data.value.map((item, index) => ({
|
const data = this.data.value.map((item, index) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
data: {
|
||||||
|
isLastItem: index === this.data.value.length - 1,
|
||||||
|
type: "collections",
|
||||||
data: {
|
data: {
|
||||||
isLastItem: index === this.data.value.length - 1,
|
parentIndex: null,
|
||||||
type: "collections",
|
data: item,
|
||||||
data: {
|
|
||||||
parentIndex: null,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}))
|
},
|
||||||
return {
|
}))
|
||||||
status: "loaded",
|
return {
|
||||||
data: cloneDeep(data),
|
status: "loaded",
|
||||||
} as ChildrenResult<TeamCollections>
|
data: cloneDeep(data),
|
||||||
}
|
} as ChildrenResult<TeamCollections>
|
||||||
} else {
|
}
|
||||||
const parsedID = id.split("/")[id.split("/").length - 1]
|
const parsedID = id.split("/")[id.split("/").length - 1]
|
||||||
|
|
||||||
!props.teamLoadingCollections.includes(parsedID) &&
|
!props.teamLoadingCollections.includes(parsedID) &&
|
||||||
emit("expand-team-collection", parsedID)
|
emit("expand-team-collection", parsedID)
|
||||||
|
|
||||||
if (props.teamLoadingCollections.includes(parsedID)) {
|
if (props.teamLoadingCollections.includes(parsedID)) {
|
||||||
return {
|
return {
|
||||||
status: "loading",
|
status: "loading",
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const items = this.findCollInTree(this.data.value, parsedID)
|
|
||||||
if (items) {
|
|
||||||
const data = [
|
|
||||||
...(items.children
|
|
||||||
? items.children.map((item, index) => ({
|
|
||||||
id: `${id}/${item.id}`,
|
|
||||||
data: {
|
|
||||||
isLastItem:
|
|
||||||
items.children && items.children.length > 1
|
|
||||||
? index === items.children.length - 1
|
|
||||||
: false,
|
|
||||||
type: "folders",
|
|
||||||
data: {
|
|
||||||
parentIndex: parsedID,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
...(items.requests
|
|
||||||
? items.requests.map((item, index) => ({
|
|
||||||
id: `${id}/${item.id}`,
|
|
||||||
data: {
|
|
||||||
isLastItem:
|
|
||||||
items.requests && items.requests.length > 1
|
|
||||||
? index === items.requests.length - 1
|
|
||||||
: false,
|
|
||||||
type: "requests",
|
|
||||||
data: {
|
|
||||||
parentIndex: parsedID,
|
|
||||||
data: item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
: []),
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
status: "loaded",
|
|
||||||
data: cloneDeep(data),
|
|
||||||
} as ChildrenResult<TeamFolder | TeamRequests>
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
status: "loaded",
|
|
||||||
data: [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const items = this.findCollInTree(this.data.value, parsedID)
|
||||||
|
if (items) {
|
||||||
|
const data = [
|
||||||
|
...(items.children
|
||||||
|
? items.children.map((item, index) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
isLastItem:
|
||||||
|
items.children && items.children.length > 1
|
||||||
|
? index === items.children.length - 1
|
||||||
|
: false,
|
||||||
|
type: "folders",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
...(items.requests
|
||||||
|
? items.requests.map((item, index) => ({
|
||||||
|
id: `${id}/${item.id}`,
|
||||||
|
data: {
|
||||||
|
isLastItem:
|
||||||
|
items.requests && items.requests.length > 1
|
||||||
|
? index === items.requests.length - 1
|
||||||
|
: false,
|
||||||
|
type: "requests",
|
||||||
|
data: {
|
||||||
|
parentIndex: parsedID,
|
||||||
|
data: item,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: []),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: cloneDeep(data),
|
||||||
|
} as ChildrenResult<TeamFolder | TeamRequests>
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: "loaded",
|
||||||
|
data: [],
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,16 +180,18 @@
|
|||||||
:alt="`${t('empty.collection')}`"
|
:alt="`${t('empty.collection')}`"
|
||||||
:text="t('empty.collection')"
|
:text="t('empty.collection')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="t('add.new')"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="t('add.new')"
|
||||||
outline
|
filled
|
||||||
@click="
|
outline
|
||||||
emit('add-folder', {
|
@click="
|
||||||
path: `${collectionIndex}`,
|
emit('add-folder', {
|
||||||
})
|
path: `${collectionIndex}`,
|
||||||
"
|
})
|
||||||
/>
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -271,7 +273,7 @@ const collectionIcon = computed(() => {
|
|||||||
if (isSelected.value) return IconCheckCircle
|
if (isSelected.value) return IconCheckCircle
|
||||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||||
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
|
else if (!showChildren.value || props.isFiltered) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
|
|||||||
@@ -176,8 +176,7 @@
|
|||||||
:src="`/images/states/${colorMode.value}/pack.svg`"
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
:alt="`${t('empty.folder')}`"
|
:alt="`${t('empty.folder')}`"
|
||||||
:text="t('empty.folder')"
|
:text="t('empty.folder')"
|
||||||
>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
@@ -253,7 +252,7 @@ const collectionIcon = computed(() => {
|
|||||||
if (isSelected.value) return IconCheckCircle
|
if (isSelected.value) return IconCheckCircle
|
||||||
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
else if (!showChildren.value && !props.isFiltered) return IconFolder
|
||||||
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
|
else if (showChildren.value || !props.isFiltered) return IconFolderOpen
|
||||||
else return IconFolder
|
return IconFolder
|
||||||
})
|
})
|
||||||
|
|
||||||
const pick = () => {
|
const pick = () => {
|
||||||
|
|||||||
@@ -1,299 +1,227 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="graphql_collections.title"
|
||||||
:title="`${t('modal.collections')}`"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<span>
|
|
||||||
<tippy interactive trigger="click" theme="popover">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.more')"
|
|
||||||
:icon="IconMoreVertical"
|
|
||||||
:on-shown="() => tippyActions.focus()"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('import.from_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
readCollectionGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
createCollectionGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('import.from_json')"
|
|
||||||
@click="openDialogChooseFileToImportFrom"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
class="input"
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
@change="importFromJSON"
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="exportJSON"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import axios from "axios"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import { useToast } from "~/composables/toast"
|
||||||
|
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"
|
||||||
|
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
import IconDownload from "~icons/lucide/download"
|
import IconUser from "~icons/lucide/user"
|
||||||
import IconGithub from "~icons/lucide/github"
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
import { computed, ref } from "vue"
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import {
|
import {
|
||||||
graphqlCollections$,
|
graphqlCollections$,
|
||||||
setGraphqlCollections,
|
setGraphqlCollections,
|
||||||
appendGraphqlCollections,
|
|
||||||
} from "~/newstore/collections"
|
} 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"
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
show: boolean
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const collections = useReadonlyStream(graphqlCollections$, [])
|
const toast = useToast()
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template refs
|
const GqlCollectionsHoppImporter: ImporterOrExporter = {
|
||||||
const tippyActions = ref<any | null>(null)
|
metadata: {
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
id: "import.from_json",
|
||||||
|
name: "import.from_json",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.from_json_description",
|
||||||
|
onImportFromFile: async (gqlCollections) => {
|
||||||
|
const res = await hoppGqlCollectionsImporter(gqlCollections)
|
||||||
|
|
||||||
const collectionJson = computed(() => {
|
if (E.isLeft(res)) {
|
||||||
return JSON.stringify(collections.value, null, 2)
|
showImportFailedError()
|
||||||
})
|
return
|
||||||
|
|
||||||
const createCollectionGist = async () => {
|
|
||||||
if (!currentUser.value) {
|
|
||||||
toast.error(t("profile.no_permission").toString())
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await axios.post(
|
|
||||||
"https://api.github.com/gists",
|
|
||||||
{
|
|
||||||
files: {
|
|
||||||
"hoppscotch-collections.json": {
|
|
||||||
content: collectionJson.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${currentUser.value.accessToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
workspaceType: "personal",
|
||||||
|
importer: "json",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const GqlCollectionsGistImporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "import.from_gist",
|
||||||
|
name: "import.from_gist",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_gist",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.gql_collections_from_gist_description",
|
||||||
|
onImportFromGist: async (gqlCollections) => {
|
||||||
|
if (E.isLeft(gqlCollections)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppGqlCollectionsImporter(gqlCollections.right)
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
workspaceType: "personal",
|
||||||
|
importer: "gist",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const gqlCollections = useReadonlyStream(graphqlCollections$, [])
|
||||||
|
|
||||||
|
const GqlCollectionsHoppExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_json",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!gqlCollections.value.length) {
|
||||||
|
return toast.error(t("error.no_collections_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
gqlCollectionsExporter(gqlCollections.value),
|
||||||
|
"GQLCollections"
|
||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
if (E.isLeft(message)) {
|
||||||
window.open(res.data.html_url)
|
toast.error(t("export.failed"))
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileImported = () => {
|
|
||||||
toast.success(t("state.file_imported").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const failedImport = () => {
|
|
||||||
toast.error(t("import.failed").toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
const readCollectionGist = async () => {
|
|
||||||
const gist = prompt(t("import.gist_url").toString())
|
|
||||||
if (!gist) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { files } = (await axios.get(
|
|
||||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)) as {
|
|
||||||
files: {
|
|
||||||
[fileName: string]: {
|
|
||||||
content: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const collections = JSON.parse(Object.values(files)[0].content)
|
|
||||||
setGraphqlCollections(collections)
|
|
||||||
fileImported()
|
|
||||||
} catch (e) {
|
|
||||||
failedImport()
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDialogChooseFileToImportFrom = () => {
|
|
||||||
if (inputChooseFileToImportFrom.value)
|
|
||||||
inputChooseFileToImportFrom.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importFromJSON = () => {
|
|
||||||
if (!inputChooseFileToImportFrom.value) return
|
|
||||||
|
|
||||||
if (
|
|
||||||
!inputChooseFileToImportFrom.value.files ||
|
|
||||||
inputChooseFileToImportFrom.value.files.length === 0
|
|
||||||
) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const collections = JSON.parse(content)
|
toast.success(message.right)
|
||||||
if (collections[0]) {
|
|
||||||
const [name, folders, requests] = Object.keys(collections[0])
|
|
||||||
if (name === "name" && folders === "folders" && requests === "requests") {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
failedImport()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
appendGraphqlCollections(collections)
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_IMPORT_COLLECTION",
|
|
||||||
importer: "json",
|
|
||||||
workspaceType: "personal",
|
|
||||||
platform: "gql",
|
|
||||||
})
|
|
||||||
|
|
||||||
fileImported()
|
|
||||||
}
|
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
|
||||||
inputChooseFileToImportFrom.value.value = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportJSON = async () => {
|
|
||||||
const dataToWrite = collectionJson.value
|
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(dataToWrite)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
const result = await platform.io.saveFileWithDialog({
|
|
||||||
data: dataToWrite,
|
|
||||||
contentType: "application/json",
|
|
||||||
suggestedFilename: filename,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "Hoppscotch Collection JSON file",
|
|
||||||
extensions: ["json"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.type === "unknown" || result.type === "saved") {
|
|
||||||
platform?.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
exporter: "json",
|
|
||||||
platform: "gql",
|
platform: "gql",
|
||||||
|
exporter: "json",
|
||||||
})
|
})
|
||||||
|
},
|
||||||
toast.success(t("state.download_started").toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GqlCollectionsGistExporter: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
|
title: !currentUser
|
||||||
|
? "export.require_github"
|
||||||
|
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
currentUser.provider !== "github.com"
|
||||||
|
? `export.require_github`
|
||||||
|
: "export.create_secret_gist",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: !currentUser.value
|
||||||
|
? true
|
||||||
|
: currentUser.value.provider !== "github.com",
|
||||||
|
applicableTo: ["personal-workspace"],
|
||||||
|
},
|
||||||
|
action: async () => {
|
||||||
|
if (!currentUser.value) {
|
||||||
|
toast.error(t("profile.no_permission"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
const res = await gqlCollectionsGistExporter(
|
||||||
|
JSON.stringify(gqlCollections.value),
|
||||||
|
accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
toast.error(t("export.failed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t("export.success"))
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_COLLECTION",
|
||||||
|
platform: "gql",
|
||||||
|
exporter: "gist",
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(res.right, "_blank")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const importerModules = [GqlCollectionsHoppImporter, GqlCollectionsGistImporter]
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const modules = [GqlCollectionsHoppExporter]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
modules.push(GqlCollectionsGistExporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return modules
|
||||||
|
})
|
||||||
|
|
||||||
|
const showImportFailedError = () => {
|
||||||
|
toast.error(t("import.failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportToStore = async (
|
||||||
|
gqlCollections: HoppCollection<HoppGQLRequest>[]
|
||||||
|
) => {
|
||||||
|
setGraphqlCollections(gqlCollections)
|
||||||
|
toast.success(t("import.success"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
class="!border-0 bg-transparent py-2 pl-4 pr-2"
|
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
|
||||||
@@ -66,34 +66,36 @@
|
|||||||
:alt="`${t('empty.collections')}`"
|
:alt="`${t('empty.collections')}`"
|
||||||
:text="t('empty.collections')"
|
:text="t('empty.collections')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<template #body>
|
||||||
<span class="text-center text-secondaryLight">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{{ t("collection.import_or_create") }}
|
<span class="text-center text-secondaryLight">
|
||||||
</span>
|
{{ t("collection.import_or_create") }}
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
</span>
|
||||||
<HoppButtonPrimary
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
:icon="IconImport"
|
<HoppButtonPrimary
|
||||||
:label="t('import.title')"
|
:icon="IconImport"
|
||||||
filled
|
:label="t('import.title')"
|
||||||
outline
|
filled
|
||||||
@click="displayModalImportExport(true)"
|
outline
|
||||||
/>
|
@click="displayModalImportExport(true)"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
:label="t('add.new')"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="t('add.new')"
|
||||||
outline
|
filled
|
||||||
:icon="IconPlus"
|
outline
|
||||||
@click="displayModalAdd(true)"
|
:icon="IconPlus"
|
||||||
/>
|
@click="displayModalAdd(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
|
||||||
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<CollectionsGraphqlAdd
|
<CollectionsGraphqlAdd
|
||||||
@@ -137,7 +139,7 @@
|
|||||||
@hide-modal="displayModalEditRequest(false)"
|
@hide-modal="displayModalEditRequest(false)"
|
||||||
/>
|
/>
|
||||||
<CollectionsGraphqlImportExport
|
<CollectionsGraphqlImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
@dragend="draggingToRoot = false"
|
@dragend="draggingToRoot = false"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
|
class="sticky z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary border-b border-dividerLight"
|
||||||
:class="{ 'rounded-t': saveRequest }"
|
:class="{ 'rounded-t': saveRequest }"
|
||||||
:style="
|
:style="
|
||||||
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
saveRequest ? 'top: calc(-1 * var(--line-height-body))' : 'top: 0'
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
v-model="filterTexts"
|
v-model="filterTexts"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="flex h-8 w-full bg-transparent p-4 py-2"
|
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||||
:placeholder="t('action.search')"
|
:placeholder="t('action.search')"
|
||||||
:disabled="collectionsType.type === 'team-collections'"
|
:disabled="collectionsType.type === 'team-collections'"
|
||||||
/>
|
/>
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@remove-folder="removeFolder"
|
@remove-folder="removeFolder"
|
||||||
|
@share-request="shareRequest"
|
||||||
@drop-collection="dropCollection"
|
@drop-collection="dropCollection"
|
||||||
@update-request-order="updateRequestOrder"
|
@update-request-order="updateRequestOrder"
|
||||||
@update-collection-order="updateCollectionOrder"
|
@update-collection-order="updateCollectionOrder"
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
@export-data="exportData"
|
@export-data="exportData"
|
||||||
@remove-collection="removeCollection"
|
@remove-collection="removeCollection"
|
||||||
@remove-folder="removeFolder"
|
@remove-folder="removeFolder"
|
||||||
|
@share-request="shareRequest"
|
||||||
@edit-request="editRequest"
|
@edit-request="editRequest"
|
||||||
@duplicate-request="duplicateRequest"
|
@duplicate-request="duplicateRequest"
|
||||||
@remove-request="removeRequest"
|
@remove-request="removeRequest"
|
||||||
@@ -138,17 +140,13 @@
|
|||||||
@hide-modal="showConfirmModal = false"
|
@hide-modal="showConfirmModal = false"
|
||||||
@resolve="resolveConfirmModal"
|
@resolve="resolveConfirmModal"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CollectionsImportExport
|
<CollectionsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
:collections-type="collectionsType.type"
|
:collections-type="collectionsType"
|
||||||
:exporting-team-collections="exportingTeamCollections"
|
|
||||||
:creating-gist-collection="creatingGistCollection"
|
|
||||||
:importing-my-collections="importingMyCollections"
|
|
||||||
@export-json-collection="exportJSONCollection"
|
|
||||||
@create-collection-gist="createCollectionGist"
|
|
||||||
@import-to-teams="importToTeams"
|
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TeamsAdd
|
<TeamsAdd
|
||||||
:show="showTeamModalAdd"
|
:show="showTeamModalAdd"
|
||||||
@hide-modal="displayTeamModalAdd(false)"
|
@hide-modal="displayTeamModalAdd(false)"
|
||||||
@@ -197,7 +195,6 @@ import {
|
|||||||
createChildCollection,
|
createChildCollection,
|
||||||
renameCollection,
|
renameCollection,
|
||||||
deleteCollection,
|
deleteCollection,
|
||||||
importJSONToTeam,
|
|
||||||
moveRESTTeamCollection,
|
moveRESTTeamCollection,
|
||||||
updateOrderRESTTeamCollection,
|
updateOrderRESTTeamCollection,
|
||||||
} from "~/helpers/backend/mutations/TeamCollection"
|
} from "~/helpers/backend/mutations/TeamCollection"
|
||||||
@@ -212,12 +209,9 @@ import { TeamCollection } from "~/helpers/teams/TeamCollection"
|
|||||||
import { Collection as NodeCollection } from "./MyCollections.vue"
|
import { Collection as NodeCollection } from "./MyCollections.vue"
|
||||||
import {
|
import {
|
||||||
getCompleteCollectionTree,
|
getCompleteCollectionTree,
|
||||||
getTeamCollectionJSON,
|
|
||||||
teamCollToHoppRESTColl,
|
teamCollToHoppRESTColl,
|
||||||
} from "~/helpers/backend/helpers"
|
} from "~/helpers/backend/helpers"
|
||||||
import * as E from "fp-ts/Either"
|
|
||||||
import { platform } from "~/platform"
|
import { platform } from "~/platform"
|
||||||
import { createCollectionGists } from "~/helpers/gist"
|
|
||||||
import {
|
import {
|
||||||
getRequestsByPath,
|
getRequestsByPath,
|
||||||
resolveSaveContextOnRequestReorder,
|
resolveSaveContextOnRequestReorder,
|
||||||
@@ -229,7 +223,7 @@ import {
|
|||||||
resetTeamRequestsContext,
|
resetTeamRequestsContext,
|
||||||
} from "~/helpers/collection/collection"
|
} from "~/helpers/collection/collection"
|
||||||
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
import { currentReorderingStatus$ } from "~/newstore/reordering"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
import { WorkspaceService } from "~/services/workspace.service"
|
import { WorkspaceService } from "~/services/workspace.service"
|
||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
@@ -303,12 +297,6 @@ const draggingToRoot = ref(false)
|
|||||||
const collectionMoveLoading = ref<string[]>([])
|
const collectionMoveLoading = ref<string[]>([])
|
||||||
const requestMoveLoading = ref<string[]>([])
|
const requestMoveLoading = ref<string[]>([])
|
||||||
|
|
||||||
// Export - Import refs
|
|
||||||
const collectionJSON = ref("")
|
|
||||||
const exportingTeamCollections = ref(false)
|
|
||||||
const creatingGistCollection = ref(false)
|
|
||||||
const importingMyCollections = ref(false)
|
|
||||||
|
|
||||||
// TeamList-Adapter
|
// TeamList-Adapter
|
||||||
const workspaceService = useService(WorkspaceService)
|
const workspaceService = useService(WorkspaceService)
|
||||||
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
|
||||||
@@ -412,14 +400,12 @@ const currentReorderingStatus = useReadonlyStream(currentReorderingStatus$, {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const hasTeamWriteAccess = computed(() => {
|
const hasTeamWriteAccess = computed(() => {
|
||||||
if (!collectionsType.value.selectedTeam) return false
|
if (collectionsType.value.type !== "team-collections") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
const role = collectionsType.value.selectedTeam?.myRole
|
||||||
collectionsType.value.type === "team-collections" &&
|
return role === "OWNER" || role === "EDITOR"
|
||||||
collectionsType.value.selectedTeam.myRole !== "VIEWER"
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
else return false
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredCollections = computed(() => {
|
const filteredCollections = computed(() => {
|
||||||
@@ -1069,7 +1055,7 @@ const onRemoveCollection = () => {
|
|||||||
const collectionIndex = editingCollectionIndex.value
|
const collectionIndex = editingCollectionIndex.value
|
||||||
|
|
||||||
const collectionToRemove =
|
const collectionToRemove =
|
||||||
collectionIndex || collectionIndex == 0
|
collectionIndex || collectionIndex === 0
|
||||||
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
? navigateToFolderWithIndexPath(restCollectionStore.value.state, [
|
||||||
collectionIndex,
|
collectionIndex,
|
||||||
])
|
])
|
||||||
@@ -1468,9 +1454,8 @@ const checkIfCollectionIsAParentOfTheChildren = (
|
|||||||
)
|
)
|
||||||
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
if (isEqual(slicedDestinationCollectionPath, collectionDraggedPath)) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -1491,9 +1476,8 @@ const isMoveToSameLocation = (
|
|||||||
|
|
||||||
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
if (isEqual(draggedItemParentPathArr, destinationPathArr)) {
|
||||||
return true
|
return true
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1673,25 +1657,22 @@ const isSameSameParent = (
|
|||||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||||
|
|
||||||
return dragedItemParent.join("/") === destinationCollectionIndex
|
return dragedItemParent.join("/") === destinationCollectionIndex
|
||||||
} else {
|
|
||||||
if (destinationItemPath === null) return false
|
|
||||||
const destinationItemIndex = pathToIndex(destinationItemPath)
|
|
||||||
|
|
||||||
// length of 1 means the request is in the root
|
|
||||||
if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
|
|
||||||
return true
|
|
||||||
} else if (draggedItemIndex.length === destinationItemIndex.length) {
|
|
||||||
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
|
||||||
const destinationItemParent = destinationItemIndex.slice(0, -1)
|
|
||||||
if (isEqual(dragedItemParent, destinationItemParent)) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (destinationItemPath === null) return false
|
||||||
|
const destinationItemIndex = pathToIndex(destinationItemPath)
|
||||||
|
|
||||||
|
// length of 1 means the request is in the root
|
||||||
|
if (draggedItemIndex.length === 1 && destinationItemIndex.length === 1) {
|
||||||
|
return true
|
||||||
|
} else if (draggedItemIndex.length === destinationItemIndex.length) {
|
||||||
|
const dragedItemParent = draggedItemIndex.slice(0, -1)
|
||||||
|
const destinationItemParent = destinationItemIndex.slice(0, -1)
|
||||||
|
if (isEqual(dragedItemParent, destinationItemParent)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1833,33 +1814,6 @@ const updateCollectionOrder = (payload: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Import - Export Collection functions
|
// Import - Export Collection functions
|
||||||
/**
|
|
||||||
* Export the whole my collection or specific team collection to JSON
|
|
||||||
*/
|
|
||||||
const getJSONCollection = async () => {
|
|
||||||
if (collectionsType.value.type === "my-collections") {
|
|
||||||
collectionJSON.value = JSON.stringify(myCollections.value, null, 2)
|
|
||||||
} else {
|
|
||||||
if (!collectionsType.value.selectedTeam) return
|
|
||||||
exportingTeamCollections.value = true
|
|
||||||
pipe(
|
|
||||||
await getTeamCollectionJSON(collectionsType.value.selectedTeam.id),
|
|
||||||
E.match(
|
|
||||||
(err) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
exportingTeamCollections.value = false
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
const { exportCollectionsToJSON } = result
|
|
||||||
collectionJSON.value = exportCollectionsToJSON
|
|
||||||
exportingTeamCollections.value = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return collectionJSON.value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a downloadable file from a collection and prompts the user to download it.
|
* Create a downloadable file from a collection and prompts the user to download it.
|
||||||
@@ -1928,88 +1882,15 @@ const exportData = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportJSONCollection = async () => {
|
const shareRequest = ({ request }: { request: HoppRESTRequest }) => {
|
||||||
platform.analytics?.logEvent({
|
if (currentUser.value) {
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
// opens the share request modal
|
||||||
exporter: "json",
|
invokeAction("share.request", {
|
||||||
platform: "rest",
|
request,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
await getJSONCollection()
|
invokeAction("modals.login.toggle")
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(collectionJSON.value)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_collections_to_export"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeDownloadCollection(collectionJSON.value, null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const createCollectionGist = async () => {
|
|
||||||
if (!currentUser.value || !currentUser.value.accessToken) {
|
|
||||||
toast.error(t("profile.no_permission").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "gist",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
creatingGistCollection.value = true
|
|
||||||
await getJSONCollection()
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
createCollectionGists(collectionJSON.value, currentUser.value.accessToken),
|
|
||||||
TE.match(
|
|
||||||
(err) => {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(err)
|
|
||||||
creatingGistCollection.value = false
|
|
||||||
},
|
|
||||||
(result) => {
|
|
||||||
toast.success(t("export.gist_created").toString())
|
|
||||||
creatingGistCollection.value = false
|
|
||||||
window.open(result.data.html_url)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importToTeams = async (collection: HoppCollection<HoppRESTRequest>[]) => {
|
|
||||||
if (!hasTeamWriteAccess.value) {
|
|
||||||
toast.error(t("team.no_access").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!collectionsType.value.selectedTeam) return
|
|
||||||
|
|
||||||
importingMyCollections.value = true
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_EXPORT_COLLECTION",
|
|
||||||
exporter: "import-to-teams",
|
|
||||||
platform: "rest",
|
|
||||||
})
|
|
||||||
|
|
||||||
pipe(
|
|
||||||
importJSONToTeam(
|
|
||||||
JSON.stringify(collection),
|
|
||||||
collectionsType.value.selectedTeam.id
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
importingMyCollections.value = false
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
importingMyCollections.value = false
|
|
||||||
displayModalImportExport(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolveConfirmModal = (title: string | null) => {
|
const resolveConfirmModal = (title: string | null) => {
|
||||||
@@ -2041,37 +1922,36 @@ const getErrorMessage = (err: GQLError<string>) => {
|
|||||||
console.error(err)
|
console.error(err)
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_coll/short_title":
|
case "team_coll/short_title":
|
||||||
return t("collection.name_length_insufficient")
|
return t("collection.name_length_insufficient")
|
||||||
case "team/invalid_coll_id":
|
case "team/invalid_coll_id":
|
||||||
case "bug/team_coll/no_coll_id":
|
case "bug/team_coll/no_coll_id":
|
||||||
case "team_req/invalid_target_id":
|
case "team_req/invalid_target_id":
|
||||||
return t("team.invalid_coll_id")
|
return t("team.invalid_coll_id")
|
||||||
case "team/not_required_role":
|
case "team/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_required_role":
|
case "team_req/not_required_role":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
case "team_req/not_found":
|
case "team_req/not_found":
|
||||||
return t("team.no_request_found")
|
return t("team.no_request_found")
|
||||||
case "bug/team_req/no_req_id":
|
case "bug/team_req/no_req_id":
|
||||||
return t("team.no_request_found")
|
return t("team.no_request_found")
|
||||||
case "team/collection_is_parent_coll":
|
case "team/collection_is_parent_coll":
|
||||||
return t("team.parent_coll_move")
|
return t("team.parent_coll_move")
|
||||||
case "team/target_and_destination_collection_are_same":
|
case "team/target_and_destination_collection_are_same":
|
||||||
return t("team.same_target_destination")
|
return t("team.same_target_destination")
|
||||||
case "team/target_collection_is_already_root_collection":
|
case "team/target_collection_is_already_root_collection":
|
||||||
return t("collection.invalid_root_move")
|
return t("collection.invalid_root_move")
|
||||||
case "team_req/requests_not_from_same_collection":
|
case "team_req/requests_not_from_same_collection":
|
||||||
return t("request.different_collection")
|
return t("request.different_collection")
|
||||||
case "team/team_collections_have_different_parents":
|
case "team/team_collections_have_different_parents":
|
||||||
return t("collection.different_parent")
|
return t("collection.different_parent")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@
|
|||||||
v-if="!currentInterceptorSupportsCookies"
|
v-if="!currentInterceptorSupportsCookies"
|
||||||
:text="t('cookies.modal.interceptor_no_support')"
|
:text="t('cookies.modal.interceptor_no_support')"
|
||||||
>
|
>
|
||||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
<template #body>
|
||||||
|
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-col">
|
<div v-else class="flex flex-col">
|
||||||
<div
|
<div
|
||||||
@@ -38,8 +40,7 @@
|
|||||||
:alt="`${t('cookies.modal.empty_domains')}`"
|
:alt="`${t('cookies.modal.empty_domains')}`"
|
||||||
:text="t('cookies.modal.empty_domains')"
|
:text="t('cookies.modal.empty_domains')"
|
||||||
class="mt-6"
|
class="mt-6"
|
||||||
>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<div
|
<div
|
||||||
v-for="[domain, entries] in workingCookieJar.entries()"
|
v-for="[domain, entries] in workingCookieJar.entries()"
|
||||||
v-else
|
v-else
|
||||||
|
|||||||
206
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
206
packages/hoppscotch-common/src/components/embeds/index.vue
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<header
|
||||||
|
class="flex items-center justify-between flex-1 flex-shrink-0 px-2 py-2 space-x-2 overflow-x-auto overflow-y-hidden"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between flex-1 space-x-2">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
class="!font-bold uppercase tracking-wide !text-secondaryDark hover:bg-primaryDark focus-visible:bg-primaryDark"
|
||||||
|
:label="t('app.name')"
|
||||||
|
to="https://hoppscotch.io"
|
||||||
|
blank
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('app.open_in_hoppscotch')"
|
||||||
|
:to="sharedRequestURL"
|
||||||
|
blank
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="sticky top-0 z-10 flex-1">
|
||||||
|
<div
|
||||||
|
class="flex-none flex-shrink-0 p-4 bg-primary sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex flex-1 overflow-hidden border divide-x rounded text-secondaryDark divide-divider min-w-[12rem] overflow-x-auto border-divider"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center px-4 py-2 font-semibold transition rounded-l"
|
||||||
|
>
|
||||||
|
{{ tab.document.request.method }}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="flex items-center flex-1 flex-shrink-0 min-w-0 px-4 py-2 truncate rounded-r"
|
||||||
|
>
|
||||||
|
{{ tab.document.request.endpoint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex mt-2 space-x-2 sm:mt-0">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
id="send"
|
||||||
|
:title="`${t(
|
||||||
|
'action.send'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
|
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||||
|
class="flex-1 min-w-20"
|
||||||
|
outline
|
||||||
|
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||||
|
/>
|
||||||
|
<div class="flex">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:title="`${t(
|
||||||
|
'request.save'
|
||||||
|
)} <kbd>${getSpecialKey()}</kbd><kbd>S</kbd>`"
|
||||||
|
:label="t('request.save')"
|
||||||
|
filled
|
||||||
|
:icon="IconSave"
|
||||||
|
class="flex-1 rounded"
|
||||||
|
blank
|
||||||
|
outline
|
||||||
|
:to="sharedRequestURL"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HttpRequestOptions
|
||||||
|
v-model="tab.document.request"
|
||||||
|
v-model:option-tab="selectedOptionTab"
|
||||||
|
:properties="properties"
|
||||||
|
/>
|
||||||
|
<HttpResponse :document="tab.document" :is-embed="true" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { Ref } from "vue"
|
||||||
|
import { computed, useModel } from "vue"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { useStreamSubscriber } from "~/composables/stream"
|
||||||
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
|
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||||
|
import { HoppTab } from "~/services/tab"
|
||||||
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
|
import IconSave from "~icons/lucide/save"
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelTab: HoppTab<HoppRESTDocument>
|
||||||
|
properties: string[]
|
||||||
|
sharedRequestID: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tab = useModel(props, "modelTab")
|
||||||
|
|
||||||
|
const selectedOptionTab = ref(props.properties[0])
|
||||||
|
|
||||||
|
const requestCancelFunc: Ref<(() => void) | null> = ref(null)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||||
|
const sharedRequestURL = computed(() => {
|
||||||
|
return `${baseURL}/r/${props.sharedRequestID}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
|
|
||||||
|
const newSendRequest = async () => {
|
||||||
|
if (newEndpoint.value === "" || /^\s+$/.test(newEndpoint.value)) {
|
||||||
|
toast.error(`${t("empty.endpoint")}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureMethodInEndpoint()
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const [cancel, streamPromise] = runRESTRequest$(tab)
|
||||||
|
const streamResult = await streamPromise
|
||||||
|
|
||||||
|
requestCancelFunc.value = cancel
|
||||||
|
if (E.isRight(streamResult)) {
|
||||||
|
subscribeToStream(
|
||||||
|
streamResult.right,
|
||||||
|
(responseState) => {
|
||||||
|
if (loading.value) {
|
||||||
|
// Check exists because, loading can be set to false
|
||||||
|
// when cancelled
|
||||||
|
updateRESTResponse(responseState)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
loading.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// TODO: Change this any to a proper type
|
||||||
|
const result = (streamResult.right as any).value
|
||||||
|
if (
|
||||||
|
result.type === "network_fail" &&
|
||||||
|
result.error?.error === "NO_PW_EXT_HOOK"
|
||||||
|
) {
|
||||||
|
const errorResponse: HoppRESTResponse = {
|
||||||
|
type: "extension_error",
|
||||||
|
error: result.error.humanMessage.heading,
|
||||||
|
component: result.error.component,
|
||||||
|
req: result.req,
|
||||||
|
}
|
||||||
|
updateRESTResponse(errorResponse)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
loading.value = false
|
||||||
|
toast.error(`${t("error.script_fail")}`)
|
||||||
|
let error: Error
|
||||||
|
if (typeof streamResult.left === "string") {
|
||||||
|
error = { name: "RequestFailure", message: streamResult.left }
|
||||||
|
} else {
|
||||||
|
error = streamResult.left
|
||||||
|
}
|
||||||
|
updateRESTResponse({
|
||||||
|
type: "script_fail",
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
||||||
|
tab.value.document.response = response
|
||||||
|
}
|
||||||
|
|
||||||
|
const newEndpoint = computed(() => {
|
||||||
|
return tab.value.document.request.endpoint
|
||||||
|
})
|
||||||
|
|
||||||
|
const ensureMethodInEndpoint = () => {
|
||||||
|
if (
|
||||||
|
!/^http[s]?:\/\//.test(newEndpoint.value) &&
|
||||||
|
!newEndpoint.value.startsWith("<<")
|
||||||
|
) {
|
||||||
|
const domain = newEndpoint.value.split(/[/:#?]+/)[0]
|
||||||
|
if (domain === "localhost" || /([0-9]+\.)*[0-9]/.test(domain)) {
|
||||||
|
tab.value.document.request.endpoint =
|
||||||
|
"http://" + tab.value.document.request.endpoint
|
||||||
|
} else {
|
||||||
|
tab.value.document.request.endpoint =
|
||||||
|
"https://" + tab.value.document.request.endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelRequest = () => {
|
||||||
|
loading.value = false
|
||||||
|
requestCancelFunc.value?.()
|
||||||
|
|
||||||
|
updateRESTResponse(null)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
<template #body>
|
<template #body>
|
||||||
<div class="flex flex-1 flex-col space-y-4">
|
<div class="flex flex-1 flex-col space-y-4">
|
||||||
<div class="ml-2 flex items-center space-x-8">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="name" class="min-w-10 font-semibold">{{
|
<label for="name" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.name")
|
t("environment.name")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<input
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-2 flex items-center space-x-8">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="value" class="min-w-10 font-semibold">{{
|
<label for="value" class="min-w-[2.5rem] font-semibold">{{
|
||||||
t("environment.value")
|
t("environment.value")
|
||||||
}}</label>
|
}}</label>
|
||||||
<input
|
<input
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-2 flex items-center space-x-8">
|
<div class="ml-2 flex items-center space-x-8">
|
||||||
<label for="scope" class="min-w-10 font-semibold">
|
<label for="scope" class="min-w-[2.5rem] font-semibold">
|
||||||
{{ t("environment.scope") }}
|
{{ t("environment.scope") }}
|
||||||
</label>
|
</label>
|
||||||
<div
|
<div
|
||||||
@@ -39,10 +39,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2">
|
<div v-if="replaceWithVariable" class="mt-3 flex space-x-2">
|
||||||
<div class="min-w-18" />
|
<div class="min-w-[4rem]" />
|
||||||
<HoppSmartCheckbox
|
<HoppSmartCheckbox
|
||||||
:on="replaceWithVariable"
|
:on="replaceWithVariable"
|
||||||
title="t('environment.replace_with_variable'))"
|
:title="t('environment.replace_with_variable')"
|
||||||
@change="replaceWithVariable = !replaceWithVariable"
|
@change="replaceWithVariable = !replaceWithVariable"
|
||||||
/>
|
/>
|
||||||
<label for="replaceWithVariable">
|
<label for="replaceWithVariable">
|
||||||
@@ -205,15 +205,14 @@ const addEnvironment = async () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
case "Forbidden resource":
|
case "Forbidden resource":
|
||||||
return t("profile.no_permission")
|
return t("profile.no_permission")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,154 +1,60 @@
|
|||||||
<template>
|
<template>
|
||||||
<HoppSmartModal
|
<ImportExportBase
|
||||||
v-if="show"
|
ref="collections-import-export"
|
||||||
dialog
|
modal-title="environment.title"
|
||||||
:title="`${t('environment.title')}`"
|
:importer-modules="importerModules"
|
||||||
styles="sm:max-w-md"
|
:exporter-modules="exporterModules"
|
||||||
@close="hideModal"
|
@hide-modal="emit('hide-modal')"
|
||||||
>
|
/>
|
||||||
<template #actions>
|
|
||||||
<span>
|
|
||||||
<tippy
|
|
||||||
interactive
|
|
||||||
trigger="click"
|
|
||||||
theme="popover"
|
|
||||||
:on-shown="() => tippyActions!.focus()"
|
|
||||||
>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.more')"
|
|
||||||
:icon="IconMoreVertical"
|
|
||||||
/>
|
|
||||||
<template #content="{ hide }">
|
|
||||||
<div
|
|
||||||
ref="tippyActions"
|
|
||||||
class="flex flex-col focus:outline-none"
|
|
||||||
tabindex="0"
|
|
||||||
@keyup.escape="hide()"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('import.from_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
readEnvironmentGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="
|
|
||||||
!currentUser
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? `${t('export.require_github')}`
|
|
||||||
: undefined
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<HoppSmartItem
|
|
||||||
:disabled="
|
|
||||||
!currentUser
|
|
||||||
? true
|
|
||||||
: currentUser.provider !== 'github.com'
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
"
|
|
||||||
:icon="IconGithub"
|
|
||||||
:label="t('export.create_secret_gist')"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
createEnvironmentGist()
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</tippy>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<template #body>
|
|
||||||
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
|
||||||
<HoppSmartSpinner class="my-4" />
|
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
|
||||||
</div>
|
|
||||||
<div v-else class="flex flex-col space-y-2">
|
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconFolderPlus"
|
|
||||||
:label="t('import.from_json')"
|
|
||||||
@click="openDialogChooseFileToImportFrom"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
ref="inputChooseFileToImportFrom"
|
|
||||||
class="input"
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
@change="importFromJSON"
|
|
||||||
/>
|
|
||||||
<hr />
|
|
||||||
<HoppSmartItem
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.download_file')"
|
|
||||||
:icon="IconDownload"
|
|
||||||
:label="t('export.as_json')"
|
|
||||||
@click="exportJSON"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</HoppSmartModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconMoreVertical from "~icons/lucide/more-vertical"
|
import { useI18n } from "~/composables/i18n"
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
import { useToast } from "~/composables/toast"
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconGithub from "~icons/lucide/github"
|
|
||||||
import { computed, ref } from "vue"
|
|
||||||
import { Environment } from "@hoppscotch/data"
|
import { Environment } from "@hoppscotch/data"
|
||||||
import { platform } from "~/platform"
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
import axios from "axios"
|
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
import { hoppEnvImporter } from "~/helpers/import-export/import/hoppEnv"
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import {
|
import * as E from "fp-ts/Either"
|
||||||
environments$,
|
import { appendEnvironments, environments$ } from "~/newstore/environments"
|
||||||
replaceEnvironments,
|
|
||||||
appendEnvironments,
|
|
||||||
} from "~/newstore/environments"
|
|
||||||
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
import { createTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
|
||||||
|
import { TeamEnvironment } from "~/helpers/teams/TeamEnvironment"
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
import { TippyComponent } from "vue-tippy"
|
import { CreateTeamEnvironmentMutation } from "~/helpers/backend/graphql"
|
||||||
|
import { postmanEnvImporter } from "~/helpers/import-export/import/postmanEnv"
|
||||||
|
|
||||||
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
|
import IconPostman from "~icons/hopp/postman"
|
||||||
|
import IconUser from "~icons/lucide/user"
|
||||||
|
import { initializeDownloadCollection } from "~/helpers/import-export/export"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { environmentsExporter } from "~/helpers/import-export/export/environments"
|
||||||
|
import { environmentsGistExporter } from "~/helpers/import-export/export/environmentsGistExport"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean
|
|
||||||
teamEnvironments?: TeamEnvironment[]
|
teamEnvironments?: TeamEnvironment[]
|
||||||
teamId?: string | undefined
|
teamId?: string | undefined
|
||||||
environmentType: "MY_ENV" | "TEAM_ENV"
|
environmentType: "MY_ENV" | "TEAM_ENV"
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "hide-modal"): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const toast = useToast()
|
|
||||||
const t = useI18n()
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const myEnvironments = useReadonlyStream(environments$, [])
|
const myEnvironments = useReadonlyStream(environments$, [])
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
const currentUser = useReadonlyStream(
|
||||||
platform.auth.getCurrentUserStream(),
|
platform.auth.getCurrentUserStream(),
|
||||||
platform.auth.getCurrentUser()
|
platform.auth.getCurrentUser()
|
||||||
)
|
)
|
||||||
|
|
||||||
// Template refs
|
const isTeamEnvironment = computed(() => {
|
||||||
const tippyActions = ref<TippyComponent | null>(null)
|
return props.environmentType === "TEAM_ENV"
|
||||||
const inputChooseFileToImportFrom = ref<HTMLInputElement>()
|
})
|
||||||
|
|
||||||
const environmentJson = computed(() => {
|
const environmentJson = computed(() => {
|
||||||
if (
|
if (
|
||||||
@@ -158,266 +64,249 @@ const environmentJson = computed(() => {
|
|||||||
const teamEnvironments = props.teamEnvironments.map(
|
const teamEnvironments = props.teamEnvironments.map(
|
||||||
(x) => x.environment as Environment
|
(x) => x.environment as Environment
|
||||||
)
|
)
|
||||||
return JSON.stringify(teamEnvironments, null, 2)
|
return teamEnvironments
|
||||||
} else {
|
|
||||||
return JSON.stringify(myEnvironments.value, null, 2)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return myEnvironments.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const createEnvironmentGist = async () => {
|
const HoppEnvironmentsImport: ImporterOrExporter = {
|
||||||
if (!currentUser.value) {
|
metadata: {
|
||||||
toast.error(t("profile.no_permission").toString())
|
id: "import.from_json",
|
||||||
|
name: "import.from_json",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.hoppscotch_environment_description",
|
||||||
|
onImportFromFile: async (environments) => {
|
||||||
|
const res = await hoppEnvImporter(environments)()
|
||||||
|
|
||||||
return
|
if (E.isLeft(res)) {
|
||||||
}
|
showImportFailedError()
|
||||||
|
return
|
||||||
try {
|
|
||||||
const res = await axios.post(
|
|
||||||
"https://api.github.com/gists",
|
|
||||||
{
|
|
||||||
files: {
|
|
||||||
"hoppscotch-environments.json": {
|
|
||||||
content: environmentJson.value,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `token ${currentUser.value.accessToken}`,
|
|
||||||
Accept: "application/vnd.github.v3+json",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const PostmanEnvironmentsImport: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "import.from_postman",
|
||||||
|
name: "import.from_postman",
|
||||||
|
icon: IconPostman,
|
||||||
|
title: "import.from_json",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: FileSource({
|
||||||
|
acceptedFileTypes: "application/json",
|
||||||
|
caption: "import.postman_environment_description",
|
||||||
|
onImportFromFile: async (environments) => {
|
||||||
|
const res = await postmanEnvImporter(environments)()
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore([res.right])
|
||||||
|
|
||||||
|
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",
|
||||||
|
name: "import.environments_from_gist",
|
||||||
|
icon: IconFolderPlus,
|
||||||
|
title: "import.environments_from_gist",
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
component: GistSource({
|
||||||
|
caption: "import.environments_from_gist_description",
|
||||||
|
onImportFromGist: async (environments) => {
|
||||||
|
if (E.isLeft(environments)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await hoppEnvImporter(environments.right)()
|
||||||
|
|
||||||
|
if (E.isLeft(res)) {
|
||||||
|
showImportFailedError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleImportToStore(res.right)
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_IMPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
workspaceType: isTeamEnvironment.value ? "team" : "personal",
|
||||||
|
})
|
||||||
|
emit("hide-modal")
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const HoppEnvironmentsExport: ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: "export.as_json",
|
||||||
|
name: "export.as_json",
|
||||||
|
title: "action.download_file",
|
||||||
|
icon: IconUser,
|
||||||
|
disabled: false,
|
||||||
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
|
},
|
||||||
|
action: () => {
|
||||||
|
if (!environmentJson.value.length) {
|
||||||
|
return toast.error(t("error.no_environments_to_export"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = initializeDownloadCollection(
|
||||||
|
environmentsExporter(environmentJson.value),
|
||||||
|
"Environments"
|
||||||
)
|
)
|
||||||
|
|
||||||
toast.success(t("export.gist_created").toString())
|
if (E.isLeft(message)) {
|
||||||
|
toast.error(t(message.left))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t(message.right))
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
platform.analytics?.logEvent({
|
||||||
type: "HOPP_EXPORT_ENVIRONMENT",
|
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||||
platform: "rest",
|
platform: "rest",
|
||||||
})
|
})
|
||||||
|
},
|
||||||
window.open(res.data.html_url)
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(t("error.something_went_wrong").toString())
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileImported = () => {
|
const HoppEnvironmentsGistExporter: ImporterOrExporter = {
|
||||||
toast.success(t("state.file_imported").toString())
|
metadata: {
|
||||||
}
|
id: "export.as_gist",
|
||||||
|
name: "export.create_secret_gist",
|
||||||
const failedImport = () => {
|
title:
|
||||||
toast.error(t("import.failed").toString())
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
}
|
// @ts-ignore
|
||||||
|
currentUser?.provider === "github.com"
|
||||||
const readEnvironmentGist = async () => {
|
? "export.create_secret_gist"
|
||||||
const gist = prompt(t("import.gist_url").toString())
|
: "export.require_github",
|
||||||
if (!gist) return
|
icon: IconUser,
|
||||||
|
disabled: !currentUser.value
|
||||||
try {
|
? true
|
||||||
const { files } = (await axios.get(
|
: currentUser.value.provider !== "github.com",
|
||||||
`https://api.github.com/gists/${gist.split("/").pop()}`,
|
applicableTo: ["personal-workspace", "team-workspace"],
|
||||||
{
|
},
|
||||||
headers: {
|
action: async () => {
|
||||||
Accept: "application/vnd.github.v3+json",
|
if (!currentUser.value) {
|
||||||
},
|
toast.error(t("profile.no_permission"))
|
||||||
}
|
|
||||||
)) as {
|
|
||||||
files: {
|
|
||||||
[fileName: string]: {
|
|
||||||
content: any
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const environments = JSON.parse(Object.values(files)[0].content)
|
|
||||||
|
|
||||||
if (props.environmentType === "MY_ENV") {
|
|
||||||
replaceEnvironments(environments)
|
|
||||||
fileImported()
|
|
||||||
} else {
|
|
||||||
importToTeams(environments)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
failedImport()
|
|
||||||
console.error(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const hideModal = () => {
|
|
||||||
emit("hide-modal")
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDialogChooseFileToImportFrom = () => {
|
|
||||||
if (inputChooseFileToImportFrom.value)
|
|
||||||
inputChooseFileToImportFrom.value.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
const importToTeams = async (content: Environment[]) => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "team",
|
|
||||||
})
|
|
||||||
|
|
||||||
for (const [i, env] of content.entries()) {
|
|
||||||
if (i === content.length - 1) {
|
|
||||||
await pipe(
|
|
||||||
createTeamEnvironment(
|
|
||||||
JSON.stringify(env.variables),
|
|
||||||
props.teamId as string,
|
|
||||||
env.name
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
loading.value = false
|
|
||||||
hideModal()
|
|
||||||
fileImported()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
} else {
|
|
||||||
await pipe(
|
|
||||||
createTeamEnvironment(
|
|
||||||
JSON.stringify(env.variables),
|
|
||||||
props.teamId as string,
|
|
||||||
env.name
|
|
||||||
),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
console.error(err)
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
// wait for all the environments to be created then fire the toast
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const importFromJSON = () => {
|
|
||||||
if (!inputChooseFileToImportFrom.value) return
|
|
||||||
|
|
||||||
if (
|
|
||||||
!inputChooseFileToImportFrom.value.files ||
|
|
||||||
inputChooseFileToImportFrom.value.files.length === 0
|
|
||||||
) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_IMPORT_ENVIRONMENT",
|
|
||||||
platform: "rest",
|
|
||||||
workspaceType: "personal",
|
|
||||||
})
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.onload = ({ target }) => {
|
|
||||||
const content = target!.result as string | null
|
|
||||||
|
|
||||||
if (!content) {
|
|
||||||
toast.show(t("action.choose_file").toString())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const environments = JSON.parse(content)
|
const accessToken = currentUser.value?.accessToken
|
||||||
|
|
||||||
if (
|
if (accessToken) {
|
||||||
environments._postman_variable_scope === "environment" ||
|
const res = await environmentsGistExporter(
|
||||||
environments._postman_variable_scope === "globals"
|
JSON.stringify(environmentJson.value),
|
||||||
) {
|
accessToken
|
||||||
importFromPostman(environments)
|
)
|
||||||
} else if (environments[0]) {
|
|
||||||
const [name, variables] = Object.keys(environments[0])
|
if (E.isLeft(res)) {
|
||||||
if (name === "name" && variables === "variables") {
|
toast.error(t("export.failed"))
|
||||||
// Do nothing
|
return
|
||||||
}
|
}
|
||||||
importFromHoppscotch(environments)
|
|
||||||
} else {
|
|
||||||
failedImport()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.readAsText(inputChooseFileToImportFrom.value.files[0])
|
toast.success(t("export.success"))
|
||||||
inputChooseFileToImportFrom.value.value = ""
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_EXPORT_ENVIRONMENT",
|
||||||
|
platform: "rest",
|
||||||
|
})
|
||||||
|
|
||||||
|
window.open(res.right, "_blank")
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const importFromHoppscotch = (environments: Environment[]) => {
|
const importerModules = [
|
||||||
|
HoppEnvironmentsImport,
|
||||||
|
EnvironmentsImportFromGIST,
|
||||||
|
PostmanEnvironmentsImport,
|
||||||
|
]
|
||||||
|
|
||||||
|
const exporterModules = computed(() => {
|
||||||
|
const enabledExporters = [HoppEnvironmentsExport]
|
||||||
|
|
||||||
|
if (platform.platformFeatureFlags.exportAsGIST) {
|
||||||
|
enabledExporters.push(HoppEnvironmentsGistExporter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledExporters
|
||||||
|
})
|
||||||
|
|
||||||
|
const showImportFailedError = () => {
|
||||||
|
toast.error(t("import.failed").toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleImportToStore = async (environments: Environment[]) => {
|
||||||
if (props.environmentType === "MY_ENV") {
|
if (props.environmentType === "MY_ENV") {
|
||||||
appendEnvironments(environments)
|
appendEnvironments(environments)
|
||||||
fileImported()
|
toast.success(t("state.file_imported"))
|
||||||
} else {
|
} else {
|
||||||
importToTeams(environments)
|
await importToTeams(environments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const importFromPostman = ({
|
const importToTeams = async (content: Environment[]) => {
|
||||||
name,
|
const envImportPromises: Promise<
|
||||||
values,
|
E.Either<GQLError<"">, CreateTeamEnvironmentMutation>
|
||||||
}: {
|
>[] = []
|
||||||
name: string
|
|
||||||
values: { key: string; value: string }[]
|
|
||||||
}) => {
|
|
||||||
const environment: Environment = { name, variables: [] }
|
|
||||||
values.forEach(({ key, value }) => environment.variables.push({ key, value }))
|
|
||||||
const environments = [environment]
|
|
||||||
|
|
||||||
importFromHoppscotch(environments)
|
for (const [, env] of content.entries()) {
|
||||||
}
|
const res = createTeamEnvironment(
|
||||||
|
JSON.stringify(env.variables),
|
||||||
|
props.teamId as string,
|
||||||
|
env.name
|
||||||
|
)()
|
||||||
|
|
||||||
const exportJSON = async () => {
|
envImportPromises.push(res)
|
||||||
const dataToWrite = environmentJson.value
|
|
||||||
|
|
||||||
const parsedCollections = JSON.parse(dataToWrite)
|
|
||||||
|
|
||||||
if (!parsedCollections.length) {
|
|
||||||
return toast.error(t("error.no_environments_to_export"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = new Blob([dataToWrite], { type: "application/json" })
|
const res = await Promise.all(envImportPromises)
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
const failedImports = res.some((r) => E.isLeft(r))
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
if (failedImports) {
|
||||||
|
toast.error(t("import.failed"))
|
||||||
const result = await platform.io.saveFileWithDialog({
|
|
||||||
data: dataToWrite,
|
|
||||||
contentType: "application/json",
|
|
||||||
suggestedFilename: filename,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "JSON file",
|
|
||||||
extensions: ["json"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result.type === "unknown" || result.type === "saved") {
|
|
||||||
toast.success(t("state.download_started").toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
|
||||||
if (err.type === "network_error") {
|
|
||||||
return t("error.network_error")
|
|
||||||
} else {
|
} else {
|
||||||
switch (err.error) {
|
toast.success(t("import.success"))
|
||||||
case "team_environment/not_found":
|
|
||||||
return t("team_environment.not_found")
|
|
||||||
default:
|
|
||||||
return t("error.something_went_wrong")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): () => void
|
||||||
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,10 +6,9 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => envSelectorActions!.focus()"
|
:on-shown="() => envSelectorActions!.focus()"
|
||||||
>
|
>
|
||||||
<span
|
<HoppSmartSelectWrapper
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:title="`${t('environment.select')}`"
|
:title="`${t('environment.select')}`"
|
||||||
class="select-wrapper"
|
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:icon="IconLayers"
|
:icon="IconLayers"
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
"
|
"
|
||||||
class="flex-1 !justify-start rounded-none pr-8"
|
class="flex-1 !justify-start rounded-none pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="envSelectorActions"
|
ref="envSelectorActions"
|
||||||
@@ -94,20 +93,12 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<HoppSmartPlaceholder
|
||||||
v-if="myEnvironments.length === 0"
|
v-if="myEnvironments.length === 0"
|
||||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
>
|
:alt="`${t('empty.environments')}`"
|
||||||
<img
|
:text="t('empty.environments')"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
/>
|
||||||
loading="lazy"
|
|
||||||
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
/>
|
|
||||||
<span class="pb-2 text-center">
|
|
||||||
{{ t("empty.environments") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
:id="'team-environments'"
|
:id="'team-environments'"
|
||||||
@@ -141,20 +132,12 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<HoppSmartPlaceholder
|
||||||
v-if="teamEnvironmentList.length === 0"
|
v-if="teamEnvironmentList.length === 0"
|
||||||
class="flex flex-col items-center justify-center text-secondaryLight"
|
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
||||||
>
|
:alt="`${t('empty.environments')}`"
|
||||||
<img
|
:text="t('empty.environments')"
|
||||||
:src="`/images/states/${colorMode.value}/blockchain.svg`"
|
/>
|
||||||
loading="lazy"
|
|
||||||
class="mb-2 inline-flex h-16 w-16 flex-col object-contain object-center"
|
|
||||||
:alt="`${t('empty.environments')}`"
|
|
||||||
/>
|
|
||||||
<span class="pb-2 text-center">
|
|
||||||
{{ t("empty.environments") }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="!teamListLoading && teamAdapterError"
|
v-if="!teamListLoading && teamAdapterError"
|
||||||
@@ -207,10 +190,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
<div class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
||||||
<div class="flex flex-1 space-x-4">
|
<div class="flex flex-1 space-x-4">
|
||||||
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.name") }}
|
{{ t("environment.name") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-32 w-full truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.value") }}
|
{{ t("environment.value") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,10 +206,10 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="flex flex-1 space-x-4"
|
class="flex flex-1 space-x-4"
|
||||||
>
|
>
|
||||||
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-32 w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
{{ variable.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -258,10 +245,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
<div v-else class="my-2 flex flex-1 flex-col space-y-2 pl-4 pr-2">
|
||||||
<div class="flex flex-1 space-x-4">
|
<div class="flex flex-1 space-x-4">
|
||||||
<span class="min-w-32 w-1/4 truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-1/4 truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.name") }}
|
{{ t("environment.name") }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-32 w-full truncate text-tiny font-semibold">
|
<span
|
||||||
|
class="min-w-[9rem] w-full truncate text-tiny font-semibold"
|
||||||
|
>
|
||||||
{{ t("environment.value") }}
|
{{ t("environment.value") }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -270,10 +261,10 @@
|
|||||||
:key="index"
|
:key="index"
|
||||||
class="flex flex-1 space-x-4"
|
class="flex flex-1 space-x-4"
|
||||||
>
|
>
|
||||||
<span class="min-w-32 w-1/4 truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-1/4 truncate text-secondaryLight">
|
||||||
{{ variable.key }}
|
{{ variable.key }}
|
||||||
</span>
|
</span>
|
||||||
<span class="min-w-32 w-full truncate text-secondaryLight">
|
<span class="min-w-[9rem] w-full truncate text-secondaryLight">
|
||||||
{{ variable.value }}
|
{{ variable.value }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -446,12 +437,11 @@ const isEnvActive = (id: string | number) => {
|
|||||||
} else {
|
} else {
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
return selectedEnv.value.index === id
|
return selectedEnv.value.index === id
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
|
||||||
selectedEnv.value.teamEnvID === id
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
return (
|
||||||
|
selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnv.value.teamEnvID === id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,40 +486,36 @@ const selectedEnv = computed(() => {
|
|||||||
name: props.modelValue.environment.environment.name,
|
name: props.modelValue.environment.environment.name,
|
||||||
teamEnvID: props.modelValue.environment.id,
|
teamEnvID: props.modelValue.environment.id,
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return { type: "global", name: "Global" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
|
||||||
const environment =
|
|
||||||
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
|
||||||
return {
|
|
||||||
type: "MY_ENV",
|
|
||||||
index: selectedEnvironmentIndex.value.index,
|
|
||||||
name: environment.name,
|
|
||||||
variables: environment.variables,
|
|
||||||
}
|
|
||||||
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
|
||||||
const teamEnv = teamEnvironmentList.value.find(
|
|
||||||
(env) =>
|
|
||||||
env.id ===
|
|
||||||
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
|
||||||
selectedEnvironmentIndex.value.teamEnvID)
|
|
||||||
)
|
|
||||||
if (teamEnv) {
|
|
||||||
return {
|
|
||||||
type: "TEAM_ENV",
|
|
||||||
name: teamEnv.environment.name,
|
|
||||||
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
|
||||||
variables: teamEnv.environment.variables,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { type: "NO_ENV_SELECTED" }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return { type: "NO_ENV_SELECTED" }
|
|
||||||
}
|
}
|
||||||
|
return { type: "global", name: "Global" }
|
||||||
}
|
}
|
||||||
|
if (selectedEnvironmentIndex.value.type === "MY_ENV") {
|
||||||
|
const environment =
|
||||||
|
myEnvironments.value[selectedEnvironmentIndex.value.index]
|
||||||
|
return {
|
||||||
|
type: "MY_ENV",
|
||||||
|
index: selectedEnvironmentIndex.value.index,
|
||||||
|
name: environment.name,
|
||||||
|
variables: environment.variables,
|
||||||
|
}
|
||||||
|
} else if (selectedEnvironmentIndex.value.type === "TEAM_ENV") {
|
||||||
|
const teamEnv = teamEnvironmentList.value.find(
|
||||||
|
(env) =>
|
||||||
|
env.id ===
|
||||||
|
(selectedEnvironmentIndex.value.type === "TEAM_ENV" &&
|
||||||
|
selectedEnvironmentIndex.value.teamEnvID)
|
||||||
|
)
|
||||||
|
if (teamEnv) {
|
||||||
|
return {
|
||||||
|
type: "TEAM_ENV",
|
||||||
|
name: teamEnv.environment.name,
|
||||||
|
teamEnvID: selectedEnvironmentIndex.value.teamEnvID,
|
||||||
|
variables: teamEnv.environment.variables,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { type: "NO_ENV_SELECTED" }
|
||||||
|
}
|
||||||
|
return { type: "NO_ENV_SELECTED" }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set the selected environment as initial scope value
|
// Set the selected environment as initial scope value
|
||||||
@@ -577,13 +563,12 @@ const envQuickPeekActions = ref<TippyComponent | null>(null)
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,9 +577,8 @@ const globalEnvs = useReadonlyStream(globalEnv$, [])
|
|||||||
const environmentVariables = computed(() => {
|
const environmentVariables = computed(() => {
|
||||||
if (selectedEnv.value.variables) {
|
if (selectedEnv.value.variables) {
|
||||||
return selectedEnv.value.variables
|
return selectedEnv.value.variables
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
}
|
||||||
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
const editGlobalEnv = () => {
|
const editGlobalEnv = () => {
|
||||||
|
|||||||
@@ -78,11 +78,13 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
@click="addEnvironmentVariable"
|
filled
|
||||||
/>
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -198,9 +200,8 @@ const workingEnv = computed(() => {
|
|||||||
type: "MY_ENV",
|
type: "MY_ENV",
|
||||||
index: props.editingEnvironmentIndex,
|
index: props.editingEnvironmentIndex,
|
||||||
})
|
})
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
const envList = useReadonlyStream(environments$, []) || props.envVars()
|
||||||
@@ -226,12 +227,11 @@ const liveEnvs = computed(() => {
|
|||||||
return [
|
return [
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||||
]
|
]
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
|
||||||
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return [
|
||||||
|
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
||||||
|
...globalVars.value.map((x) => ({ ...x, source: "Global" })),
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -38,27 +38,29 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<template #body>
|
||||||
<span class="text-center text-secondaryLight">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{{ t("environment.import_or_create") }}
|
<span class="text-center text-secondaryLight">
|
||||||
</span>
|
{{ t("environment.import_or_create") }}
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
</span>
|
||||||
<HoppButtonPrimary
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
:icon="IconImport"
|
<HoppButtonPrimary
|
||||||
:label="t('import.title')"
|
:icon="IconImport"
|
||||||
filled
|
:label="t('import.title')"
|
||||||
outline
|
filled
|
||||||
@click="displayModalImportExport(true)"
|
outline
|
||||||
/>
|
@click="displayModalImportExport(true)"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
:icon="IconPlus"
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
:icon="IconPlus"
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
outline
|
filled
|
||||||
@click="displayModalAdd(true)"
|
outline
|
||||||
/>
|
@click="displayModalAdd(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showModalDetails"
|
:show="showModalDetails"
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
environment-type="MY_ENV"
|
environment-type="MY_ENV"
|
||||||
@hide-modal="displayModalImportExport(false)"
|
@hide-modal="displayModalImportExport(false)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -81,18 +81,20 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
v-if="isViewer"
|
<HoppButtonSecondary
|
||||||
disabled
|
v-if="isViewer"
|
||||||
:label="`${t('add.new')}`"
|
disabled
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
/>
|
filled
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
v-else
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
v-else
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
@click="addEnvironmentVariable"
|
filled
|
||||||
/>
|
@click="addEnvironmentVariable"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,11 +207,8 @@ const evnExpandError = computed(() => {
|
|||||||
const liveEnvs = computed(() => {
|
const liveEnvs = computed(() => {
|
||||||
if (evnExpandError.value) {
|
if (evnExpandError.value) {
|
||||||
return []
|
return []
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
...vars.value.map((x) => ({ ...x.env, source: editingName.value! })),
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
return [...vars.value.map((x) => ({ ...x.env, source: editingName.value! }))]
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -338,13 +337,12 @@ const hideModal = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -184,13 +184,12 @@ const duplicateEnvironments = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -49,31 +49,33 @@
|
|||||||
:alt="`${t('empty.environments')}`"
|
:alt="`${t('empty.environments')}`"
|
||||||
:text="t('empty.environments')"
|
:text="t('empty.environments')"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<template #body>
|
||||||
<span class="text-center text-secondaryLight">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
{{ t("environment.import_or_create") }}
|
<span class="text-center text-secondaryLight">
|
||||||
</span>
|
{{ t("environment.import_or_create") }}
|
||||||
<div class="flex flex-col items-stretch gap-4">
|
</span>
|
||||||
<HoppButtonPrimary
|
<div class="flex flex-col items-stretch gap-4">
|
||||||
:icon="IconImport"
|
<HoppButtonPrimary
|
||||||
:label="t('import.title')"
|
:icon="IconImport"
|
||||||
filled
|
:label="t('import.title')"
|
||||||
outline
|
filled
|
||||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
outline
|
||||||
:disabled="isTeamViewer"
|
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||||
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
:disabled="isTeamViewer"
|
||||||
/>
|
@click="isTeamViewer ? null : displayModalImportExport(true)"
|
||||||
<HoppButtonSecondary
|
/>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
outline
|
filled
|
||||||
:icon="IconPlus"
|
outline
|
||||||
:title="isTeamViewer ? t('team.no_access') : ''"
|
:icon="IconPlus"
|
||||||
:disabled="isTeamViewer"
|
:title="isTeamViewer ? t('team.no_access') : ''"
|
||||||
@click="isTeamViewer ? null : displayModalAdd(true)"
|
:disabled="isTeamViewer"
|
||||||
/>
|
@click="isTeamViewer ? null : displayModalAdd(true)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else-if="!loading">
|
<div v-else-if="!loading">
|
||||||
<EnvironmentsTeamsEnvironment
|
<EnvironmentsTeamsEnvironment
|
||||||
@@ -107,7 +109,7 @@
|
|||||||
@hide-modal="displayModalEdit(false)"
|
@hide-modal="displayModalEdit(false)"
|
||||||
/>
|
/>
|
||||||
<EnvironmentsImportExport
|
<EnvironmentsImportExport
|
||||||
:show="showModalImportExport"
|
v-if="showModalImportExport"
|
||||||
:team-environments="teamEnvironments"
|
:team-environments="teamEnvironments"
|
||||||
:team-id="team?.id"
|
:team-id="team?.id"
|
||||||
environment-type="TEAM_ENV"
|
environment-type="TEAM_ENV"
|
||||||
@@ -174,13 +176,12 @@ const resetSelectedData = () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
}
|
||||||
switch (err.error) {
|
switch (err.error) {
|
||||||
case "team_environment/not_found":
|
case "team_environment/not_found":
|
||||||
return t("team_environment.not_found")
|
return t("team_environment.not_found")
|
||||||
default:
|
default:
|
||||||
return t("error.something_went_wrong")
|
return t("error.something_went_wrong")
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
:label="authName"
|
:label="authName"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="tippyActions"
|
||||||
@@ -120,14 +120,16 @@
|
|||||||
:alt="`${t('empty.authorization')}`"
|
:alt="`${t('empty.authorization')}`"
|
||||||
:text="t('empty.authorization')"
|
:text="t('empty.authorization')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
outline
|
<HoppButtonSecondary
|
||||||
:label="t('app.documentation')"
|
outline
|
||||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
:label="t('app.documentation')"
|
||||||
blank
|
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||||
:icon="IconExternalLink"
|
blank
|
||||||
reverse
|
:icon="IconExternalLink"
|
||||||
/>
|
reverse
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
@@ -171,7 +173,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
>
|
>
|
||||||
<div class="pb-2 text-secondaryLight">
|
<div class="pb-2 text-secondaryLight">
|
||||||
{{ t("helpers.authorization") }}
|
{{ t("helpers.authorization") }}
|
||||||
|
|||||||
@@ -162,12 +162,14 @@
|
|||||||
:alt="`${t('empty.headers')}`"
|
:alt="`${t('empty.headers')}`"
|
||||||
:text="t('empty.headers')"
|
:text="t('empty.headers')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
filled
|
||||||
@click="addHeader"
|
:icon="IconPlus"
|
||||||
/>
|
@click="addHeader"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
:title="`${t(
|
:title="`${t(
|
||||||
'action.download_file'
|
'action.download_file'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>J</kbd>`"
|
||||||
:icon="downloadResponseIcon"
|
:icon="downloadIcon"
|
||||||
@click="downloadResponse"
|
@click="downloadResponse"
|
||||||
/>
|
/>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
@@ -33,9 +33,41 @@
|
|||||||
:title="`${t(
|
:title="`${t(
|
||||||
'action.copy'
|
'action.copy'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>.</kbd>`"
|
||||||
:icon="copyResponseIcon"
|
:icon="copyIcon"
|
||||||
@click="copyResponse(response[0].data)"
|
@click="copyResponse"
|
||||||
/>
|
/>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => copyInterfaceTippyActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('app.copy_interface_type')"
|
||||||
|
:icon="IconMore"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="copyInterfaceTippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-for="(language, index) in interfaceLanguages"
|
||||||
|
:key="index"
|
||||||
|
:label="language"
|
||||||
|
:icon="
|
||||||
|
copiedInterfaceLanguage === language
|
||||||
|
? copyInterfaceIcon
|
||||||
|
: IconCopy
|
||||||
|
"
|
||||||
|
@click="runCopyInterface(language)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
<div ref="schemaEditor" class="flex flex-1 flex-col"></div>
|
||||||
@@ -59,22 +91,22 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import IconDownload from "~icons/lucide/download"
|
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
import IconCopy from "~icons/lucide/copy"
|
||||||
|
import IconMore from "~icons/lucide/more-horizontal"
|
||||||
import { computed, reactive, ref } from "vue"
|
import { computed, reactive, ref } from "vue"
|
||||||
import { refAutoReset } from "@vueuse/core"
|
|
||||||
import { useCodemirror } from "@composables/codemirror"
|
import { useCodemirror } from "@composables/codemirror"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
import { GQLResponseEvent } from "~/helpers/graphql/connection"
|
||||||
import { platform } from "~/platform"
|
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||||
|
import {
|
||||||
|
useCopyInterface,
|
||||||
|
useCopyResponse,
|
||||||
|
useDownloadResponse,
|
||||||
|
} from "~/composables/lens-actions"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -101,6 +133,7 @@ const responseString = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const schemaEditor = ref<any | null>(null)
|
const schemaEditor = ref<any | null>(null)
|
||||||
|
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
|
|
||||||
useCodemirror(
|
useCodemirror(
|
||||||
@@ -118,55 +151,29 @@ useCodemirror(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const downloadResponseIcon = refAutoReset<
|
const { copyIcon, copyResponse } = useCopyResponse(responseString)
|
||||||
typeof IconDownload | typeof IconCheck
|
const { copyInterfaceIcon, copyInterface } = useCopyInterface(responseString)
|
||||||
>(IconDownload, 1000)
|
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||||
const copyResponseIcon = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
"application/json",
|
||||||
IconCopy,
|
responseString
|
||||||
1000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const copyResponse = (str: string) => {
|
const copiedInterfaceLanguage = ref("")
|
||||||
copyToClipboard(str)
|
|
||||||
copyResponseIcon.value = IconCheck
|
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadResponse = async (str: string) => {
|
const runCopyInterface = (language: string) => {
|
||||||
const dataToWrite = str
|
copyInterface(language).then(() => {
|
||||||
const file = new Blob([dataToWrite!], { type: "application/json" })
|
copiedInterfaceLanguage.value = language
|
||||||
const url = URL.createObjectURL(file)
|
|
||||||
|
|
||||||
const filename = `${url.split("/").pop()!.split("#")[0].split("?")[0]}.json`
|
|
||||||
|
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
|
|
||||||
const result = await platform.io.saveFileWithDialog({
|
|
||||||
data: dataToWrite,
|
|
||||||
contentType: "application/json",
|
|
||||||
suggestedFilename: filename,
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: "JSON file",
|
|
||||||
extensions: ["json"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.type === "unknown" || result.type === "saved") {
|
|
||||||
downloadResponseIcon.value = IconCheck
|
|
||||||
toast.success(`${t("state.download_started")}`)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"response.file.download",
|
"response.file.download",
|
||||||
() => downloadResponse(responseString.value),
|
() => downloadResponse(),
|
||||||
computed(() => !!props.response && props.response.length > 0)
|
computed(() => !!props.response && props.response.length > 0)
|
||||||
)
|
)
|
||||||
defineActionHandler(
|
defineActionHandler(
|
||||||
"response.copy",
|
"response.copy",
|
||||||
() => copyResponse(responseString.value),
|
() => copyResponse(),
|
||||||
computed(() => !!props.response && props.response.length > 0)
|
computed(() => !!props.response && props.response.length > 0)
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
:src="`/images/states/${colorMode.value}/add_comment.svg`"
|
:src="`/images/states/${colorMode.value}/add_comment.svg`"
|
||||||
:alt="`${t('empty.documentation')}`"
|
:alt="`${t('empty.documentation')}`"
|
||||||
:text="t('empty.documentation')"
|
:text="t('empty.documentation')"
|
||||||
>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
v-model="graphqlFieldsFilterText"
|
v-model="graphqlFieldsFilterText"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="flex h-8 w-full bg-transparent p-4 py-2"
|
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||||
:placeholder="`${t('action.search')}`"
|
:placeholder="`${t('action.search')}`"
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions!.focus()"
|
:on-shown="() => tippyActions!.focus()"
|
||||||
>
|
>
|
||||||
<span class="truncate px-2 leading-8">
|
<span class="truncate">
|
||||||
{{ tab.document.request.name }}
|
{{ tab.document.request.name }}
|
||||||
</span>
|
</span>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const isScalar = computed(() => {
|
|||||||
|
|
||||||
function resolveRootType(type: GraphQLType) {
|
function resolveRootType(type: GraphQLType) {
|
||||||
let t = type as any
|
let t = type as any
|
||||||
while (t.ofType != null) t = t.ofType
|
while (t.ofType !== null) t = t.ofType
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
v-model="filterText"
|
v-model="filterText"
|
||||||
type="search"
|
type="search"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
class="flex h-8 w-full bg-transparent p-4 py-2"
|
class="flex w-full bg-transparent px-4 py-2 h-8"
|
||||||
:placeholder="`${t('action.search')}`"
|
:placeholder="`${t('action.search')}`"
|
||||||
/>
|
/>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
@@ -114,8 +114,7 @@
|
|||||||
:src="`/images/states/${colorMode.value}/history.svg`"
|
:src="`/images/states/${colorMode.value}/history.svg`"
|
||||||
:alt="`${t('empty.history')}`"
|
:alt="`${t('empty.history')}`"
|
||||||
:text="t('empty.history')"
|
:text="t('empty.history')"
|
||||||
>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else-if="
|
v-else-if="
|
||||||
Object.keys(filteredHistoryGroups).length === 0 ||
|
Object.keys(filteredHistoryGroups).length === 0 ||
|
||||||
@@ -124,18 +123,20 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.clear')"
|
||||||
|
outline
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
filterText = ''
|
||||||
|
filterSelection = 'ALL'
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<HoppButtonSecondary
|
|
||||||
:label="t('action.clear')"
|
|
||||||
outline
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
filterText = ''
|
|
||||||
filterSelection = 'ALL'
|
|
||||||
}
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartConfirmModal
|
<HoppSmartConfirmModal
|
||||||
:show="confirmRemove"
|
:show="confirmRemove"
|
||||||
|
|||||||
@@ -121,7 +121,8 @@ const duration = computed(() => {
|
|||||||
return responseDuration > 0
|
return responseDuration > 0
|
||||||
? `${t("request.duration")}: ${responseDuration}ms`
|
? `${t("request.duration")}: ${responseDuration}ms`
|
||||||
: t("error.no_duration")
|
: t("error.no_duration")
|
||||||
} else return t("error.no_duration")
|
}
|
||||||
|
return t("error.no_duration")
|
||||||
})
|
})
|
||||||
|
|
||||||
const entryStatus = computed(() => {
|
const entryStatus = computed(() => {
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
:label="authName"
|
:label="authName"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="tippyActions"
|
||||||
@@ -119,14 +119,16 @@
|
|||||||
:alt="`${t('empty.authorization')}`"
|
:alt="`${t('empty.authorization')}`"
|
||||||
:text="t('empty.authorization')"
|
:text="t('empty.authorization')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
outline
|
<HoppButtonSecondary
|
||||||
:label="t('app.documentation')"
|
outline
|
||||||
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
:label="t('app.documentation')"
|
||||||
blank
|
to="https://docs.hoppscotch.io/documentation/features/authorization"
|
||||||
:icon="IconExternalLink"
|
blank
|
||||||
reverse
|
:icon="IconExternalLink"
|
||||||
/>
|
reverse
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div v-else class="flex flex-1 border-b border-dividerLight">
|
<div v-else class="flex flex-1 border-b border-dividerLight">
|
||||||
<div class="w-2/3 border-r border-dividerLight">
|
<div class="w-2/3 border-r border-dividerLight">
|
||||||
@@ -149,7 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
>
|
>
|
||||||
<div class="pb-2 text-secondaryLight">
|
<div class="pb-2 text-secondaryLight">
|
||||||
{{ t("helpers.authorization") }}
|
{{ t("helpers.authorization") }}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="body.contentType || t('state.none')"
|
:label="body.contentType || t('state.none')"
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="tippyActions"
|
||||||
@@ -112,14 +112,16 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
outline
|
<HoppButtonSecondary
|
||||||
:label="`${t('app.documentation')}`"
|
outline
|
||||||
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
:label="`${t('app.documentation')}`"
|
||||||
blank
|
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
|
||||||
:icon="IconExternalLink"
|
blank
|
||||||
reverse
|
:icon="IconExternalLink"
|
||||||
/>
|
reverse
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -158,12 +158,14 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
filled
|
:label="`${t('add.new')}`"
|
||||||
:icon="IconPlus"
|
filled
|
||||||
@click="addBodyParam"
|
:icon="IconPlus"
|
||||||
/>
|
@click="addBodyParam"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -339,7 +341,7 @@ const deleteBodyParam = (index: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workingParams.value = workingParams.value.filter(
|
workingParams.value = workingParams.value.filter(
|
||||||
(_, arrIndex) => arrIndex != index
|
(_, arrIndex) => arrIndex !== index
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
placement="bottom"
|
placement="bottom"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="
|
:label="
|
||||||
CodegenDefinitions.find((x) => x.name === codegenType)!.caption
|
CodegenDefinitions.find((x) => x.name === codegenType)!.caption
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
outline
|
outline
|
||||||
class="flex-1 pr-8"
|
class="flex-1 pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div class="flex flex-col space-y-2">
|
<div class="flex flex-col space-y-2">
|
||||||
<div class="sticky top-0 z-10 flex-shrink-0 overflow-x-auto">
|
<div class="sticky top-0 z-10 flex-shrink-0 overflow-x-auto">
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
:text="`${t('state.nothing_found')} ‟${searchQuery}”`"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
|
<icon-lucide-search class="svg-icons opacity-75" />
|
||||||
</template>
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,10 +214,9 @@ const requestCode = computed(() => {
|
|||||||
if (O.isSome(result)) {
|
if (O.isSome(result)) {
|
||||||
errorState.value = false
|
errorState.value = false
|
||||||
return result.value
|
return result.value
|
||||||
} else {
|
|
||||||
errorState.value = true
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
|
errorState.value = true
|
||||||
|
return ""
|
||||||
})
|
})
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
|
|||||||
@@ -209,12 +209,14 @@
|
|||||||
:alt="`${t('empty.headers')}`"
|
:alt="`${t('empty.headers')}`"
|
||||||
:text="t('empty.headers')"
|
:text="t('empty.headers')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
:icon="IconPlus"
|
:label="`${t('add.new')}`"
|
||||||
@click="addHeader"
|
:icon="IconPlus"
|
||||||
/>
|
@click="addHeader"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -157,12 +157,14 @@
|
|||||||
:alt="`${t('empty.parameters')}`"
|
:alt="`${t('empty.parameters')}`"
|
||||||
:text="t('empty.parameters')"
|
:text="t('empty.parameters')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
:label="`${t('add.new')}`"
|
<HoppButtonSecondary
|
||||||
:icon="IconPlus"
|
:label="`${t('add.new')}`"
|
||||||
filled
|
:icon="IconPlus"
|
||||||
@click="addParam"
|
filled
|
||||||
/>
|
@click="addParam"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div ref="preRequestEditor" class="h-full"></div>
|
<div ref="preRequestEditor" class="h-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
>
|
>
|
||||||
<div class="pb-2 text-secondaryLight">
|
<div class="pb-2 text-secondaryLight">
|
||||||
{{ t("helpers.pre_request_script") }}
|
{{ t("helpers.pre_request_script") }}
|
||||||
|
|||||||
@@ -126,19 +126,19 @@ const linewrapEnabled = ref(true)
|
|||||||
const rawBodyParameters = ref<any | null>(null)
|
const rawBodyParameters = ref<any | null>(null)
|
||||||
|
|
||||||
const codemirrorValue: Ref<string | undefined> =
|
const codemirrorValue: Ref<string | undefined> =
|
||||||
typeof rawParamsBody.value == "string"
|
typeof rawParamsBody.value === "string"
|
||||||
? ref(rawParamsBody.value)
|
? ref(rawParamsBody.value)
|
||||||
: ref(undefined)
|
: ref(undefined)
|
||||||
|
|
||||||
watch(rawParamsBody, (newVal) => {
|
watch(rawParamsBody, (newVal) => {
|
||||||
typeof newVal == "string"
|
typeof newVal === "string"
|
||||||
? (codemirrorValue.value = newVal)
|
? (codemirrorValue.value = newVal)
|
||||||
: (codemirrorValue.value = undefined)
|
: (codemirrorValue.value = undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
// propagate the edits from codemirror back to the body
|
// propagate the edits from codemirror back to the body
|
||||||
watch(codemirrorValue, (updatedValue) => {
|
watch(codemirrorValue, (updatedValue) => {
|
||||||
if (updatedValue && updatedValue != rawParamsBody.value) {
|
if (updatedValue && updatedValue !== rawParamsBody.value) {
|
||||||
rawParamsBody.value = updatedValue
|
rawParamsBody.value = updatedValue
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -185,7 +185,7 @@ const prettifyRequestBody = () => {
|
|||||||
if (body.value.contentType.endsWith("json")) {
|
if (body.value.contentType.endsWith("json")) {
|
||||||
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
const jsonObj = JSON.parse(rawParamsBody.value as string)
|
||||||
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
prettifyBody = JSON.stringify(jsonObj, null, 2)
|
||||||
} else if (body.value.contentType == "application/xml") {
|
} else if (body.value.contentType === "application/xml") {
|
||||||
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
prettifyBody = prettifyXML(rawParamsBody.value as string)
|
||||||
}
|
}
|
||||||
rawParamsBody.value = prettifyBody
|
rawParamsBody.value = prettifyBody
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
class="sticky top-0 z-20 flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
class="sticky top-0 z-20 flex-none flex-shrink-0 bg-primary p-4 sm:flex sm:flex-shrink-0 sm:space-x-2"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="min-w-52 flex flex-1 whitespace-nowrap rounded border border-divider"
|
class="min-w-[12rem] flex flex-1 whitespace-nowrap rounded border border-divider"
|
||||||
>
|
>
|
||||||
<div class="relative flex">
|
<div class="relative flex">
|
||||||
<label for="method">
|
<label for="method">
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => methodTippyActions.focus()"
|
:on-shown="() => methodTippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<input
|
<input
|
||||||
id="method"
|
id="method"
|
||||||
class="flex w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
class="flex w-26 cursor-pointer rounded-l bg-primaryLight px-4 py-2 font-semibold text-secondaryDark transition"
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
:placeholder="`${t('request.method')}`"
|
:placeholder="`${t('request.method')}`"
|
||||||
@input="onSelectMethod($event)"
|
@input="onSelectMethod($event)"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="methodTippyActions"
|
ref="methodTippyActions"
|
||||||
@@ -34,6 +34,9 @@
|
|||||||
v-for="(method, index) in methods"
|
v-for="(method, index) in methods"
|
||||||
:key="`method-${index}`"
|
:key="`method-${index}`"
|
||||||
:label="method"
|
:label="method"
|
||||||
|
:style="{
|
||||||
|
color: getMethodLabelColor(method),
|
||||||
|
}"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
updateMethod(method)
|
updateMethod(method)
|
||||||
@@ -67,7 +70,7 @@
|
|||||||
'action.send'
|
'action.send'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||||
class="min-w-20 flex-1 rounded-r-none"
|
class="min-w-[5rem] flex-1 rounded-r-none"
|
||||||
@click="!loading ? newSendRequest() : cancelRequest()"
|
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||||
/>
|
/>
|
||||||
<span class="flex">
|
<span class="flex">
|
||||||
@@ -179,20 +182,16 @@
|
|||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="copyRequestAction"
|
ref="copyRequestAction"
|
||||||
:label="shareButtonText"
|
:label="t('request.share_request')"
|
||||||
:icon="copyLinkIcon"
|
:icon="IconShare2"
|
||||||
:loading="fetchingShareLink"
|
:loading="fetchingShareLink"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
copyRequest()
|
shareRequest()
|
||||||
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<HoppSmartItem
|
|
||||||
:icon="IconLink2"
|
|
||||||
:label="`${t('request.view_my_links')}`"
|
|
||||||
to="/profile"
|
|
||||||
/>
|
|
||||||
<hr />
|
<hr />
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
ref="saveRequestAction"
|
ref="saveRequestAction"
|
||||||
@@ -236,25 +235,20 @@ import { useI18n } from "@composables/i18n"
|
|||||||
import { useSetting } from "@composables/settings"
|
import { useSetting } from "@composables/settings"
|
||||||
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
|
import { useReadonlyStream, useStreamSubscriber } from "@composables/stream"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { refAutoReset, useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
import { Ref, computed, onBeforeUnmount, ref } from "vue"
|
import { Ref, computed, onBeforeUnmount, ref } from "vue"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
import { runMutation } from "~/helpers/backend/GQLClient"
|
import { runMutation } from "~/helpers/backend/GQLClient"
|
||||||
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
import { UpdateRequestDocument } from "~/helpers/backend/graphql"
|
||||||
import { createShortcode } from "~/helpers/backend/mutations/Shortcode"
|
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
import { runRESTRequest$ } from "~/helpers/RequestRunner"
|
||||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
|
||||||
import { editRESTRequest } from "~/newstore/collections"
|
import { editRESTRequest } from "~/newstore/collections"
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
import IconChevronDown from "~icons/lucide/chevron-down"
|
import IconChevronDown from "~icons/lucide/chevron-down"
|
||||||
import IconCode2 from "~icons/lucide/code-2"
|
import IconCode2 from "~icons/lucide/code-2"
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconFileCode from "~icons/lucide/file-code"
|
import IconFileCode from "~icons/lucide/file-code"
|
||||||
import IconFolderPlus from "~icons/lucide/folder-plus"
|
import IconFolderPlus from "~icons/lucide/folder-plus"
|
||||||
import IconLink2 from "~icons/lucide/link-2"
|
|
||||||
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
import IconRotateCCW from "~icons/lucide/rotate-ccw"
|
||||||
import IconSave from "~icons/lucide/save"
|
import IconSave from "~icons/lucide/save"
|
||||||
import IconShare2 from "~icons/lucide/share-2"
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
@@ -268,6 +262,7 @@ import { InterceptorService } from "~/services/interceptor.service"
|
|||||||
import { HoppTab } from "~/services/tab"
|
import { HoppTab } from "~/services/tab"
|
||||||
import { HoppRESTDocument } from "~/helpers/rest/document"
|
import { HoppRESTDocument } from "~/helpers/rest/document"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import { getMethodLabelColor } from "~/helpers/rest/labelColoring"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const interceptorService = useService(InterceptorService)
|
const interceptorService = useService(InterceptorService)
|
||||||
@@ -279,8 +274,8 @@ const methods = [
|
|||||||
"PATCH",
|
"PATCH",
|
||||||
"DELETE",
|
"DELETE",
|
||||||
"HEAD",
|
"HEAD",
|
||||||
"CONNECT",
|
|
||||||
"OPTIONS",
|
"OPTIONS",
|
||||||
|
"CONNECT",
|
||||||
"TRACE",
|
"TRACE",
|
||||||
"CUSTOM",
|
"CUSTOM",
|
||||||
]
|
]
|
||||||
@@ -309,8 +304,6 @@ const showCurlImportModal = ref(false)
|
|||||||
const showCodegenModal = ref(false)
|
const showCodegenModal = ref(false)
|
||||||
const showSaveRequestModal = ref(false)
|
const showSaveRequestModal = ref(false)
|
||||||
|
|
||||||
const hasNavigatorShare = !!navigator.share
|
|
||||||
|
|
||||||
// Template refs
|
// Template refs
|
||||||
const methodTippyActions = ref<any | null>(null)
|
const methodTippyActions = ref<any | null>(null)
|
||||||
const sendTippyActions = ref<any | null>(null)
|
const sendTippyActions = ref<any | null>(null)
|
||||||
@@ -453,62 +446,20 @@ const updateRESTResponse = (response: HoppRESTResponse | null) => {
|
|||||||
tab.value.document.response = response
|
tab.value.document.response = response
|
||||||
}
|
}
|
||||||
|
|
||||||
const copyLinkIcon = refAutoReset<
|
const currentUser = useReadonlyStream(
|
||||||
typeof IconShare2 | typeof IconCopy | typeof IconCheck
|
platform.auth.getCurrentUserStream(),
|
||||||
>(hasNavigatorShare ? IconShare2 : IconCopy, 1000)
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
const shareLink = ref<string | null>("")
|
|
||||||
const fetchingShareLink = ref(false)
|
const fetchingShareLink = ref(false)
|
||||||
|
|
||||||
const shareButtonText = computed(() => {
|
const shareRequest = () => {
|
||||||
if (shareLink.value) {
|
if (currentUser.value) {
|
||||||
return shareLink.value
|
invokeAction("share.request", {
|
||||||
} else if (fetchingShareLink.value) {
|
request: tab.value.document.request,
|
||||||
return t("state.loading")
|
|
||||||
} else {
|
|
||||||
return t("request.copy_link")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const copyRequest = async () => {
|
|
||||||
if (shareLink.value) {
|
|
||||||
copyShareLink(shareLink.value)
|
|
||||||
} else {
|
|
||||||
shareLink.value = ""
|
|
||||||
fetchingShareLink.value = true
|
|
||||||
const shortcodeResult = await createShortcode(tab.value.document.request)()
|
|
||||||
|
|
||||||
platform.analytics?.logEvent({
|
|
||||||
type: "HOPP_SHORTCODE_CREATED",
|
|
||||||
})
|
|
||||||
|
|
||||||
if (E.isLeft(shortcodeResult)) {
|
|
||||||
toast.error(`${shortcodeResult.left.error}`)
|
|
||||||
shareLink.value = `${t("error.something_went_wrong")}`
|
|
||||||
} else if (E.isRight(shortcodeResult)) {
|
|
||||||
shareLink.value = `/${shortcodeResult.right.createShortcode.id}`
|
|
||||||
copyShareLink(shareLink.value)
|
|
||||||
}
|
|
||||||
fetchingShareLink.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyShareLink = (shareLink: string) => {
|
|
||||||
const link = `${
|
|
||||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
|
||||||
}/r${shareLink}`
|
|
||||||
if (navigator.share) {
|
|
||||||
const time = new Date().toLocaleTimeString()
|
|
||||||
const date = new Date().toLocaleDateString()
|
|
||||||
navigator.share({
|
|
||||||
title: "Hoppscotch",
|
|
||||||
text: `Hoppscotch • Open source API development ecosystem at ${time} on ${date}`,
|
|
||||||
url: link,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
copyLinkIcon.value = IconCheck
|
invokeAction("modals.login.toggle")
|
||||||
copyToClipboard(link)
|
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +562,7 @@ defineActionHandler("request.send-cancel", () => {
|
|||||||
else cancelRequest()
|
else cancelRequest()
|
||||||
})
|
})
|
||||||
defineActionHandler("request.reset", clearContent)
|
defineActionHandler("request.reset", clearContent)
|
||||||
defineActionHandler("request.copy-link", copyRequest)
|
defineActionHandler("request.share-request", shareRequest)
|
||||||
defineActionHandler("request.method.next", cycleDownMethod)
|
defineActionHandler("request.method.next", cycleDownMethod)
|
||||||
defineActionHandler("request.method.prev", cycleUpMethod)
|
defineActionHandler("request.method.prev", cycleUpMethod)
|
||||||
defineActionHandler("request.save", saveRequest)
|
defineActionHandler("request.save", saveRequest)
|
||||||
|
|||||||
@@ -5,13 +5,18 @@
|
|||||||
render-inactive-tabs
|
render-inactive-tabs
|
||||||
>
|
>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('parameters') : true"
|
||||||
:id="'params'"
|
:id="'params'"
|
||||||
:label="`${t('tab.parameters')}`"
|
:label="`${t('tab.parameters')}`"
|
||||||
:info="`${newActiveParamsCount$}`"
|
:info="`${newActiveParamsCount$}`"
|
||||||
>
|
>
|
||||||
<HttpParameters v-model="request.params" />
|
<HttpParameters v-model="request.params" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('body') : true"
|
||||||
|
:id="'bodyParams'"
|
||||||
|
:label="`${t('tab.body')}`"
|
||||||
|
>
|
||||||
<HttpBody
|
<HttpBody
|
||||||
v-model:headers="request.headers"
|
v-model:headers="request.headers"
|
||||||
v-model:body="request.body"
|
v-model:body="request.body"
|
||||||
@@ -19,16 +24,22 @@
|
|||||||
/>
|
/>
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('headers') : true"
|
||||||
:id="'headers'"
|
:id="'headers'"
|
||||||
:label="`${t('tab.headers')}`"
|
:label="`${t('tab.headers')}`"
|
||||||
:info="`${newActiveHeadersCount$}`"
|
:info="`${newActiveHeadersCount$}`"
|
||||||
>
|
>
|
||||||
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
|
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab :id="'authorization'" :label="`${t('tab.authorization')}`">
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('authorization') : true"
|
||||||
|
:id="'authorization'"
|
||||||
|
:label="`${t('tab.authorization')}`"
|
||||||
|
>
|
||||||
<HttpAuthorization v-model="request.auth" />
|
<HttpAuthorization v-model="request.auth" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('preRequestScript') : true"
|
||||||
:id="'preRequestScript'"
|
:id="'preRequestScript'"
|
||||||
:label="`${t('tab.pre_request_script')}`"
|
:label="`${t('tab.pre_request_script')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -40,6 +51,7 @@
|
|||||||
<HttpPreRequestScript v-model="request.preRequestScript" />
|
<HttpPreRequestScript v-model="request.preRequestScript" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
<HoppSmartTab
|
<HoppSmartTab
|
||||||
|
v-if="properties ? properties.includes('tests') : true"
|
||||||
:id="'tests'"
|
:id="'tests'"
|
||||||
:label="`${t('tab.tests')}`"
|
:label="`${t('tab.tests')}`"
|
||||||
:indicator="
|
:indicator="
|
||||||
@@ -76,6 +88,7 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue: HoppRESTRequest
|
modelValue: HoppRESTRequest
|
||||||
optionTab: RESTOptionTabs
|
optionTab: RESTOptionTabs
|
||||||
|
properties?: string[]
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
optionTab: "params",
|
optionTab: "params",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative flex flex-1 flex-col">
|
<div class="relative flex flex-1 flex-col">
|
||||||
<HttpResponseMeta :response="doc.response" />
|
<HttpResponseMeta :response="doc.response" :is-embed="isEmbed" />
|
||||||
<LensesResponseBodyRenderer
|
<LensesResponseBodyRenderer
|
||||||
v-if="!loading && hasResponse"
|
v-if="!loading && hasResponse"
|
||||||
v-model:document="doc"
|
v-model:document="doc"
|
||||||
@@ -15,6 +15,7 @@ import { HoppRESTDocument } from "~/helpers/rest/document"
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
document: HoppRESTDocument
|
document: HoppRESTDocument
|
||||||
|
isEmbed: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
|
|||||||
@@ -2,8 +2,20 @@
|
|||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4"
|
class="sticky top-0 z-10 flex flex-shrink-0 items-center justify-center overflow-auto overflow-x-auto whitespace-nowrap bg-primary p-4"
|
||||||
>
|
>
|
||||||
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
|
<AppShortcutsPrompt v-if="response == null && !isEmbed" class="flex-1" />
|
||||||
<div v-else class="flex flex-1 flex-col">
|
|
||||||
|
<div v-if="response == null && isEmbed">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="`${t('app.documentation')}`"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/rest-api-testing#response"
|
||||||
|
:icon="IconExternalLink"
|
||||||
|
blank
|
||||||
|
outline
|
||||||
|
reverse
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="response" class="flex flex-1 flex-col">
|
||||||
<div
|
<div
|
||||||
v-if="response.type === 'loading'"
|
v-if="response.type === 'loading'"
|
||||||
class="flex flex-col items-center justify-center"
|
class="flex flex-col items-center justify-center"
|
||||||
@@ -25,7 +37,9 @@
|
|||||||
:text="t('helpers.network_fail')"
|
:text="t('helpers.network_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
<template #body>
|
||||||
|
<AppInterceptor class="rounded border border-dividerLight p-2" />
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-if="response.type === 'script_fail'"
|
v-if="response.type === 'script_fail'"
|
||||||
@@ -35,12 +49,14 @@
|
|||||||
:text="t('helpers.script_fail')"
|
:text="t('helpers.script_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<div
|
<template #body>
|
||||||
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
<div
|
||||||
>
|
class="mt-2 w-full overflow-auto whitespace-normal rounded bg-primaryLight px-4 py-2 font-mono text-red-400"
|
||||||
{{ response.error.name }}: {{ response.error.message }}<br />
|
>
|
||||||
{{ response.error.stack }}
|
{{ response.error.name }}: {{ response.error.message }}<br />
|
||||||
</div>
|
{{ response.error.stack }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<div
|
<div
|
||||||
v-if="response.type === 'success' || response.type === 'fail'"
|
v-if="response.type === 'success' || response.type === 'fail'"
|
||||||
@@ -53,7 +69,12 @@
|
|||||||
<span v-if="response.statusCode">
|
<span v-if="response.statusCode">
|
||||||
<span class="text-secondary"> {{ t("response.status") }}: </span>
|
<span class="text-secondary"> {{ t("response.status") }}: </span>
|
||||||
{{ `${response.statusCode}\xA0 • \xA0`
|
{{ `${response.statusCode}\xA0 • \xA0`
|
||||||
}}{{ getStatusCodeReasonPhrase(response.statusCode) }}
|
}}{{
|
||||||
|
getStatusCodeReasonPhrase(
|
||||||
|
response.statusCode,
|
||||||
|
response.statusText
|
||||||
|
)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="response.meta && response.meta.responseDuration">
|
<span v-if="response.meta && response.meta.responseDuration">
|
||||||
<span class="text-secondary"> {{ t("response.time") }}: </span>
|
<span class="text-secondary"> {{ t("response.time") }}: </span>
|
||||||
@@ -100,6 +121,7 @@ import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
|
|||||||
import { useService } from "dioc/vue"
|
import { useService } from "dioc/vue"
|
||||||
import { InspectionService } from "~/services/inspection"
|
import { InspectionService } from "~/services/inspection"
|
||||||
import { RESTTabService } from "~/services/tab/rest"
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -107,6 +129,7 @@ const tabs = useService(RESTTabService)
|
|||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
response: HoppRESTResponse | null | undefined
|
response: HoppRESTResponse | null | undefined
|
||||||
|
isEmbed?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,6 +26,13 @@
|
|||||||
>
|
>
|
||||||
<History :page="'rest'" />
|
<History :page="'rest'" />
|
||||||
</HoppSmartTab>
|
</HoppSmartTab>
|
||||||
|
<HoppSmartTab
|
||||||
|
:id="'share-request'"
|
||||||
|
:icon="IconShare2"
|
||||||
|
:label="`${t('tab.shared_requests')}`"
|
||||||
|
>
|
||||||
|
<Share />
|
||||||
|
</HoppSmartTab>
|
||||||
</HoppSmartTabs>
|
</HoppSmartTabs>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -33,6 +40,7 @@
|
|||||||
import IconClock from "~icons/lucide/clock"
|
import IconClock from "~icons/lucide/clock"
|
||||||
import IconLayers from "~icons/lucide/layers"
|
import IconLayers from "~icons/lucide/layers"
|
||||||
import IconFolder from "~icons/lucide/folder"
|
import IconFolder from "~icons/lucide/folder"
|
||||||
|
import IconShare2 from "~icons/lucide/share-2"
|
||||||
import { ref } from "vue"
|
import { ref } from "vue"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,11 @@
|
|||||||
@click.middle="emit('close-tab')"
|
@click.middle="emit('close-tab')"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-tiny font-semibold"
|
class="text-tiny font-semibold mr-2"
|
||||||
:style="{ color: getMethodLabelColorClassOf(tab.document.request) }"
|
:style="{ color: getMethodLabelColorClassOf(tab.document.request) }"
|
||||||
>
|
>
|
||||||
{{ tab.document.request.method }}
|
{{ tab.document.request.method }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<tippy
|
<tippy
|
||||||
ref="options"
|
ref="options"
|
||||||
trigger="manual"
|
trigger="manual"
|
||||||
@@ -21,7 +20,7 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions!.focus()"
|
:on-shown="() => tippyActions!.focus()"
|
||||||
>
|
>
|
||||||
<span class="truncate px-2 leading-8">
|
<span class="truncate">
|
||||||
{{ tab.document.request.name }}
|
{{ tab.document.request.name }}
|
||||||
</span>
|
</span>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
<div class="divide-y divide-dividerLight">
|
<div class="divide-y divide-dividerLight">
|
||||||
<div
|
<div
|
||||||
v-if="noEnvSelected && !globalHasAdditions"
|
v-if="noEnvSelected && !globalHasAdditions"
|
||||||
class="flex bg-error p-4 text-secondaryDark"
|
class="flex bg-bannerInfo p-4 text-secondaryDark"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<icon-lucide-alert-triangle class="svg-icons mr-4" />
|
<icon-lucide-alert-triangle class="svg-icons mr-4" />
|
||||||
@@ -159,8 +159,7 @@
|
|||||||
:alt="`${t('error.test_script_fail')}`"
|
:alt="`${t('error.test_script_fail')}`"
|
||||||
:heading="t('error.test_script_fail')"
|
:heading="t('error.test_script_fail')"
|
||||||
:text="t('helpers.test_script_fail')"
|
:text="t('helpers.test_script_fail')"
|
||||||
>
|
/>
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<HoppSmartPlaceholder
|
<HoppSmartPlaceholder
|
||||||
v-else
|
v-else
|
||||||
:src="`/images/states/${colorMode.value}/validation.svg`"
|
:src="`/images/states/${colorMode.value}/validation.svg`"
|
||||||
@@ -168,15 +167,16 @@
|
|||||||
:heading="t('empty.tests')"
|
:heading="t('empty.tests')"
|
||||||
:text="t('helpers.tests')"
|
:text="t('helpers.tests')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
outline
|
<HoppButtonSecondary
|
||||||
:label="`${t('action.learn_more')}`"
|
outline
|
||||||
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
|
:label="`${t('action.learn_more')}`"
|
||||||
blank
|
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
|
||||||
:icon="IconExternalLink"
|
blank
|
||||||
reverse
|
:icon="IconExternalLink"
|
||||||
class="my-4"
|
reverse
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
<EnvironmentsMyDetails
|
<EnvironmentsMyDetails
|
||||||
:show="showMyEnvironmentDetailsModal"
|
:show="showMyEnvironmentDetailsModal"
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<div ref="testScriptEditor" class="h-full"></div>
|
<div ref="testScriptEditor" class="h-full"></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="z-9 sticky top-upperTertiaryStickyFold h-full min-w-46 max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
class="z-[9] sticky top-upperTertiaryStickyFold h-full min-w-[12rem] max-w-1/3 flex-shrink-0 overflow-auto overflow-x-auto bg-primary p-4"
|
||||||
>
|
>
|
||||||
<div class="pb-2 text-secondaryLight">
|
<div class="pb-2 text-secondaryLight">
|
||||||
{{ t("helpers.post_request_tests") }}
|
{{ t("helpers.post_request_tests") }}
|
||||||
|
|||||||
@@ -149,12 +149,14 @@
|
|||||||
:alt="`${t('empty.body')}`"
|
:alt="`${t('empty.body')}`"
|
||||||
:text="t('empty.body')"
|
:text="t('empty.body')"
|
||||||
>
|
>
|
||||||
<HoppButtonSecondary
|
<template #body>
|
||||||
filled
|
<HoppButtonSecondary
|
||||||
:label="`${t('add.new')}`"
|
filled
|
||||||
:icon="IconPlus"
|
:label="`${t('add.new')}`"
|
||||||
@click="addUrlEncodedParam"
|
:icon="IconPlus"
|
||||||
/>
|
@click="addUrlEncodedParam"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,7 +244,7 @@ const urlEncodedParamsRaw = pluckRef(body, "body")
|
|||||||
|
|
||||||
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
const urlEncodedParams = computed<RawKeyValueEntry[]>({
|
||||||
get() {
|
get() {
|
||||||
return typeof urlEncodedParamsRaw.value == "string"
|
return typeof urlEncodedParamsRaw.value === "string"
|
||||||
? parseRawKeyValueEntries(urlEncodedParamsRaw.value)
|
? parseRawKeyValueEntries(urlEncodedParamsRaw.value)
|
||||||
: []
|
: []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => authTippyActions.focus()"
|
:on-shown="() => authTippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="auth.addTo || t('state.none')"
|
:label="auth.addTo || t('state.none')"
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="authTippyActions"
|
ref="authTippyActions"
|
||||||
|
|||||||
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
163
packages/hoppscotch-common/src/components/importExport/Base.vue
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
dialog
|
||||||
|
:title="t(modalTitle)"
|
||||||
|
styles="sm:max-w-md"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
|
<template #actions>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-if="hasPreviousStep"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.go_back')"
|
||||||
|
:icon="IconArrowLeft"
|
||||||
|
@click="goToPreviousStep"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<component :is="currentStep.component" v-bind="currentStep.props()" />
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconArrowLeft from "~icons/lucide/arrow-left"
|
||||||
|
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { PropType, ref } from "vue"
|
||||||
|
|
||||||
|
import { useSteps, defineStep } from "~/composables/step-components"
|
||||||
|
import ImportExportList from "./ImportExportList.vue"
|
||||||
|
|
||||||
|
import ImportExportSourcesList from "./ImportExportSourcesList.vue"
|
||||||
|
import { ImporterOrExporter } from "~/components/importExport/types"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
importerModules: {
|
||||||
|
// type: Array as PropType<ReturnType<typeof defineImporter>[]>,
|
||||||
|
type: Array as PropType<ImporterOrExporter[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
exporterModules: {
|
||||||
|
type: Array as PropType<ImporterOrExporter[]>,
|
||||||
|
default: () => [],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
addStep,
|
||||||
|
currentStep,
|
||||||
|
goToStep,
|
||||||
|
goToNextStep,
|
||||||
|
goToPreviousStep,
|
||||||
|
hasPreviousStep,
|
||||||
|
} = useSteps()
|
||||||
|
|
||||||
|
const selectedImporterID = ref<string | null>(null)
|
||||||
|
const selectedSourceID = ref<string | null>(null)
|
||||||
|
|
||||||
|
const chooseImporterOrExporter = defineStep(
|
||||||
|
"choose_importer_or_exporter",
|
||||||
|
ImportExportList,
|
||||||
|
() => ({
|
||||||
|
importers: props.importerModules.map((importer) => ({
|
||||||
|
id: importer.metadata.id,
|
||||||
|
name: importer.metadata.name,
|
||||||
|
title: importer.metadata.title,
|
||||||
|
icon: importer.metadata.icon,
|
||||||
|
disabled: importer.metadata.disabled,
|
||||||
|
})),
|
||||||
|
exporters: props.exporterModules.map((exporter) => ({
|
||||||
|
id: exporter.metadata.id,
|
||||||
|
name: exporter.metadata.name,
|
||||||
|
title: exporter.metadata.title,
|
||||||
|
icon: exporter.metadata.icon,
|
||||||
|
disabled: exporter.metadata.disabled,
|
||||||
|
loading: exporter.metadata.isLoading?.value ?? false,
|
||||||
|
})),
|
||||||
|
"onImporter-selected": (id: string) => {
|
||||||
|
selectedImporterID.value = id
|
||||||
|
|
||||||
|
const selectedImporter = props.importerModules.find(
|
||||||
|
(i) => i.metadata.id === id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedImporter?.supported_sources) goToNextStep()
|
||||||
|
else if (selectedImporter?.component)
|
||||||
|
goToStep(selectedImporter.component.id)
|
||||||
|
},
|
||||||
|
"onExporter-selected": (id: string) => {
|
||||||
|
const selectedExporter = props.exporterModules.find(
|
||||||
|
(i) => i.metadata.id === id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (selectedExporter && selectedExporter.action) {
|
||||||
|
selectedExporter.action()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const chooseImportSource = defineStep(
|
||||||
|
"choose_import_source",
|
||||||
|
ImportExportSourcesList,
|
||||||
|
() => {
|
||||||
|
const currentImporter = props.importerModules.find(
|
||||||
|
(i) => i.metadata.id === selectedImporterID.value
|
||||||
|
)
|
||||||
|
|
||||||
|
const sources = currentImporter?.supported_sources
|
||||||
|
|
||||||
|
if (!sources)
|
||||||
|
return {
|
||||||
|
sources: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
sources.forEach((source) => {
|
||||||
|
addStep(source.step)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
sources: sources.map((source) => ({
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
icon: source.icon,
|
||||||
|
})),
|
||||||
|
"onImport-source-selected": (sourceID) => {
|
||||||
|
selectedSourceID.value = sourceID
|
||||||
|
|
||||||
|
const sourceStep = sources.find((s) => s.id === sourceID)?.step
|
||||||
|
|
||||||
|
if (sourceStep) {
|
||||||
|
goToStep(sourceStep.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
addStep(chooseImporterOrExporter)
|
||||||
|
addStep(chooseImportSource)
|
||||||
|
|
||||||
|
props.importerModules.forEach((importer) => {
|
||||||
|
if (importer.component) {
|
||||||
|
addStep(importer.component)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
// resetImport()
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<HoppSmartExpand>
|
||||||
|
<template #body>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-for="importer in importers"
|
||||||
|
:key="importer.id"
|
||||||
|
:icon="importer.icon"
|
||||||
|
:label="t(`${importer.name}`)"
|
||||||
|
@click="emit('importer-selected', importer.id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartExpand>
|
||||||
|
<hr />
|
||||||
|
<div class="flex flex-col space-y-2">
|
||||||
|
<template v-for="exporter in exporters" :key="exporter.id">
|
||||||
|
<!-- adding the title to a span if the item is visible, otherwise the title won't be shown -->
|
||||||
|
|
||||||
|
<span
|
||||||
|
v-if="exporter.disabled && exporter.title"
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t(`${exporter.title}`)"
|
||||||
|
class="flex"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="exporter.icon"
|
||||||
|
:label="t(`${exporter.name}`)"
|
||||||
|
:disabled="exporter.disabled"
|
||||||
|
:loading="exporter.loading"
|
||||||
|
@click="emit('exporter-selected', exporter.id)"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<HoppSmartItem
|
||||||
|
v-else
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="exporter.icon"
|
||||||
|
:title="t(`${exporter.title}`)"
|
||||||
|
:label="t(`${exporter.name}`)"
|
||||||
|
:loading="exporter.loading"
|
||||||
|
:disabled="exporter.disabled"
|
||||||
|
@click="emit('exporter-selected', exporter.id)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { Component } from "vue"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
type ImportExportEntryMeta = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: Component
|
||||||
|
disabled: boolean
|
||||||
|
title?: string
|
||||||
|
loading?: boolean
|
||||||
|
isVisible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
importers: ImportExportEntryMeta[]
|
||||||
|
exporters: ImportExportEntryMeta[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "importer-selected", importerID: string): void
|
||||||
|
(e: "exporter-selected", exporterID: string): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<HoppSmartItem
|
||||||
|
v-for="source in sources"
|
||||||
|
:key="source.id"
|
||||||
|
:icon="source.icon"
|
||||||
|
:label="t(`${source.name}`)"
|
||||||
|
@click="emit('import-source-selected', source.id)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { Component } from "vue"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
type ListItemMeta = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: Component
|
||||||
|
title?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
sources: ListItemMeta[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "import-source-selected", sourceID: string): void
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||||
|
:class="{
|
||||||
|
'!text-green-500': hasFile,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<icon-lucide-check-circle class="svg-icons" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ t(`${caption}`) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="flex flex-col ml-10 border border-dashed rounded border-dividerDark"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="inputChooseFileToImportFrom"
|
||||||
|
ref="inputChooseFileToImportFrom"
|
||||||
|
name="inputChooseFileToImportFrom"
|
||||||
|
type="file"
|
||||||
|
class="p-4 cursor-pointer transition file:transition file:cursor-pointer text-secondary hover:text-secondaryDark file:mr-2 file:py-2 file:px-4 file:rounded file:border-0 file:text-secondary hover:file:text-secondaryDark file:bg-primaryLight hover:file:bg-primaryDark"
|
||||||
|
:accept="acceptedFileTypes"
|
||||||
|
@change="onFileChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
class="w-full"
|
||||||
|
:label="t('import.title')"
|
||||||
|
:disabled="!hasFile"
|
||||||
|
@click="emit('importFromFile', fileContent)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useToast } from "@composables/toast"
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
caption: string
|
||||||
|
acceptedFileTypes: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const hasFile = ref(false)
|
||||||
|
const fileContent = ref("")
|
||||||
|
|
||||||
|
const inputChooseFileToImportFrom = ref<HTMLInputElement | any>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "importFromFile", content: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const onFileChange = () => {
|
||||||
|
const inputFileToImport = inputChooseFileToImportFrom.value
|
||||||
|
|
||||||
|
if (!inputFileToImport) {
|
||||||
|
hasFile.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inputFileToImport.files || inputFileToImport.files.length === 0) {
|
||||||
|
inputChooseFileToImportFrom.value[0].value = ""
|
||||||
|
hasFile.value = false
|
||||||
|
toast.show(t("action.choose_file").toString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
|
||||||
|
reader.onload = ({ target }) => {
|
||||||
|
const content = target!.result as string | null
|
||||||
|
if (!content) {
|
||||||
|
hasFile.value = false
|
||||||
|
toast.show(t("action.choose_file").toString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContent.value = content
|
||||||
|
|
||||||
|
hasFile.value = !!content?.length
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(inputFileToImport.files[0])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="mySelectedCollectionID"
|
||||||
|
autocomplete="off"
|
||||||
|
class="select"
|
||||||
|
autofocus
|
||||||
|
>
|
||||||
|
<option :key="undefined" :value="undefined" disabled selected>
|
||||||
|
{{ t("collection.select") }}
|
||||||
|
</option>
|
||||||
|
<option
|
||||||
|
v-for="(collection, collectionIndex) in myCollections"
|
||||||
|
:key="`collection-${collectionIndex}`"
|
||||||
|
:value="collectionIndex"
|
||||||
|
class="bg-primary"
|
||||||
|
>
|
||||||
|
{{ collection.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
class="w-full"
|
||||||
|
:label="t('import.title')"
|
||||||
|
:disabled="!hasSelectedCollectionID"
|
||||||
|
@click="fetchCollectionFromMyCollections"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { getRESTCollection, restCollections$ } from "~/newstore/collections"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const mySelectedCollectionID = ref<number | undefined>(undefined)
|
||||||
|
|
||||||
|
const hasSelectedCollectionID = computed(() => {
|
||||||
|
return mySelectedCollectionID.value !== undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const myCollections = useReadonlyStream(restCollections$, [])
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "importFromMyCollection", content: HoppCollection<HoppRESTRequest>): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const fetchCollectionFromMyCollections = async () => {
|
||||||
|
if (mySelectedCollectionID.value === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection = getRESTCollection(mySelectedCollectionID.value)
|
||||||
|
|
||||||
|
if (collection) {
|
||||||
|
emit("importFromMyCollection", collection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<template>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="flex items-center">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center justify-center flex-shrink-0 mr-4 border-4 rounded-full border-primary text-dividerDark"
|
||||||
|
:class="{
|
||||||
|
'!text-green-500': hasURL,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<icon-lucide-check-circle class="svg-icons" />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ t(caption) }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p class="flex flex-col ml-10">
|
||||||
|
<input
|
||||||
|
v-model="inputChooseGistToImportFrom"
|
||||||
|
type="url"
|
||||||
|
class="input"
|
||||||
|
:placeholder="`${t('import.from_url')}`"
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
class="w-full"
|
||||||
|
:label="t('import.title')"
|
||||||
|
:disabled="!hasURL"
|
||||||
|
:loading="isFetchingUrl"
|
||||||
|
@click="fetchUrlData"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import axios, { AxiosResponse } from "axios"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
caption: string
|
||||||
|
fetchLogic?: (url: string) => Promise<AxiosResponse<any>>
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "importFromURL", content: unknown): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const inputChooseGistToImportFrom = ref<string>("")
|
||||||
|
const hasURL = ref(false)
|
||||||
|
|
||||||
|
const isFetchingUrl = ref(false)
|
||||||
|
|
||||||
|
watch(inputChooseGistToImportFrom, (url) => {
|
||||||
|
hasURL.value = !!url
|
||||||
|
})
|
||||||
|
|
||||||
|
const urlFetchLogic =
|
||||||
|
props.fetchLogic ??
|
||||||
|
async function (url: string) {
|
||||||
|
const res = await axios.get(url, {
|
||||||
|
transitional: {
|
||||||
|
forcedJSONParsing: false,
|
||||||
|
silentJSONParsing: false,
|
||||||
|
clarifyTimeoutError: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUrlData() {
|
||||||
|
isFetchingUrl.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await urlFetchLogic(inputChooseGistToImportFrom.value)
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
emit("importFromURL", res.data)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(t("import.failed"))
|
||||||
|
console.log(e)
|
||||||
|
} finally {
|
||||||
|
isFetchingUrl.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Component, Ref } from "vue"
|
||||||
|
import { defineStep } from "~/composables/step-components"
|
||||||
|
|
||||||
|
// TODO: move the metadata except disabled and isLoading to importers.ts
|
||||||
|
export type ImporterOrExporter = {
|
||||||
|
metadata: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: any
|
||||||
|
title: string
|
||||||
|
disabled: boolean
|
||||||
|
applicableTo: Array<"personal-workspace" | "team-workspace" | "url-import">
|
||||||
|
isLoading?: Ref<boolean>
|
||||||
|
}
|
||||||
|
supported_sources?: {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
icon: Component
|
||||||
|
step: ReturnType<typeof defineStep>
|
||||||
|
}[]
|
||||||
|
component?: ReturnType<typeof defineStep>
|
||||||
|
action?: (...args: any[]) => any
|
||||||
|
}
|
||||||
@@ -5,55 +5,57 @@
|
|||||||
:heading="t('error.network_fail')"
|
:heading="t('error.network_fail')"
|
||||||
large
|
large
|
||||||
>
|
>
|
||||||
<div class="my-1 flex flex-col items-center text-secondaryLight">
|
<template #body>
|
||||||
<span>
|
<div class="my-1 flex flex-col items-center text-secondaryLight">
|
||||||
{{ t("error.please_install_extension") }}
|
<span>
|
||||||
</span>
|
{{ t("error.please_install_extension") }}
|
||||||
<span>
|
</span>
|
||||||
{{ t("error.check_how_to_add_origin") }}
|
<span>
|
||||||
<HoppSmartLink
|
{{ t("error.check_how_to_add_origin") }}
|
||||||
blank
|
<HoppSmartLink
|
||||||
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
|
blank
|
||||||
class="text-accent hover:text-accentDark"
|
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
|
||||||
>
|
class="text-accent hover:text-accentDark"
|
||||||
here
|
>
|
||||||
</HoppSmartLink>
|
here
|
||||||
</span>
|
</HoppSmartLink>
|
||||||
</div>
|
</span>
|
||||||
<div class="flex flex-col space-y-2 py-4">
|
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
|
||||||
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
|
||||||
blank
|
|
||||||
:icon="IconChrome"
|
|
||||||
label="Chrome"
|
|
||||||
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
|
|
||||||
:active-info-icon="hasChromeExtInstalled"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<HoppSmartItem
|
|
||||||
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
|
||||||
blank
|
|
||||||
:icon="IconFirefox"
|
|
||||||
label="Firefox"
|
|
||||||
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
|
|
||||||
:active-info-icon="hasFirefoxExtInstalled"
|
|
||||||
outline
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-4 py-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<HoppSmartToggle
|
|
||||||
:on="extensionEnabled"
|
|
||||||
@change="extensionEnabled = !extensionEnabled"
|
|
||||||
>
|
|
||||||
{{ t("settings.extensions_use_toggle") }}
|
|
||||||
</HoppSmartToggle>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col space-y-2 py-4">
|
||||||
|
<span>
|
||||||
|
<HoppSmartItem
|
||||||
|
to="https://chrome.google.com/webstore/detail/hoppscotch-browser-extens/amknoiejhlmhancpahfcfcfhllgkpbld"
|
||||||
|
blank
|
||||||
|
:icon="IconChrome"
|
||||||
|
label="Chrome"
|
||||||
|
:info-icon="hasChromeExtInstalled ? IconCheckCircle : null"
|
||||||
|
:active-info-icon="hasChromeExtInstalled"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<HoppSmartItem
|
||||||
|
to="https://addons.mozilla.org/en-US/firefox/addon/hoppscotch"
|
||||||
|
blank
|
||||||
|
:icon="IconFirefox"
|
||||||
|
label="Firefox"
|
||||||
|
:info-icon="hasFirefoxExtInstalled ? IconCheckCircle : null"
|
||||||
|
:active-info-icon="hasFirefoxExtInstalled"
|
||||||
|
outline
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<HoppSmartToggle
|
||||||
|
:on="extensionEnabled"
|
||||||
|
@change="extensionEnabled = !extensionEnabled"
|
||||||
|
>
|
||||||
|
{{ t("settings.extensions_use_toggle") }}
|
||||||
|
</HoppSmartToggle>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</HoppSmartPlaceholder>
|
</HoppSmartPlaceholder>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,39 @@
|
|||||||
:icon="copyIcon"
|
:icon="copyIcon"
|
||||||
@click="copyResponse"
|
@click="copyResponse"
|
||||||
/>
|
/>
|
||||||
|
<tippy
|
||||||
|
v-if="response.body"
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => copyInterfaceTippyActions.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('app.copy_interface_type')"
|
||||||
|
:icon="IconMore"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="copyInterfaceTippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
v-for="(language, index) in interfaceLanguages"
|
||||||
|
:key="index"
|
||||||
|
:label="language"
|
||||||
|
:icon="
|
||||||
|
copiedInterfaceLanguage === language
|
||||||
|
? copyInterfaceIcon
|
||||||
|
: IconCopy
|
||||||
|
"
|
||||||
|
@click="runCopyInterface(language)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -201,7 +234,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import IconWrapText from "~icons/lucide/wrap-text"
|
import IconWrapText from "~icons/lucide/wrap-text"
|
||||||
import IconFilter from "~icons/lucide/filter"
|
import IconFilter from "~icons/lucide/filter"
|
||||||
|
import IconMore from "~icons/lucide/more-horizontal"
|
||||||
import IconHelpCircle from "~icons/lucide/help-circle"
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import IconCopy from "~icons/lucide/copy"
|
||||||
import * as LJSON from "lossless-json"
|
import * as LJSON from "lossless-json"
|
||||||
import * as O from "fp-ts/Option"
|
import * as O from "fp-ts/Option"
|
||||||
import * as E from "fp-ts/Either"
|
import * as E from "fp-ts/Either"
|
||||||
@@ -221,9 +256,11 @@ import {
|
|||||||
useCopyResponse,
|
useCopyResponse,
|
||||||
useResponseBody,
|
useResponseBody,
|
||||||
useDownloadResponse,
|
useDownloadResponse,
|
||||||
|
useCopyInterface,
|
||||||
} from "@composables/lens-actions"
|
} from "@composables/lens-actions"
|
||||||
import { defineActionHandler } from "~/helpers/actions"
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
import interfaceLanguages from "~/helpers/utils/interfaceLanguages"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
|
|
||||||
@@ -235,6 +272,13 @@ const { responseBodyText } = useResponseBody(props.response)
|
|||||||
|
|
||||||
const toggleFilter = ref(false)
|
const toggleFilter = ref(false)
|
||||||
const filterQueryText = ref("")
|
const filterQueryText = ref("")
|
||||||
|
const copiedInterfaceLanguage = ref("")
|
||||||
|
|
||||||
|
const runCopyInterface = (language: string) => {
|
||||||
|
copyInterface(language).then(() => {
|
||||||
|
copiedInterfaceLanguage.value = language
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type BodyParseError =
|
type BodyParseError =
|
||||||
| { type: "JSON_PARSE_FAILED" }
|
| { type: "JSON_PARSE_FAILED" }
|
||||||
@@ -269,9 +313,8 @@ const jsonResponseBodyText = computed(() => {
|
|||||||
),
|
),
|
||||||
E.map(JSON.stringify)
|
E.map(JSON.stringify)
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
return E.right(responseBodyText.value)
|
|
||||||
}
|
}
|
||||||
|
return E.right(responseBodyText.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
const jsonBodyText = computed(() =>
|
const jsonBodyText = computed(() =>
|
||||||
@@ -319,6 +362,7 @@ const filterResponseError = computed(() =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
|
const { copyIcon, copyResponse } = useCopyResponse(jsonBodyText)
|
||||||
|
const { copyInterfaceIcon, copyInterface } = useCopyInterface(jsonBodyText)
|
||||||
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
const { downloadIcon, downloadResponse } = useDownloadResponse(
|
||||||
"application/json",
|
"application/json",
|
||||||
jsonBodyText
|
jsonBodyText
|
||||||
@@ -327,6 +371,7 @@ const { downloadIcon, downloadResponse } = useDownloadResponse(
|
|||||||
// Template refs
|
// Template refs
|
||||||
const tippyActions = ref<any | null>(null)
|
const tippyActions = ref<any | null>(null)
|
||||||
const jsonResponse = ref<any | null>(null)
|
const jsonResponse = ref<any | null>(null)
|
||||||
|
const copyInterfaceTippyActions = ref<any | null>(null)
|
||||||
const linewrapEnabled = ref(true)
|
const linewrapEnabled = ref(true)
|
||||||
|
|
||||||
const { cursor } = useCodemirror(
|
const { cursor } = useCodemirror(
|
||||||
|
|||||||
@@ -5,12 +5,11 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
responseBodyText() {
|
responseBodyText() {
|
||||||
if (typeof this.response.body === "string") return this.response.body
|
if (typeof this.response.body === "string") return this.response.body
|
||||||
else {
|
|
||||||
const res = new TextDecoder("utf-8").decode(this.response.body)
|
|
||||||
|
|
||||||
// HACK: Temporary trailing null character issue from the extension fix
|
const res = new TextDecoder("utf-8").decode(this.response.body)
|
||||||
return res.replace(/\0+$/, "")
|
|
||||||
}
|
// HACK: Temporary trailing null character issue from the extension fix
|
||||||
|
return res.replace(/\0+$/, "")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
class="my-6 block w-full divide-y divide-dividerLight border border-dividerLight lg:my-0 lg:flex lg:divide-x lg:divide-y-0 lg:border-0"
|
|
||||||
>
|
|
||||||
<div class="table-box font-mono text-tiny">
|
|
||||||
{{ shortcode.id }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box" :class="requestLabelColor">
|
|
||||||
{{ parseShortcodeRequest.method }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box">
|
|
||||||
{{ parseShortcodeRequest.endpoint }}
|
|
||||||
</div>
|
|
||||||
<div ref="timeStampRef" class="table-box">
|
|
||||||
{{ dateStamp }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box justify-center">
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.open_workspace')"
|
|
||||||
:to="`${shortcodeBaseURL}/r/${shortcode.id}`"
|
|
||||||
blank
|
|
||||||
:icon="IconExternalLink"
|
|
||||||
class="px-3 text-accent hover:text-accent"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.copy')"
|
|
||||||
color="green"
|
|
||||||
:icon="copyIconRefs"
|
|
||||||
class="px-3"
|
|
||||||
@click="copyShortcode(shortcode.id)"
|
|
||||||
/>
|
|
||||||
<HoppButtonSecondary
|
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
|
||||||
:title="t('action.delete')"
|
|
||||||
:icon="IconTrash"
|
|
||||||
color="red"
|
|
||||||
class="px-3"
|
|
||||||
@click="deleteShortcode(shortcode.id)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from "vue"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as RR from "fp-ts/ReadonlyRecord"
|
|
||||||
import * as O from "fp-ts/Option"
|
|
||||||
import { translateToNewRequest } from "@hoppscotch/data"
|
|
||||||
import { refAutoReset } from "@vueuse/core"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
|
||||||
import { Shortcode } from "~/helpers/shortcodes/Shortcode"
|
|
||||||
import { shortDateTime } from "~/helpers/utils/date"
|
|
||||||
|
|
||||||
import IconTrash from "~icons/lucide/trash"
|
|
||||||
import IconExternalLink from "~icons/lucide/external-link"
|
|
||||||
import IconCopy from "~icons/lucide/copy"
|
|
||||||
import IconCheck from "~icons/lucide/check"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
shortcode: Shortcode
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
|
||||||
(e: "delete-shortcode", codeID: string): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const deleteShortcode = (codeID: string) => {
|
|
||||||
emit("delete-shortcode", codeID)
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestMethodLabels = {
|
|
||||||
get: "text-green-500",
|
|
||||||
post: "text-yellow-500",
|
|
||||||
put: "text-blue-500",
|
|
||||||
delete: "text-red-500",
|
|
||||||
default: "text-gray-500",
|
|
||||||
} as const
|
|
||||||
|
|
||||||
const timeStampRef = ref()
|
|
||||||
|
|
||||||
const copyIconRefs = refAutoReset<typeof IconCopy | typeof IconCheck>(
|
|
||||||
IconCopy,
|
|
||||||
1000
|
|
||||||
)
|
|
||||||
|
|
||||||
const parseShortcodeRequest = computed(() =>
|
|
||||||
pipe(props.shortcode.request, JSON.parse, translateToNewRequest)
|
|
||||||
)
|
|
||||||
|
|
||||||
const requestLabelColor = computed(() =>
|
|
||||||
pipe(
|
|
||||||
requestMethodLabels,
|
|
||||||
RR.lookup(parseShortcodeRequest.value.method.toLowerCase()),
|
|
||||||
O.getOrElseW(() => requestMethodLabels.default)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
const dateStamp = computed(() => shortDateTime(props.shortcode.createdOn))
|
|
||||||
|
|
||||||
const shortcodeBaseURL =
|
|
||||||
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
|
||||||
|
|
||||||
const copyShortcode = (codeID: string) => {
|
|
||||||
copyToClipboard(`${shortcodeBaseURL}/r/${codeID}`)
|
|
||||||
toast.success(`${t("state.copied_to_clipboard")}`)
|
|
||||||
copyIconRefs.value = IconCheck
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.table-box {
|
|
||||||
@apply flex flex-1 items-center truncate px-4 py-1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
<template>
|
|
||||||
<section class="p-4">
|
|
||||||
<h4 class="font-semibold text-secondaryDark">
|
|
||||||
{{ t("settings.short_codes") }}
|
|
||||||
</h4>
|
|
||||||
<div class="my-1 text-secondaryLight">
|
|
||||||
{{ t("settings.short_codes_description") }}
|
|
||||||
</div>
|
|
||||||
<div class="relative overflow-x-auto py-4">
|
|
||||||
<div v-if="loading" class="flex flex-col items-center justify-center">
|
|
||||||
<HoppSmartSpinner class="mb-4" />
|
|
||||||
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
|
||||||
</div>
|
|
||||||
<HoppSmartPlaceholder
|
|
||||||
v-if="!loading && myShortcodes.length === 0"
|
|
||||||
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
|
||||||
:alt="`${t('empty.shortcodes')}`"
|
|
||||||
:text="t('empty.shortcodes')"
|
|
||||||
>
|
|
||||||
</HoppSmartPlaceholder>
|
|
||||||
<div v-else-if="!loading">
|
|
||||||
<div
|
|
||||||
class="hidden w-full rounded-t border-x border-t border-dividerLight bg-primaryLight lg:flex"
|
|
||||||
>
|
|
||||||
<div class="flex w-full overflow-y-scroll">
|
|
||||||
<div class="table-box">
|
|
||||||
{{ t("shortcodes.short_code") }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box">
|
|
||||||
{{ t("shortcodes.method") }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box">
|
|
||||||
{{ t("shortcodes.url") }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box">
|
|
||||||
{{ t("shortcodes.created_on") }}
|
|
||||||
</div>
|
|
||||||
<div class="table-box justify-center">
|
|
||||||
{{ t("shortcodes.actions") }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex max-h-sm w-full flex-col items-center justify-between divide-dividerLight overflow-y-scroll rounded border border-dividerLight lg:divide-y lg:rounded-t-none"
|
|
||||||
>
|
|
||||||
<ProfileShortcode
|
|
||||||
v-for="(shortcode, shortcodeIndex) in myShortcodes"
|
|
||||||
:key="`shortcode-${shortcodeIndex}`"
|
|
||||||
:shortcode="shortcode"
|
|
||||||
@delete-shortcode="deleteShortcode"
|
|
||||||
/>
|
|
||||||
<HoppSmartIntersection
|
|
||||||
v-if="hasMoreShortcodes && myShortcodes.length > 0"
|
|
||||||
@intersecting="loadMoreShortcodes()"
|
|
||||||
>
|
|
||||||
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
|
|
||||||
<HoppSmartSpinner />
|
|
||||||
</div>
|
|
||||||
</HoppSmartIntersection>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!loading && adapterError"
|
|
||||||
class="flex flex-col items-center py-4"
|
|
||||||
>
|
|
||||||
<icon-lucide-help-circle class="svg-icons mb-4" />
|
|
||||||
{{ getErrorMessage(adapterError) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, watchEffect, computed } from "vue"
|
|
||||||
import { pipe } from "fp-ts/function"
|
|
||||||
import * as TE from "fp-ts/TaskEither"
|
|
||||||
import { GQLError } from "~/helpers/backend/GQLClient"
|
|
||||||
import { platform } from "~/platform"
|
|
||||||
|
|
||||||
import { onAuthEvent, onLoggedIn } from "@composables/auth"
|
|
||||||
import { useReadonlyStream } from "@composables/stream"
|
|
||||||
import { useI18n } from "@composables/i18n"
|
|
||||||
import { useToast } from "@composables/toast"
|
|
||||||
import { useColorMode } from "@composables/theming"
|
|
||||||
import { usePageHead } from "@composables/head"
|
|
||||||
|
|
||||||
import ShortcodeListAdapter from "~/helpers/shortcodes/ShortcodeListAdapter"
|
|
||||||
import { deleteShortcode as backendDeleteShortcode } from "~/helpers/backend/mutations/Shortcode"
|
|
||||||
|
|
||||||
const t = useI18n()
|
|
||||||
const toast = useToast()
|
|
||||||
const colorMode = useColorMode()
|
|
||||||
|
|
||||||
usePageHead({
|
|
||||||
title: computed(() => t("navigation.profile")),
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentUser = useReadonlyStream(
|
|
||||||
platform.auth.getCurrentUserStream(),
|
|
||||||
platform.auth.getCurrentUser()
|
|
||||||
)
|
|
||||||
|
|
||||||
const displayName = ref(currentUser.value?.displayName)
|
|
||||||
watchEffect(() => (displayName.value = currentUser.value?.displayName))
|
|
||||||
|
|
||||||
const emailAddress = ref(currentUser.value?.email)
|
|
||||||
watchEffect(() => (emailAddress.value = currentUser.value?.email))
|
|
||||||
|
|
||||||
const adapter = new ShortcodeListAdapter(true)
|
|
||||||
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
|
||||||
const adapterError = useReadonlyStream(adapter.error$, null)
|
|
||||||
const myShortcodes = useReadonlyStream(adapter.shortcodes$, [])
|
|
||||||
const hasMoreShortcodes = useReadonlyStream(adapter.hasMoreShortcodes$, true)
|
|
||||||
|
|
||||||
const loading = computed(
|
|
||||||
() => adapterLoading.value && myShortcodes.value.length === 0
|
|
||||||
)
|
|
||||||
|
|
||||||
onLoggedIn(() => {
|
|
||||||
try {
|
|
||||||
adapter.initialize()
|
|
||||||
} catch (e) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
onAuthEvent((ev) => {
|
|
||||||
if (ev.event === "logout" && adapter.isInitialized()) {
|
|
||||||
adapter.dispose()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteShortcode = (codeID: string) => {
|
|
||||||
pipe(
|
|
||||||
backendDeleteShortcode(codeID),
|
|
||||||
TE.match(
|
|
||||||
(err: GQLError<string>) => {
|
|
||||||
toast.error(`${getErrorMessage(err)}`)
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
toast.success(`${t("shortcodes.deleted")}`)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)()
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadMoreShortcodes = () => {
|
|
||||||
adapter.loadMore()
|
|
||||||
}
|
|
||||||
|
|
||||||
const getErrorMessage = (err: GQLError<string>) => {
|
|
||||||
if (err.type === "network_error") {
|
|
||||||
return t("error.network_error")
|
|
||||||
} else {
|
|
||||||
switch (err.error) {
|
|
||||||
case "shortcode/not_found":
|
|
||||||
return t("shortcodes.not_found")
|
|
||||||
default:
|
|
||||||
return t("error.something_went_wrong")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.table-box {
|
|
||||||
@apply flex flex-1 items-center truncate px-4 py-2;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="myTeams.length"
|
v-else-if="myTeams.length"
|
||||||
class="flex flex-col space-y-2 rounded-lg border border-red-500 bg-error p-4 text-secondaryDark"
|
class="bg-bannerInfo flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||||
>
|
>
|
||||||
<h2 class="font-bold text-red-500">
|
<h2 class="font-bold text-red-500">
|
||||||
{{ t("error.danger_zone") }}
|
{{ t("error.danger_zone") }}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div
|
<div
|
||||||
class="mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 bg-error p-4 text-secondaryDark"
|
class="bg-bannerInfo mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 p-4 text-secondaryDark"
|
||||||
>
|
>
|
||||||
<h2 class="font-bold text-red-500">
|
<h2 class="font-bold text-red-500">
|
||||||
{{ t("error.danger_zone") }}
|
{{ t("error.danger_zone") }}
|
||||||
@@ -173,13 +173,8 @@ const deleteUserAccount = async () => {
|
|||||||
const getErrorMessage = (err: GQLError<string>) => {
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
if (err.type === "network_error") {
|
if (err.type === "network_error") {
|
||||||
return t("error.network_error")
|
return t("error.network_error")
|
||||||
} else {
|
|
||||||
switch (err.error) {
|
|
||||||
case "shortcode/not_found":
|
|
||||||
return t("shortcodes.not_found")
|
|
||||||
default:
|
|
||||||
return t("error.something_went_wrong")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return t("error.something_went_wrong")
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -33,12 +33,12 @@
|
|||||||
theme="popover"
|
theme="popover"
|
||||||
:on-shown="() => tippyActions.focus()"
|
:on-shown="() => tippyActions.focus()"
|
||||||
>
|
>
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
:label="contentType || t('state.none').toLowerCase()"
|
:label="contentType || t('state.none').toLowerCase()"
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div
|
<div
|
||||||
ref="tippyActions"
|
ref="tippyActions"
|
||||||
@@ -279,6 +279,7 @@ defineActionHandler("request.send-cancel", sendMessage)
|
|||||||
:deep(.cm-panels) {
|
:deep(.cm-panels) {
|
||||||
@apply top-upperSecondaryStickyFold #{!important};
|
@apply top-upperSecondaryStickyFold #{!important};
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventFeildShown :deep(.cm-panels),
|
.eventFeildShown :deep(.cm-panels),
|
||||||
.cmResponsePrimaryStickyFold :deep(.cm-panels) {
|
.cmResponsePrimaryStickyFold :deep(.cm-panels) {
|
||||||
@apply top-upperTertiaryStickyFold #{!important};
|
@apply top-upperTertiaryStickyFold #{!important};
|
||||||
|
|||||||
@@ -62,12 +62,12 @@
|
|||||||
{{ t("mqtt.lw_qos") }}
|
{{ t("mqtt.lw_qos") }}
|
||||||
</label>
|
</label>
|
||||||
<tippy interactive trigger="click" theme="popover">
|
<tippy interactive trigger="click" theme="popover">
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary
|
<HoppButtonSecondary
|
||||||
class="ml-2 rounded-none pr-8"
|
class="ml-2 rounded-none pr-8"
|
||||||
:label="`${config.lwQos}`"
|
:label="`${config.lwQos}`"
|
||||||
/>
|
/>
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div class="flex flex-col" role="menu">
|
<div class="flex flex-col" role="menu">
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
autoScrollEnabled ? t('action.turn_off') : t('action.turn_on')
|
autoScrollEnabled ? t('action.turn_off') : t('action.turn_on')
|
||||||
}`"
|
}`"
|
||||||
:icon="IconChevronsDown"
|
:icon="IconChevronsDown"
|
||||||
:class="toggleAutoscrollColor"
|
:color="autoScrollEnabled ? 'green' : 'red'"
|
||||||
@click="toggleAutoscroll()"
|
@click="autoScrollEnabled = !autoScrollEnabled"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, PropType, computed, watch, Ref } from "vue"
|
import { ref, PropType, watch, Ref } from "vue"
|
||||||
import IconTrash from "~icons/lucide/trash"
|
import IconTrash from "~icons/lucide/trash"
|
||||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||||
@@ -123,12 +123,4 @@ watch(
|
|||||||
}, 200),
|
}, 200),
|
||||||
{ flush: "post" }
|
{ flush: "post" }
|
||||||
)
|
)
|
||||||
|
|
||||||
const toggleAutoscroll = () => {
|
|
||||||
autoScrollEnabled.value = !autoScrollEnabled.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleAutoscrollColor = computed(() =>
|
|
||||||
autoScrollEnabled.value ? "text-green-500" : "text-red-500"
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="entry.ts !== undefined"
|
v-if="entry.ts !== undefined"
|
||||||
class="w-34 hidden items-center px-1 sm:inline-flex"
|
class="w-36 hidden items-center px-1 sm:inline-flex"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
@@ -269,12 +269,12 @@ const ast = computed(() =>
|
|||||||
|
|
||||||
const editorText = computed(() => {
|
const editorText = computed(() => {
|
||||||
if (selectedTab.value === "json") return jsonBodyText.value
|
if (selectedTab.value === "json") return jsonBodyText.value
|
||||||
else return logPayload.value
|
return logPayload.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const editorMode = computed(() => {
|
const editorMode = computed(() => {
|
||||||
if (selectedTab.value === "json") return "application/ld+json"
|
if (selectedTab.value === "json") return "application/ld+json"
|
||||||
else return "text/plain"
|
return "text/plain"
|
||||||
})
|
})
|
||||||
|
|
||||||
const { cursor } = useCodemirror(
|
const { cursor } = useCodemirror(
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
{{ t("mqtt.qos") }}
|
{{ t("mqtt.qos") }}
|
||||||
</label>
|
</label>
|
||||||
<tippy interactive trigger="click" theme="popover">
|
<tippy interactive trigger="click" theme="popover">
|
||||||
<span class="select-wrapper">
|
<HoppSmartSelectWrapper>
|
||||||
<HoppButtonSecondary class="pr-8" :label="`${QoS}`" />
|
<HoppButtonSecondary class="pr-8" :label="`${QoS}`" />
|
||||||
</span>
|
</HoppSmartSelectWrapper>
|
||||||
<template #content="{ hide }">
|
<template #content="{ hide }">
|
||||||
<div class="flex flex-col" role="menu">
|
<div class="flex flex-col" role="menu">
|
||||||
<HoppSmartItem
|
<HoppSmartItem
|
||||||
|
|||||||
144
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal file
144
packages/hoppscotch-common/src/components/share/CreateModal.vue
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="selectedWidget"
|
||||||
|
class="border divide-y rounded divide-divider border-divider"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="px-4 py-2">
|
||||||
|
{{ t("shared_requests.creating_widget") }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="px-4 py-2">
|
||||||
|
{{ t("shared_requests.description") }}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col divide-y divide-divider">
|
||||||
|
<div class="flex flex-col p-4 space-y-4">
|
||||||
|
<div
|
||||||
|
v-for="widget in widgets"
|
||||||
|
:key="widget.value"
|
||||||
|
class="flex flex-col p-4 border rounded cursor-pointer border-divider hover:bg-dividerLight"
|
||||||
|
:class="{
|
||||||
|
'!border-accentLight': selectedWidget.value === widget.value,
|
||||||
|
}"
|
||||||
|
@click="selectedWidget = widget"
|
||||||
|
>
|
||||||
|
<span class="mb-1 font-bold text-secondaryDark">
|
||||||
|
{{ widget.label }}
|
||||||
|
</span>
|
||||||
|
<span class="text-tiny">
|
||||||
|
{{ widget.info }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-center justify-center p-4">
|
||||||
|
<span
|
||||||
|
class="flex justify-center flex-1 mb-2 text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ t("shared_requests.preview") }}
|
||||||
|
</span>
|
||||||
|
<div class="w-full">
|
||||||
|
<ShareTemplatesEmbeds
|
||||||
|
v-if="selectedWidget.value === 'embed'"
|
||||||
|
:endpoint="request?.endpoint"
|
||||||
|
:method="request?.method"
|
||||||
|
:model-value="embedOption"
|
||||||
|
/>
|
||||||
|
<ShareTemplatesButton
|
||||||
|
v-else-if="selectedWidget.value === 'button'"
|
||||||
|
img="badge.svg"
|
||||||
|
/>
|
||||||
|
<ShareTemplatesLink v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { PropType, ref } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
request: {
|
||||||
|
type: Object as PropType<HoppRESTRequest | null>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Widget | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedWidget = useVModel(props, "modelValue")
|
||||||
|
|
||||||
|
type WidgetID = "embed" | "button" | "link"
|
||||||
|
|
||||||
|
type Widget = {
|
||||||
|
value: WidgetID
|
||||||
|
label: string
|
||||||
|
info: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgets: Widget[] = [
|
||||||
|
{
|
||||||
|
value: "embed",
|
||||||
|
label: t("shared_requests.embed"),
|
||||||
|
info: t("shared_requests.embed_info"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "button",
|
||||||
|
label: t("shared_requests.button"),
|
||||||
|
info: t("shared_requests.button_info"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "link",
|
||||||
|
label: t("shared_requests.link"),
|
||||||
|
info: t("shared_requests.link_info"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
type Tabs = "parameters" | "body" | "headers" | "authorization"
|
||||||
|
|
||||||
|
type EmbedOption = {
|
||||||
|
selectedTab: Tabs
|
||||||
|
tabs: {
|
||||||
|
value: Tabs
|
||||||
|
label: string
|
||||||
|
enabled: boolean
|
||||||
|
}[]
|
||||||
|
theme: "light" | "dark" | "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
const embedOption = ref<EmbedOption>({
|
||||||
|
selectedTab: "parameters",
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
value: "parameters",
|
||||||
|
label: t("tab.parameters"),
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "body",
|
||||||
|
label: t("tab.body"),
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "headers",
|
||||||
|
label: t("tab.headers"),
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "authorization",
|
||||||
|
label: t("tab.authorization"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: "system",
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,432 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="selectedWidget"
|
||||||
|
class="border divide-y rounded divide-divider border-divider"
|
||||||
|
>
|
||||||
|
<div v-if="loading" class="px-4 py-2">
|
||||||
|
{{ t("shared_requests.creating_widget") }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="px-4 py-2">
|
||||||
|
{{ t("shared_requests.customize") }}
|
||||||
|
</div>
|
||||||
|
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||||
|
<HoppSmartSpinner class="my-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col divide-y divide-divider">
|
||||||
|
<div class="flex flex-col p-2 space-y-2">
|
||||||
|
<HoppSmartRadioGroup
|
||||||
|
v-model="selectedWidget.value"
|
||||||
|
:radios="widgets"
|
||||||
|
class="flex !flex-row"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col divide-y divide-divider">
|
||||||
|
<div class="flex items-center justify-center px-6 py-4">
|
||||||
|
<div v-if="selectedWidget.value === 'embed'" class="w-full">
|
||||||
|
<div class="flex flex-col pb-4">
|
||||||
|
<div
|
||||||
|
v-for="option in embedOptions.tabs"
|
||||||
|
:key="option.value"
|
||||||
|
class="flex justify-between py-2"
|
||||||
|
>
|
||||||
|
<span class="capitalize">
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
<HoppSmartCheckbox
|
||||||
|
:on="option.enabled"
|
||||||
|
@change="removeEmbedOption(option.value)"
|
||||||
|
>
|
||||||
|
</HoppSmartCheckbox>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
{{ t("shared_requests.theme.title") }}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<tippy
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
class="!py-2 !px-0 capitalize"
|
||||||
|
:label="embedOptions.theme"
|
||||||
|
:icon="embedThemeIcon"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('shared_requests.theme.system')"
|
||||||
|
:icon="IconMonitor"
|
||||||
|
:active="embedOptions.theme === 'system'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
embedOptions.theme = 'system'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('shared_requests.theme.light')"
|
||||||
|
:icon="IconSun"
|
||||||
|
:active="embedOptions.theme === 'light'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
embedOptions.theme = 'light'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
:label="t('shared_requests.theme.dark')"
|
||||||
|
:icon="IconMoon"
|
||||||
|
:active="embedOptions.theme === 'dark'"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
embedOptions.theme = 'dark'
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ t("shared_requests.preview") }}
|
||||||
|
</span>
|
||||||
|
<ShareTemplatesEmbeds
|
||||||
|
:endpoint="request?.endpoint"
|
||||||
|
:method="request?.method"
|
||||||
|
:model-value="embedOptions"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('shared_requests.copy_html')"
|
||||||
|
class="underline text-secondaryDark"
|
||||||
|
@click="
|
||||||
|
copyContent({
|
||||||
|
widget: 'embed',
|
||||||
|
type: 'html',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="selectedWidget.value === 'button'"
|
||||||
|
class="flex flex-col space-y-8"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="variant in buttonVariants"
|
||||||
|
:key="variant.id"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ t("shared_requests.preview") }}
|
||||||
|
</span>
|
||||||
|
<ShareTemplatesButton :img="variant.img" />
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('shared_requests.copy_html')"
|
||||||
|
class="underline text-secondaryDark"
|
||||||
|
@click="
|
||||||
|
copyContent({
|
||||||
|
widget: 'button',
|
||||||
|
type: 'html',
|
||||||
|
id: variant.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('shared_requests.copy_markdown')"
|
||||||
|
class="underline text-secondaryDark"
|
||||||
|
@click="
|
||||||
|
copyContent({
|
||||||
|
widget: 'button',
|
||||||
|
type: 'markdown',
|
||||||
|
id: variant.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col space-y-8">
|
||||||
|
<div
|
||||||
|
v-for="variant in linkVariants"
|
||||||
|
:key="variant.type"
|
||||||
|
class="flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center mb-2 text-secondaryLight text-tiny"
|
||||||
|
>
|
||||||
|
{{ t("shared_requests.preview") }}
|
||||||
|
</span>
|
||||||
|
<ShareTemplatesLink :link="variant.link" :label="variant.label" />
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t(`shared_requests.copy_${variant.type}`)"
|
||||||
|
class="underline text-secondaryDark"
|
||||||
|
@click="
|
||||||
|
copyContent({
|
||||||
|
widget: 'link',
|
||||||
|
type: variant.type,
|
||||||
|
id: variant.id,
|
||||||
|
})
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { computed, ref } from "vue"
|
||||||
|
import { PropType } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import IconMonitor from "~icons/lucide/monitor"
|
||||||
|
import IconSun from "~icons/lucide/sun"
|
||||||
|
import IconMoon from "~icons/lucide/moon"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
request: {
|
||||||
|
type: Object as PropType<HoppRESTRequest | null>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Widget | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
embedOptions: {
|
||||||
|
type: Object as PropType<EmbedOption>,
|
||||||
|
default: () => ({
|
||||||
|
selectedTab: "parameters",
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
value: "parameters",
|
||||||
|
label: "shared_requests.parameters",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "body",
|
||||||
|
label: "shared_requests.body",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "headers",
|
||||||
|
label: "shared_requests.headers",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "authorization",
|
||||||
|
label: "shared_requests.authorization",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: "system",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: "copy-shared-request",
|
||||||
|
request: {
|
||||||
|
sharedRequestID: string | undefined
|
||||||
|
content: string | undefined
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
(e: "update:modelValue", value: string): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const selectedWidget = useVModel(props, "modelValue")
|
||||||
|
const embedOptions = useVModel(props, "embedOptions")
|
||||||
|
|
||||||
|
type WidgetID = "embed" | "button" | "link"
|
||||||
|
|
||||||
|
type Widget = {
|
||||||
|
value: WidgetID
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const widgets: Widget[] = [
|
||||||
|
{
|
||||||
|
value: "embed",
|
||||||
|
label: t("shared_requests.embed"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "button",
|
||||||
|
label: t("shared_requests.button"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "link",
|
||||||
|
label: t("shared_requests.link"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||||
|
|
||||||
|
type EmbedOption = {
|
||||||
|
selectedTab: EmbedTabs
|
||||||
|
tabs: {
|
||||||
|
value: EmbedTabs
|
||||||
|
label: string
|
||||||
|
enabled: boolean
|
||||||
|
}[]
|
||||||
|
theme: "light" | "dark" | "system"
|
||||||
|
}
|
||||||
|
const embedThemeIcon = computed(() => {
|
||||||
|
if (embedOptions.value.theme === "system") {
|
||||||
|
return IconMonitor
|
||||||
|
} else if (embedOptions.value.theme === "light") {
|
||||||
|
return IconSun
|
||||||
|
}
|
||||||
|
return IconMoon
|
||||||
|
})
|
||||||
|
|
||||||
|
const removeEmbedOption = (option: EmbedTabs) => {
|
||||||
|
const index = embedOptions.value.tabs.findIndex((tab) => tab.value === option)
|
||||||
|
if (index === -1) return
|
||||||
|
|
||||||
|
//if removed tab is the selected tab, select the next tab with enabled true
|
||||||
|
if (embedOptions.value.selectedTab === option) {
|
||||||
|
const nextTab = embedOptions.value.tabs.find((tab) => tab.enabled)
|
||||||
|
if (nextTab) {
|
||||||
|
embedOptions.value.selectedTab = nextTab.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
embedOptions.value.tabs[index].enabled =
|
||||||
|
!embedOptions.value.tabs[index].enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
type ButtonVariant = {
|
||||||
|
id: string
|
||||||
|
img: string
|
||||||
|
}
|
||||||
|
const buttonVariants: ButtonVariant[] = [
|
||||||
|
{
|
||||||
|
id: "button1",
|
||||||
|
img: "badge.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "button2",
|
||||||
|
img: "badge-light.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "button3",
|
||||||
|
img: "badge-dark.svg",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
type LinkVariant = {
|
||||||
|
id: string
|
||||||
|
link?: string
|
||||||
|
label?: string
|
||||||
|
type: "html" | "markdown" | "link"
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkVariants: LinkVariant[] = [
|
||||||
|
{
|
||||||
|
id: "link1",
|
||||||
|
link: props.request?.id,
|
||||||
|
type: "link",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "link2",
|
||||||
|
label: "shared_requests.run_in_hoppscotch",
|
||||||
|
type: "html",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "link3",
|
||||||
|
label: "shared_requests.run_in_hoppscotch",
|
||||||
|
type: "markdown",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
|
||||||
|
|
||||||
|
const copyEmbed = () => {
|
||||||
|
return `<iframe src="${baseURL}/e/${props.request?.id}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyButton = (
|
||||||
|
variationID: string,
|
||||||
|
type: "html" | "markdown" | "link"
|
||||||
|
) => {
|
||||||
|
let badge = ""
|
||||||
|
if (variationID === "button1") {
|
||||||
|
badge = "badge.svg"
|
||||||
|
} else if (variationID === "button2") {
|
||||||
|
badge = "badge-light.svg"
|
||||||
|
} else {
|
||||||
|
badge = "badge-dark.svg"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "markdown") {
|
||||||
|
return `[](${baseURL}/r/${props.request?.id})`
|
||||||
|
}
|
||||||
|
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyLink = (variationID: string) => {
|
||||||
|
if (variationID === "link1") {
|
||||||
|
return `${baseURL}/r/${props.request?.id}`
|
||||||
|
} else if (variationID === "link2") {
|
||||||
|
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
|
||||||
|
}
|
||||||
|
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const copyContent = ({
|
||||||
|
id,
|
||||||
|
widget,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
id?: string | undefined
|
||||||
|
widget: WidgetID
|
||||||
|
type: "html" | "markdown" | "link"
|
||||||
|
}) => {
|
||||||
|
let content = ""
|
||||||
|
if (widget === "button") {
|
||||||
|
content = copyButton(id!, type)
|
||||||
|
} else if (widget === "link") {
|
||||||
|
content = copyLink(id!)
|
||||||
|
} else {
|
||||||
|
content = copyEmbed()
|
||||||
|
}
|
||||||
|
const copyContent = {
|
||||||
|
sharedRequestID: props.request?.id,
|
||||||
|
content,
|
||||||
|
}
|
||||||
|
emit("copy-shared-request", copyContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
</script>
|
||||||
168
packages/hoppscotch-common/src/components/share/Modal.vue
Normal file
168
packages/hoppscotch-common/src/components/share/Modal.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<HoppSmartModal
|
||||||
|
v-if="show"
|
||||||
|
dialog
|
||||||
|
:title="
|
||||||
|
step === 1 ? t('modal.share_request') : t('modal.customize_request')
|
||||||
|
"
|
||||||
|
styles="sm:max-w-md"
|
||||||
|
@close="hideModal"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div v-if="loading" class="flex flex-col items-center justify-center p-4">
|
||||||
|
<HoppSmartSpinner class="my-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
<ShareCreateModal
|
||||||
|
v-else-if="step === 1"
|
||||||
|
v-model="selectedWidget"
|
||||||
|
:request="request"
|
||||||
|
:loading="loading"
|
||||||
|
@create-shared-request="createSharedRequest"
|
||||||
|
/>
|
||||||
|
<ShareCustomizeModal
|
||||||
|
v-else-if="step === 2"
|
||||||
|
v-model="selectedWidget"
|
||||||
|
v-model:embed-options="embedOptions"
|
||||||
|
:request="request"
|
||||||
|
:loading="loading"
|
||||||
|
@copy-shared-request="copySharedRequest"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="step === 1" #footer>
|
||||||
|
<div class="flex justify-start flex-1">
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="t('action.create')"
|
||||||
|
:loading="loading"
|
||||||
|
@click="createSharedRequest"
|
||||||
|
/>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
:label="t('action.cancel')"
|
||||||
|
class="ml-2"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="hideModal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</HoppSmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { PropType } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||||
|
|
||||||
|
type EmbedOption = {
|
||||||
|
selectedTab: EmbedTabs
|
||||||
|
tabs: {
|
||||||
|
value: EmbedTabs
|
||||||
|
label: string
|
||||||
|
enabled: boolean
|
||||||
|
}[]
|
||||||
|
theme: "light" | "dark" | "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
request: {
|
||||||
|
type: Object as PropType<HoppRESTRequest | null>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object as PropType<Widget | null>,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
embedOptions: {
|
||||||
|
type: Object as PropType<EmbedOption>,
|
||||||
|
default: () => ({
|
||||||
|
selectedTab: "parameters",
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
value: "parameters",
|
||||||
|
label: "shared_requests.parameters",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "body",
|
||||||
|
label: "shared_requests.body",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "headers",
|
||||||
|
label: "shared_requests.headers",
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "authorization",
|
||||||
|
label: "shared_requests.authorization",
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: "system",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
type WidgetID = "embed" | "button" | "link"
|
||||||
|
|
||||||
|
type Widget = {
|
||||||
|
value: WidgetID
|
||||||
|
label: string
|
||||||
|
info: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedWidget = useVModel(props, "modelValue")
|
||||||
|
const embedOptions = useVModel(props, "embedOptions")
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "create-shared-request", request: HoppRESTRequest | null): void
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
(e: "update:modelValue", value: string): void
|
||||||
|
(e: "update:step", value: number): void
|
||||||
|
(
|
||||||
|
e: "copy-shared-request",
|
||||||
|
payload: {
|
||||||
|
sharedRequestID: string | undefined
|
||||||
|
content: string | undefined
|
||||||
|
}
|
||||||
|
): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const createSharedRequest = () => {
|
||||||
|
emit("create-shared-request", props.request as HoppRESTRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copySharedRequest = (payload: {
|
||||||
|
sharedRequestID: string | undefined
|
||||||
|
content: string | undefined
|
||||||
|
}) => {
|
||||||
|
emit("copy-shared-request", payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideModal = () => {
|
||||||
|
emit("hide-modal")
|
||||||
|
selectedWidget.value = {
|
||||||
|
value: "embed",
|
||||||
|
label: t("shared_requests.embed"),
|
||||||
|
info: t("shared_requests.embed_info"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
167
packages/hoppscotch-common/src/components/share/Request.vue
Normal file
167
packages/hoppscotch-common/src/components/share/Request.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex items-stretch group"
|
||||||
|
@contextmenu.prevent="options!.tippy.show()"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||||
|
class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto"
|
||||||
|
:title="`${timeStamp}`"
|
||||||
|
@click="openInNewTab"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
|
||||||
|
:style="{ color: requestLabelColor }"
|
||||||
|
>
|
||||||
|
<span class="font-semibold truncate text-tiny">
|
||||||
|
{{ parseRequest.method }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex items-center flex-1 min-w-0 pr-2 transition pointer-events-none group-hover:text-secondaryDark"
|
||||||
|
>
|
||||||
|
<span class="flex-1 truncate">
|
||||||
|
{{ parseRequest.endpoint }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex px-2 truncate text-secondaryLight group-hover:text-secondaryDark"
|
||||||
|
>
|
||||||
|
{{ parseRequest.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<tippy
|
||||||
|
ref="options"
|
||||||
|
interactive
|
||||||
|
trigger="click"
|
||||||
|
theme="popover"
|
||||||
|
:on-shown="() => tippyActions!.focus()"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.more')"
|
||||||
|
:icon="IconMoreVertical"
|
||||||
|
/>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div
|
||||||
|
ref="tippyActions"
|
||||||
|
class="flex flex-col focus:outline-none"
|
||||||
|
tabindex="0"
|
||||||
|
role="menu"
|
||||||
|
@keyup.t="openInNewTabAction?.$el.click()"
|
||||||
|
@keyup.e="customizeAction?.$el.click()"
|
||||||
|
@keyup.delete="deleteAction?.$el.click()"
|
||||||
|
@keyup.escape="hide()"
|
||||||
|
>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="openInNewTabAction"
|
||||||
|
:icon="IconArrowUpRight"
|
||||||
|
:label="`${t('shared_requests.open_new_tab')}`"
|
||||||
|
:shortcut="['T']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
openInNewTab()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="customizeAction"
|
||||||
|
:icon="IconCustomize"
|
||||||
|
:label="`${t('shared_requests.customize')}`"
|
||||||
|
:shortcut="['E']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
customizeSharedRequest()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<HoppSmartItem
|
||||||
|
ref="deleteAction"
|
||||||
|
:icon="IconTrash2"
|
||||||
|
:label="`${t('action.delete')}`"
|
||||||
|
:shortcut="['⌫']"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
deleteSharedRequest()
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||||
|
import { pipe } from "fp-ts/lib/function"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { TippyComponent } from "vue-tippy"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import { getMethodLabelColorClassOf } from "~/helpers/rest/labelColoring"
|
||||||
|
import { Shortcode } from "~/helpers/shortcode/Shortcode"
|
||||||
|
import IconArrowUpRight from "~icons/lucide/arrow-up-right-square"
|
||||||
|
import IconMoreVertical from "~icons/lucide/more-vertical"
|
||||||
|
import IconCustomize from "~icons/lucide/settings-2"
|
||||||
|
import IconTrash2 from "~icons/lucide/trash-2"
|
||||||
|
import { shortDateTime } from "~/helpers/utils/date"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
request: Shortcode
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(
|
||||||
|
e: "customize-shared-request",
|
||||||
|
request: HoppRESTRequest,
|
||||||
|
id: string,
|
||||||
|
embedProperties?: string | null
|
||||||
|
): void
|
||||||
|
(e: "delete-shared-request", codeID: string): void
|
||||||
|
(e: "open-new-tab", request: HoppRESTRequest): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const tippyActions = ref<TippyComponent | null>(null)
|
||||||
|
const openInNewTabAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const customizeAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const deleteAction = ref<HTMLButtonElement | null>(null)
|
||||||
|
const options = ref<any | null>(null)
|
||||||
|
|
||||||
|
const parseRequest = computed(() =>
|
||||||
|
pipe(props.request.request, JSON.parse, translateToNewRequest)
|
||||||
|
)
|
||||||
|
|
||||||
|
const requestLabelColor = computed(() =>
|
||||||
|
getMethodLabelColorClassOf(parseRequest.value)
|
||||||
|
)
|
||||||
|
|
||||||
|
const openInNewTab = () => {
|
||||||
|
emit("open-new-tab", parseRequest.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const customizeSharedRequest = () => {
|
||||||
|
const embedProperties = props.request.properties
|
||||||
|
emit(
|
||||||
|
"customize-shared-request",
|
||||||
|
parseRequest.value,
|
||||||
|
props.request.id,
|
||||||
|
embedProperties
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteSharedRequest = () => {
|
||||||
|
emit("delete-shared-request", props.request.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeStamp = computed(() => shortDateTime(props.request.createdOn))
|
||||||
|
</script>
|
||||||
464
packages/hoppscotch-common/src/components/share/index.vue
Normal file
464
packages/hoppscotch-common/src/components/share/index.vue
Normal file
@@ -0,0 +1,464 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto bg-primary"
|
||||||
|
>
|
||||||
|
<WorkspaceCurrent
|
||||||
|
:section="t('tab.shared_requests')"
|
||||||
|
:is-only-personal="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="sticky top-sidebarPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-end overflow-x-auto border-b border-dividerLight bg-primary"
|
||||||
|
>
|
||||||
|
<HoppButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
to="https://docs.hoppscotch.io/documentation/features/shared-request"
|
||||||
|
blank
|
||||||
|
:title="t('app.wiki')"
|
||||||
|
:icon="IconHelpCircle"
|
||||||
|
class="py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div v-if="loading" class="flex flex-col items-center justify-center">
|
||||||
|
<HoppSmartSpinner class="mb-4" />
|
||||||
|
<span class="text-secondaryLight">{{ t("state.loading") }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-else-if="!currentUser"
|
||||||
|
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||||
|
:alt="`${t('empty.shared_requests_logout')}`"
|
||||||
|
:text="`${t('empty.shared_requests_logout')}`"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<HoppButtonPrimary
|
||||||
|
:label="t('auth.login')"
|
||||||
|
@click="invokeAction('modals.login.toggle')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</HoppSmartPlaceholder>
|
||||||
|
|
||||||
|
<template v-else-if="sharedRequests.length">
|
||||||
|
<ShareRequest
|
||||||
|
v-for="request in sharedRequests"
|
||||||
|
:key="request.id"
|
||||||
|
:request="request"
|
||||||
|
@customize-shared-request="customizeSharedRequest"
|
||||||
|
@delete-shared-request="deleteSharedRequest"
|
||||||
|
@open-new-tab="openInNewTab"
|
||||||
|
/>
|
||||||
|
<HoppSmartIntersection
|
||||||
|
v-if="hasMoreSharedRequests"
|
||||||
|
@intersecting="loadMoreSharedRequests"
|
||||||
|
>
|
||||||
|
<div v-if="adapterLoading" class="flex flex-col items-center py-3">
|
||||||
|
<HoppSmartSpinner />
|
||||||
|
</div>
|
||||||
|
</HoppSmartIntersection>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-else-if="adapterError" class="flex flex-col items-center py-4">
|
||||||
|
<icon-lucide-help-circle class="svg-icons mb-4" />
|
||||||
|
{{ getErrorMessage(adapterError) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HoppSmartPlaceholder
|
||||||
|
v-else
|
||||||
|
:src="`/images/states/${colorMode.value}/add_files.svg`"
|
||||||
|
:alt="`${t('empty.shared_requests')}`"
|
||||||
|
:text="t('empty.shared_requests')"
|
||||||
|
@drop.stop
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<HoppSmartConfirmModal
|
||||||
|
:show="showConfirmModal"
|
||||||
|
:title="confirmModalTitle"
|
||||||
|
:loading-state="modalLoadingState"
|
||||||
|
@hide-modal="showConfirmModal = false"
|
||||||
|
@resolve="resolveConfirmModal"
|
||||||
|
/>
|
||||||
|
<ShareModal
|
||||||
|
v-model="selectedWidget"
|
||||||
|
v-model:embed-options="embedOptions"
|
||||||
|
:step="step"
|
||||||
|
:request="requestToShare"
|
||||||
|
:show="showShareRequestModal"
|
||||||
|
:loading="shareRequestCreatingLoading"
|
||||||
|
@hide-modal="displayCustomizeRequestModal(false, null)"
|
||||||
|
@copy-shared-request="copySharedRequest"
|
||||||
|
@create-shared-request="createSharedRequest"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
import ShortcodeListAdapter from "~/helpers/shortcode/ShortcodeListAdapter"
|
||||||
|
import { useReadonlyStream } from "~/composables/stream"
|
||||||
|
import { onAuthEvent, onLoggedIn } from "~/composables/auth"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useColorMode } from "~/composables/theming"
|
||||||
|
import { defineActionHandler, invokeAction } from "~/helpers/actions"
|
||||||
|
import { platform } from "~/platform"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import * as TE from "fp-ts/TaskEither"
|
||||||
|
import {
|
||||||
|
deleteShortcode as backendDeleteShortcode,
|
||||||
|
createShortcode,
|
||||||
|
updateEmbedProperties,
|
||||||
|
} from "~/helpers/backend/mutations/Shortcode"
|
||||||
|
import { GQLError } from "~/helpers/backend/GQLClient"
|
||||||
|
import { useToast } from "~/composables/toast"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { HoppRESTRequest } from "@hoppscotch/data"
|
||||||
|
import { copyToClipboard } from "~/helpers/utils/clipboard"
|
||||||
|
import * as E from "fp-ts/Either"
|
||||||
|
import { RESTTabService } from "~/services/tab/rest"
|
||||||
|
import { useService } from "dioc/vue"
|
||||||
|
import { watch } from "vue"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
const colorMode = useColorMode()
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const showConfirmModal = ref(false)
|
||||||
|
const confirmModalTitle = ref("")
|
||||||
|
const modalLoadingState = ref(false)
|
||||||
|
|
||||||
|
const showShareRequestModal = ref(false)
|
||||||
|
|
||||||
|
const sharedRequestID = ref("")
|
||||||
|
const shareRequestCreatingLoading = ref(false)
|
||||||
|
|
||||||
|
const requestToShare = ref<HoppRESTRequest | null>(null)
|
||||||
|
|
||||||
|
const embedOptions = ref<EmbedOption>({
|
||||||
|
selectedTab: "parameters",
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
value: "parameters",
|
||||||
|
label: t("tab.parameters"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "body",
|
||||||
|
label: t("tab.body"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "headers",
|
||||||
|
label: t("tab.headers"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "authorization",
|
||||||
|
label: t("tab.authorization"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: "system",
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateEmbedProperty = async (
|
||||||
|
shareRequestID: string,
|
||||||
|
properties: string
|
||||||
|
) => {
|
||||||
|
const customizeEmbedResult = await updateEmbedProperties(
|
||||||
|
shareRequestID,
|
||||||
|
properties
|
||||||
|
)()
|
||||||
|
|
||||||
|
if (E.isLeft(customizeEmbedResult)) {
|
||||||
|
toast.error(`${customizeEmbedResult.left.error}`)
|
||||||
|
toast.error(t("error.something_went_wrong"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => embedOptions.value,
|
||||||
|
() => {
|
||||||
|
if (
|
||||||
|
requestToShare.value &&
|
||||||
|
requestToShare.value.id &&
|
||||||
|
showShareRequestModal.value
|
||||||
|
) {
|
||||||
|
if (selectedWidget.value.value === "embed") {
|
||||||
|
const properties = {
|
||||||
|
options: embedOptions.value.tabs
|
||||||
|
.filter((tab) => tab.enabled)
|
||||||
|
.map((tab) => tab.value),
|
||||||
|
theme: embedOptions.value.theme,
|
||||||
|
}
|
||||||
|
updateEmbedProperty(requestToShare.value.id, JSON.stringify(properties))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
const restTab = useService(RESTTabService)
|
||||||
|
|
||||||
|
const currentUser = useReadonlyStream(
|
||||||
|
platform.auth.getCurrentUserStream(),
|
||||||
|
platform.auth.getCurrentUser()
|
||||||
|
)
|
||||||
|
|
||||||
|
const step = ref(1)
|
||||||
|
|
||||||
|
type EmbedTabs = "parameters" | "body" | "headers" | "authorization"
|
||||||
|
|
||||||
|
type EmbedOption = {
|
||||||
|
selectedTab: EmbedTabs
|
||||||
|
tabs: {
|
||||||
|
value: EmbedTabs
|
||||||
|
label: string
|
||||||
|
enabled: boolean
|
||||||
|
}[]
|
||||||
|
theme: "light" | "dark" | "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
type WidgetID = "embed" | "button" | "link"
|
||||||
|
|
||||||
|
type Widget = {
|
||||||
|
value: WidgetID
|
||||||
|
label: string
|
||||||
|
info: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedWidget = ref<Widget>({
|
||||||
|
value: "embed",
|
||||||
|
label: t("shared_requests.embed"),
|
||||||
|
info: t("shared_requests.embed_info"),
|
||||||
|
})
|
||||||
|
|
||||||
|
const adapter = new ShortcodeListAdapter(true)
|
||||||
|
const adapterLoading = useReadonlyStream(adapter.loading$, false)
|
||||||
|
const adapterError = useReadonlyStream(adapter.error$, null)
|
||||||
|
const sharedRequests = useReadonlyStream(adapter.shortcodes$, [])
|
||||||
|
const hasMoreSharedRequests = useReadonlyStream(
|
||||||
|
adapter.hasMoreShortcodes$,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
const loading = computed(
|
||||||
|
() => adapterLoading.value && sharedRequests.value.length === 0
|
||||||
|
)
|
||||||
|
|
||||||
|
onLoggedIn(() => {
|
||||||
|
try {
|
||||||
|
adapter.initialize()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onAuthEvent((ev) => {
|
||||||
|
if (ev.event === "logout" && adapter.isInitialized()) {
|
||||||
|
adapter.dispose()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const deleteSharedRequest = (codeID: string) => {
|
||||||
|
if (currentUser.value) {
|
||||||
|
sharedRequestID.value = codeID
|
||||||
|
confirmModalTitle.value = `${t("confirm.remove_shared_request")}`
|
||||||
|
showConfirmModal.value = true
|
||||||
|
} else {
|
||||||
|
invokeAction("modals.login.toggle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDeleteSharedRequest = () => {
|
||||||
|
modalLoadingState.value = true
|
||||||
|
pipe(
|
||||||
|
backendDeleteShortcode(sharedRequestID.value),
|
||||||
|
TE.match(
|
||||||
|
(err: GQLError<string>) => {
|
||||||
|
toast.error(getErrorMessage(err))
|
||||||
|
showConfirmModal.value = false
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
toast.success(t("shared_requests.deleted"))
|
||||||
|
sharedRequestID.value = ""
|
||||||
|
modalLoadingState.value = false
|
||||||
|
showConfirmModal.value = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMoreSharedRequests = () => {
|
||||||
|
adapter.loadMore()
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayShareRequestModal = (show: boolean) => {
|
||||||
|
showShareRequestModal.value = show
|
||||||
|
step.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayCustomizeRequestModal = (
|
||||||
|
show: boolean,
|
||||||
|
embedProperties?: string | null
|
||||||
|
) => {
|
||||||
|
showShareRequestModal.value = show
|
||||||
|
step.value = 2
|
||||||
|
if (!embedProperties) {
|
||||||
|
selectedWidget.value = {
|
||||||
|
value: "button",
|
||||||
|
label: t("shared_requests.button"),
|
||||||
|
info: t("shared_requests.button_info"),
|
||||||
|
}
|
||||||
|
embedOptions.value = {
|
||||||
|
selectedTab: "parameters",
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
value: "parameters",
|
||||||
|
label: t("tab.parameters"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "body",
|
||||||
|
label: t("tab.body"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "headers",
|
||||||
|
label: t("tab.headers"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "authorization",
|
||||||
|
label: t("tab.authorization"),
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
theme: "system",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const parsedEmbedProperties = JSON.parse(embedProperties)
|
||||||
|
embedOptions.value = {
|
||||||
|
selectedTab: parsedEmbedProperties.options[0],
|
||||||
|
tabs: embedOptions.value.tabs.map((tab) => {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
enabled: parsedEmbedProperties.options.includes(tab.value),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
theme: parsedEmbedProperties.theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createSharedRequest = async (request: HoppRESTRequest | null) => {
|
||||||
|
if (request && selectedWidget.value) {
|
||||||
|
const properties = {
|
||||||
|
options: ["parameters", "body", "headers"],
|
||||||
|
theme: "system",
|
||||||
|
}
|
||||||
|
shareRequestCreatingLoading.value = true
|
||||||
|
const sharedRequestResult = await createShortcode(
|
||||||
|
request,
|
||||||
|
selectedWidget.value.value === "embed"
|
||||||
|
? JSON.stringify(properties)
|
||||||
|
: undefined
|
||||||
|
)()
|
||||||
|
|
||||||
|
platform.analytics?.logEvent({
|
||||||
|
type: "HOPP_SHORTCODE_CREATED",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (E.isLeft(sharedRequestResult)) {
|
||||||
|
toast.error(`${sharedRequestResult.left.error}`)
|
||||||
|
toast.error(t("error.something_went_wrong"))
|
||||||
|
} else if (E.isRight(sharedRequestResult)) {
|
||||||
|
if (sharedRequestResult.right.createShortcode) {
|
||||||
|
shareRequestCreatingLoading.value = false
|
||||||
|
requestToShare.value = {
|
||||||
|
...JSON.parse(sharedRequestResult.right.createShortcode.request),
|
||||||
|
id: sharedRequestResult.right.createShortcode.id,
|
||||||
|
}
|
||||||
|
step.value = 2
|
||||||
|
|
||||||
|
if (sharedRequestResult.right.createShortcode.properties) {
|
||||||
|
const parsedEmbedProperties = JSON.parse(
|
||||||
|
sharedRequestResult.right.createShortcode.properties
|
||||||
|
)
|
||||||
|
|
||||||
|
embedOptions.value = {
|
||||||
|
selectedTab: parsedEmbedProperties.options[0],
|
||||||
|
tabs: embedOptions.value.tabs.map((tab) => {
|
||||||
|
return {
|
||||||
|
...tab,
|
||||||
|
enabled: parsedEmbedProperties.options.includes(tab.value),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
theme: parsedEmbedProperties.theme,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customizeSharedRequest = (
|
||||||
|
request: HoppRESTRequest,
|
||||||
|
shredRequestID: string,
|
||||||
|
embedProperties?: string | null
|
||||||
|
) => {
|
||||||
|
requestToShare.value = {
|
||||||
|
...request,
|
||||||
|
id: shredRequestID,
|
||||||
|
}
|
||||||
|
displayCustomizeRequestModal(true, embedProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
const copySharedRequest = (payload: {
|
||||||
|
sharedRequestID: string | undefined
|
||||||
|
content: string | undefined
|
||||||
|
}) => {
|
||||||
|
if (payload.content) {
|
||||||
|
copyToClipboard(payload.content)
|
||||||
|
toast.success(t("state.copied_to_clipboard"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openInNewTab = (request: HoppRESTRequest) => {
|
||||||
|
restTab.createNewTab({
|
||||||
|
isDirty: false,
|
||||||
|
request,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveConfirmModal = (title: string | null) => {
|
||||||
|
if (title === `${t("confirm.remove_shared_request")}`) onDeleteSharedRequest()
|
||||||
|
else {
|
||||||
|
console.error(
|
||||||
|
`Confirm modal title ${title} is not handled by the component`
|
||||||
|
)
|
||||||
|
toast.error(t("error.something_went_wrong"))
|
||||||
|
showConfirmModal.value = false
|
||||||
|
sharedRequestID.value = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getErrorMessage = (err: GQLError<string>) => {
|
||||||
|
if (err.type === "network_error") {
|
||||||
|
return t("error.network_error")
|
||||||
|
}
|
||||||
|
switch (err.error) {
|
||||||
|
case "shortcode/not_found":
|
||||||
|
return t("shared_request.not_found")
|
||||||
|
default:
|
||||||
|
return t("error.something_went_wrong")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineActionHandler("share.request", ({ request }) => {
|
||||||
|
requestToShare.value = request
|
||||||
|
displayShareRequestModal(true)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center p-4 border rounded border-dividerDark">
|
||||||
|
<img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
img: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="flex flex-col p-4 border rounded border-dividerDark"
|
||||||
|
:class="{
|
||||||
|
'bg-accentContrast': isEmbedThemeLight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-stretch space-x-2"
|
||||||
|
:class="{
|
||||||
|
'bg-accentContrast': isEmbedThemeLight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex items-center flex-1 min-w-0 border rounded border-divider"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex max-w-[4rem] rounded-l h-full items-center justify-center border-r border-divider text-tiny"
|
||||||
|
:class="{
|
||||||
|
'!border-dividerLight bg-accentContrast text-primary':
|
||||||
|
isEmbedThemeLight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="px-3 truncate">
|
||||||
|
{{ method }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="px-3 truncate"
|
||||||
|
:class="{
|
||||||
|
'text-primary': isEmbedThemeLight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ endpoint }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
class="flex items-center justify-center flex-shrink-0 px-3 py-2 font-semibold border rounded border-dividerDark bg-primaryDark text-secondary"
|
||||||
|
:class="{
|
||||||
|
'!bg-accentContrast text-primaryLight': isEmbedThemeLight,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ t("action.send") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex"
|
||||||
|
:class="{
|
||||||
|
'bg-accentContrast text-primary': isEmbedThemeLight,
|
||||||
|
'border-b border-divider pt-2': !noActiveTab,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="option in embedOptions.tabs"
|
||||||
|
v-show="option.enabled"
|
||||||
|
:key="option.value"
|
||||||
|
class="px-2 py-2"
|
||||||
|
:class="{
|
||||||
|
'border-b border-dividerDark':
|
||||||
|
embedOptions.tabs.filter((tab) => tab.enabled)[0]?.value ===
|
||||||
|
option.value,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { useVModel } from "@vueuse/core"
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
type Tabs = "parameters" | "body" | "headers" | "authorization"
|
||||||
|
|
||||||
|
type EmbedOption = {
|
||||||
|
selectedTab: Tabs
|
||||||
|
tabs: {
|
||||||
|
value: Tabs
|
||||||
|
label: string
|
||||||
|
enabled: boolean
|
||||||
|
}[]
|
||||||
|
theme: "light" | "dark" | "system"
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
method: string | undefined
|
||||||
|
endpoint: string | undefined
|
||||||
|
modelValue: EmbedOption
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const embedOptions = useVModel(props, "modelValue")
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const noActiveTab = computed(() => {
|
||||||
|
return embedOptions.value.tabs.every((tab) => !tab.enabled)
|
||||||
|
})
|
||||||
|
|
||||||
|
const isEmbedThemeLight = computed(() => embedOptions.value.theme === "light")
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center p-4 border rounded border-dividerDark">
|
||||||
|
<span
|
||||||
|
:class="{
|
||||||
|
'border-b border-secondary': label,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
link?: string | undefined
|
||||||
|
label?: string | undefined
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
return props.label ? t(props.label) : `hopp.sh/r/${props.link ?? "xxxx"}`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user