Compare commits

...

12 Commits

Author SHA1 Message Date
Liyas Thomas
a267e9c411 chore: minor ui improvements 2023-12-12 15:03:04 +05:30
Liyas Thomas
b53ae0cefe fix: use base url instead of hardcoded url 2023-12-12 15:03:04 +05:30
Liyas Thomas
ebf90207e5 chore: improve placeholder component styles (#3638)
Co-authored-by: jamesgeorge007 <jamesgeorge998001@gmail.com>
2023-12-12 15:02:42 +05:30
Liyas Thomas
4ac8a117ef chore: add protocol logos to realtime page (#3637) 2023-12-12 14:25:56 +05:30
Muhammed Ajmal M
c1bc430ee6 fix: overflowing modal fix on small screens (#3643)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-12 14:21:19 +05:30
James George
9201aa7d7d fix(common): ensure banner colors are displayed correctly (#3630) 2023-12-12 13:55:18 +05:30
Liyas Thomas
87395a4553 fix: minor ui issues 2023-12-07 17:12:33 +05:30
Andrew Bastin
6063c633ee chore: pin vue workspace-wide to 3.3.9 2023-12-07 17:04:17 +05:30
Akash K
7481feb366 feat: introduce platform defs for adding additional spotlight searchers (#3631) 2023-12-07 16:28:02 +05:30
James George
bdfa14fa54 refactor(scripting-revamp): migrate js-sandbox to web worker/Node vm based implementation (#3619) 2023-12-07 16:10:42 +05:30
Andrew Bastin
0a61ec2bfe fix: useI18n type breaking 2023-12-07 15:17:41 +05:30
Nivedin
2bf0106aa2 feat: embeds (#3627)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
2023-12-07 15:03:49 +05:30
123 changed files with 4285 additions and 4088 deletions

View File

@@ -32,6 +32,9 @@
"lint-staged": "12.4.0"
},
"pnpm": {
"overrides": {
"vue": "3.3.9"
},
"packageExtensions": {
"httpsnippet@^3.0.1": {
"peerDependencies": {

View File

@@ -147,7 +147,7 @@ module.exports = {
// The glob patterns Jest uses to detect test files
testMatch: [
// "**/__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

View File

@@ -19,8 +19,9 @@
"debugger": "node debugger.js 9999",
"prepublish": "pnpm exec tsup",
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
"test": "pnpm run build && jest && rm -rf dist",
"do-typecheck": "pnpm exec tsc --noEmit",
"test": "pnpm run build && jest && rm -rf dist"
"do-test": "pnpm test"
},
"keywords": [
"cli",

View File

@@ -28,7 +28,7 @@ describe("Test 'hopp test <file>' command:", () => {
expect(out).toBe<HoppErrorCode>("UNKNOWN_ERROR");
});
test("Malformed collection file.", async () => {
const cmd = `node ./bin/hopp test ${getTestJsonFilePath(
"malformed-collection2.json"
@@ -106,7 +106,7 @@ describe("Test 'hopp test <file> --env <file>' command:", () => {
const TESTS_PATH = getTestJsonFilePath("env-flag-tests.json");
const ENV_PATH = getTestJsonFilePath("env-flag-envs.json");
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();
});
@@ -129,7 +129,6 @@ describe("Test 'hopp test <file> --delay <delay_in_ms>' command:", () => {
const cmd = `${VALID_TEST_CMD} --delay 'NaN'`;
const { stderr } = await execAsync(cmd);
const out = getErrorCode(stderr);
console.log("invalid value thing", out)
expect(out).toBe<HoppErrorCode>("INVALID_ARGUMENT");
});

View File

@@ -1,7 +1,6 @@
{
"URL": "https://echo.hoppscotch.io",
"HOST": "echo.hoppscotch.io",
"X-COUNTRY": "IN",
"BODY_VALUE": "body_value",
"BODY_KEY": "body_key"
}

View File

@@ -12,7 +12,7 @@
"method": "POST",
"auth": { "authType": "none", "authActive": true },
"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": {
"contentType": "application/json",
"body": "{\n \"<<BODY_KEY>>\":\"<<BODY_VALUE>>\"\n}"

View File

@@ -6,23 +6,24 @@ import {
parseTemplateString,
parseTemplateStringE,
} from "@hoppscotch/data";
import { runPreRequestScript } from "@hoppscotch/js-sandbox";
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 { runPreRequestScript } from "@hoppscotch/js-sandbox/node";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as O from "fp-ts/Option";
import * as RA from "fp-ts/ReadonlyArray";
import * as TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import * as S from "fp-ts/string";
import qs from "qs";
import { EffectiveHoppRESTRequest } from "../interfaces/request";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
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 { 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

View File

@@ -1,17 +1,19 @@
import { HoppRESTRequest } from "@hoppscotch/data";
import { execTestScript, TestDescriptor } from "@hoppscotch/js-sandbox";
import { hrtime } from "process";
import { flow, pipe } from "fp-ts/function";
import * as RA from "fp-ts/ReadonlyArray";
import { TestDescriptor } from "@hoppscotch/js-sandbox";
import { runTestScript } from "@hoppscotch/js-sandbox/node";
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 TE from "fp-ts/TaskEither";
import { flow, pipe } from "fp-ts/function";
import { hrtime } from "process";
import {
RequestRunnerResponse,
TestReport,
TestScriptParams,
} from "../interfaces/response";
import { error, HoppCLIError } from "../types/errors";
import { HoppCLIError, error } from "../types/errors";
import { HoppEnvs } from "../types/request";
import { ExpectResult, TestMetrics, TestRunnerRes } from "../types/response";
import { getDurationInSeconds } from "./getters";
@@ -36,7 +38,7 @@ export const testRunner = (
pipe(
TE.of(testScriptData),
TE.chain(({ testScript, response, envs }) =>
execTestScript(testScript, envs, response)
runTestScript(testScript, envs, response)
)
)
),

View File

@@ -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

View 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

View 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

View 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

View File

@@ -96,6 +96,7 @@
"keyboard_shortcuts": "Keyboard shortcuts",
"name": "Hoppscotch",
"new_version_found": "New version found. Refresh to update.",
"open_in_hoppscotch": "Open in Hoppscotch",
"options": "Options",
"proxy_privacy_policy": "Proxy privacy policy",
"reload": "Reload",
@@ -431,8 +432,9 @@
"close_unsaved_tab": "You have unsaved changes",
"collections": "Collections",
"confirm": "Confirm",
"customize_request": "Customize Request",
"edit_request": "Edit Request",
"share_request":"Share Request",
"share_request": "Share Request",
"import_export": "Import / Export"
},
"mqtt": {
@@ -621,29 +623,30 @@
"additional": "Additional Settings",
"verify_email": "Verify email"
},
"shared_requests":{
"button":"Button",
"shared_requests": {
"button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"customize": "Customize",
"creating_widget": "Creating widget",
"copy_html": "Copy HTML",
"copy_link": "Copy Link",
"copy_markdown": "Copy Markdown",
"deleted":"Shared request deleted",
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed":"Embed",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link":"Link",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"not_found":"Shared request not found",
"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"
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
},
"shortcut": {
@@ -689,7 +692,7 @@
"save_to_collections": "Save to Collections",
"send_request": "Send Request",
"show_code": "Generate code snippet",
"share_request":"Share Request",
"share_request": "Share Request",
"title": "Request"
},
"response": {

View File

@@ -61,6 +61,7 @@ declare module 'vue' {
CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default']
CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default']
CookiesEditCookie: typeof import('./components/cookies/EditCookie.vue')['default']
Embeds: typeof import('./components/embeds/index.vue')['default']
Environments: typeof import('./components/environments/index.vue')['default']
EnvironmentsAdd: typeof import('./components/environments/Add.vue')['default']
EnvironmentsImportExport: typeof import('./components/environments/ImportExport.vue')['default']
@@ -163,6 +164,12 @@ declare module 'vue' {
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']
InterceptorsExtensionSubtitle: typeof import('./components/interceptors/ExtensionSubtitle.vue')['default']
LensesHeadersRenderer: typeof import('./components/lenses/HeadersRenderer.vue')['default']

View File

@@ -305,10 +305,9 @@ watch(isOnline, () => {
if (!isOnline.value) {
bannerID = banner.showBanner(offlineBanner)
return
} else {
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
}
if (banner.content && bannerID) {
banner.removeBanner(bannerID)
}
})

View File

@@ -17,9 +17,10 @@
v-if="isEmpty(shortcutsResults)"
: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>
<details
v-for="(sectionResults, sectionTitle) in shortcutsResults"
v-else

View File

@@ -49,13 +49,15 @@
:text="`${t('state.nothing_found')} ‟${search}”`"
>
<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>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="search = ''"
/>
</HoppSmartPlaceholder>
</div>
<div
@@ -112,6 +114,7 @@ import {
WorkspaceSpotlightSearcherService,
} from "~/services/spotlight/searchers/workspace.searcher"
import { InterceptorSpotlightSearcherService } from "~/services/spotlight/searchers/interceptor.searcher"
import { platform } from "~/platform"
const t = useI18n()
@@ -141,6 +144,10 @@ useService(WorkspaceSpotlightSearcherService)
useService(SwitchWorkspaceSpotlightSearcherService)
useService(InterceptorSpotlightSearcherService)
platform.spotlight?.additionalSearchers?.forEach((searcher) =>
useService(searcher)
)
const search = ref("")
const searchSession = ref<SpotlightSearchState>()

View File

@@ -254,7 +254,7 @@
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
@@ -263,27 +263,29 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="emit('display-modal-import-export')"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
@click="emit('display-modal-add')"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -291,18 +293,20 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -274,33 +274,37 @@
:text="t('empty.collections')"
@drop.stop
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess ? null : emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="
hasNoTeamAccess
? null
: emit('display-modal-import-export')
"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="t('add.new')"
filled
outline
:disabled="hasNoTeamAccess"
:title="hasNoTeamAccess ? t('team.no_access') : ''"
@click="hasNoTeamAccess ? null : emit('display-modal-add')"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'collections'"
@@ -309,18 +313,20 @@
:text="t('empty.collections')"
@drop.stop
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
node.data.type === 'collections' &&
emit('add-folder', {
path: node.id,
folder: node.data.data.data,
})
"
/>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-else-if="node.data.type === 'folders'"

View File

@@ -180,16 +180,18 @@
:alt="`${t('empty.collection')}`"
:text="t('empty.collection')"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
@click="
emit('add-folder', {
path: `${collectionIndex}`,
})
"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -176,8 +176,7 @@
:src="`/images/states/${colorMode.value}/pack.svg`"
:alt="`${t('empty.folder')}`"
:text="t('empty.folder')"
>
</HoppSmartPlaceholder>
/>
</div>
</div>
<HoppSmartConfirmModal

View File

@@ -11,7 +11,7 @@
type="search"
autocomplete="off"
: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
class="flex flex-1 flex-shrink-0 justify-between border-y border-dividerLight bg-primary"
@@ -66,34 +66,36 @@
:alt="`${t('empty.collections')}`"
:text="t('empty.collections')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("collection.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="t('add.new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="!(filteredCollections.length !== 0 || collections.length === 0)"
:text="`${t('state.nothing_found')}${filterText}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
<CollectionsGraphqlAdd

View File

@@ -22,7 +22,7 @@
v-model="filterTexts"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="t('action.search')"
:disabled="collectionsType.type === 'team-collections'"
/>

View File

@@ -11,7 +11,9 @@
v-if="!currentInterceptorSupportsCookies"
: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>
<div v-else class="flex flex-col">
<div
@@ -38,8 +40,7 @@
:alt="`${t('cookies.modal.empty_domains')}`"
:text="t('cookies.modal.empty_domains')"
class="mt-6"
>
</HoppSmartPlaceholder>
/>
<div
v-for="[domain, entries] in workingCookieJar.entries()"
v-else

View File

@@ -0,0 +1,208 @@
<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 shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const sharedRequestURL = computed(() => {
return `${shortcodeBaseURL}/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>

View File

@@ -93,20 +93,12 @@
}
"
/>
<div
<HoppSmartPlaceholder
v-if="myEnvironments.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
: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>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
</HoppSmartTab>
<HoppSmartTab
:id="'team-environments'"
@@ -140,20 +132,12 @@
}
"
/>
<div
<HoppSmartPlaceholder
v-if="teamEnvironmentList.length === 0"
class="flex flex-col items-center justify-center text-secondaryLight"
>
<img
: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>
:src="`/images/states/${colorMode.value}/blockchain.svg`"
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
/>
</div>
<div
v-if="!teamListLoading && teamAdapterError"

View File

@@ -78,11 +78,13 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -38,27 +38,29 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
@click="displayModalImportExport(true)"
/>
<HoppButtonSecondary
:icon="IconPlus"
:label="`${t('add.new')}`"
filled
outline
@click="displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showModalDetails"

View File

@@ -81,18 +81,20 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
<template #body>
<HoppButtonSecondary
v-if="isViewer"
disabled
:label="`${t('add.new')}`"
filled
/>
<HoppButtonSecondary
v-else
:label="`${t('add.new')}`"
filled
@click="addEnvironmentVariable"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -49,31 +49,33 @@
:alt="`${t('empty.environments')}`"
:text="t('empty.environments')"
>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
<template #body>
<div class="flex flex-col items-center space-y-4">
<span class="text-center text-secondaryLight">
{{ t("environment.import_or_create") }}
</span>
<div class="flex flex-col items-stretch gap-4">
<HoppButtonPrimary
:icon="IconImport"
:label="t('import.title')"
filled
outline
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalImportExport(true)"
/>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
outline
:icon="IconPlus"
:title="isTeamViewer ? t('team.no_access') : ''"
:disabled="isTeamViewer"
@click="isTeamViewer ? null : displayModalAdd(true)"
/>
</div>
</div>
</div>
</template>
</HoppSmartPlaceholder>
<div v-else-if="!loading">
<EnvironmentsTeamsEnvironment

View File

@@ -120,14 +120,16 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -162,12 +162,14 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addHeader"
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -20,8 +20,7 @@
:src="`/images/states/${colorMode.value}/add_comment.svg`"
:alt="`${t('empty.documentation')}`"
:text="t('empty.documentation')"
>
</HoppSmartPlaceholder>
/>
<div v-else>
<div
class="sticky top-0 z-10 flex flex-shrink-0 overflow-x-auto bg-primary"
@@ -30,7 +29,7 @@
v-model="graphqlFieldsFilterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">

View File

@@ -3,13 +3,13 @@
<div
class="sticky top-0 z-10 flex flex-shrink-0 flex-col overflow-x-auto border-b border-dividerLight bg-primary"
>
<WorkspaceCurrent :section="t('tab.history')" />
<WorkspaceCurrent :section="t('tab.history')" :is-only-personal="true" />
<div class="flex">
<input
v-model="filterText"
type="search"
autocomplete="off"
class="flex w-full bg-transparent px-4 py-2"
class="flex w-full bg-transparent px-4 py-2 h-8"
:placeholder="`${t('action.search')}`"
/>
<div class="flex">
@@ -114,8 +114,7 @@
:src="`/images/states/${colorMode.value}/history.svg`"
:alt="`${t('empty.history')}`"
:text="t('empty.history')"
>
</HoppSmartPlaceholder>
/>
<HoppSmartPlaceholder
v-else-if="
Object.keys(filteredHistoryGroups).length === 0 ||
@@ -124,18 +123,20 @@
:text="`${t('state.nothing_found')} ‟${filterText || filterSelection}”`"
>
<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>
<HoppButtonSecondary
:label="t('action.clear')"
outline
@click="
() => {
filterText = ''
filterSelection = 'ALL'
}
"
/>
</HoppSmartPlaceholder>
<HoppSmartConfirmModal
:show="confirmRemove"

View File

@@ -119,14 +119,16 @@
:alt="`${t('empty.authorization')}`"
:text="t('empty.authorization')"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="flex flex-1 border-b border-dividerLight">
<div class="w-2/3 border-r border-dividerLight">

View File

@@ -112,14 +112,16 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('app.documentation')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/uploading-data"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -158,12 +158,14 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
filled
:icon="IconPlus"
@click="addBodyParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</template>

View File

@@ -61,7 +61,7 @@
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
</div>

View File

@@ -209,12 +209,14 @@
:alt="`${t('empty.headers')}`"
:text="t('empty.headers')"
>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addHeader"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -157,12 +157,14 @@
:alt="`${t('empty.parameters')}`"
:text="t('empty.parameters')"
>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('add.new')}`"
:icon="IconPlus"
filled
@click="addParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -180,6 +180,18 @@
class="input mb-2 !bg-primaryContrast"
@keyup.enter="hide()"
/>
<HoppSmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
:icon="IconFolderPlus"
@click="
() => {
showSaveRequestModal = true
hide()
}
"
/>
<hr />
<HoppSmartItem
ref="copyRequestAction"
:label="t('request.share_request')"
@@ -192,18 +204,6 @@
}
"
/>
<hr />
<HoppSmartItem
ref="saveRequestAction"
:label="`${t('request.save_as')}`"
:icon="IconFolderPlus"
@click="
() => {
showSaveRequestModal = true
hide()
}
"
/>
</div>
</template>
</tippy>

View File

@@ -5,13 +5,18 @@
render-inactive-tabs
>
<HoppSmartTab
v-if="properties ? properties.includes('parameters') : true"
:id="'params'"
:label="`${t('tab.parameters')}`"
:info="`${newActiveParamsCount$}`"
>
<HttpParameters v-model="request.params" />
</HoppSmartTab>
<HoppSmartTab :id="'bodyParams'" :label="`${t('tab.body')}`">
<HoppSmartTab
v-if="properties ? properties.includes('body') : true"
:id="'bodyParams'"
:label="`${t('tab.body')}`"
>
<HttpBody
v-model:headers="request.headers"
v-model:body="request.body"
@@ -19,16 +24,22 @@
/>
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('headers') : true"
:id="'headers'"
:label="`${t('tab.headers')}`"
:info="`${newActiveHeadersCount$}`"
>
<HttpHeaders v-model="request" @change-tab="changeOptionTab" />
</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" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('preRequestScript') : true"
:id="'preRequestScript'"
:label="`${t('tab.pre_request_script')}`"
:indicator="
@@ -40,6 +51,7 @@
<HttpPreRequestScript v-model="request.preRequestScript" />
</HoppSmartTab>
<HoppSmartTab
v-if="properties ? properties.includes('tests') : true"
:id="'tests'"
:label="`${t('tab.tests')}`"
:indicator="
@@ -76,6 +88,7 @@ const props = withDefaults(
defineProps<{
modelValue: HoppRESTRequest
optionTab: RESTOptionTabs
properties?: string[]
}>(),
{
optionTab: "params",

View File

@@ -1,6 +1,6 @@
<template>
<div class="relative flex flex-1 flex-col">
<HttpResponseMeta :response="doc.response" />
<HttpResponseMeta :response="doc.response" :is-embed="isEmbed" />
<LensesResponseBodyRenderer
v-if="!loading && hasResponse"
v-model:document="doc"
@@ -15,6 +15,7 @@ import { HoppRESTDocument } from "~/helpers/rest/document"
const props = defineProps<{
document: HoppRESTDocument
isEmbed: boolean
}>()
const emit = defineEmits<{

View File

@@ -2,8 +2,20 @@
<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"
>
<AppShortcutsPrompt v-if="response == null" class="flex-1" />
<div v-else class="flex flex-1 flex-col">
<AppShortcutsPrompt v-if="response == null && !isEmbed" class="flex-1" />
<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
v-if="response.type === 'loading'"
class="flex flex-col items-center justify-center"
@@ -25,7 +37,9 @@
:text="t('helpers.network_fail')"
large
>
<AppInterceptor class="rounded border border-dividerLight p-2" />
<template #body>
<AppInterceptor class="rounded border border-dividerLight p-2" />
</template>
</HoppSmartPlaceholder>
<HoppSmartPlaceholder
v-if="response.type === 'script_fail'"
@@ -35,12 +49,14 @@
:text="t('helpers.script_fail')"
large
>
<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 }}
</div>
<template #body>
<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 }}
</div>
</template>
</HoppSmartPlaceholder>
<div
v-if="response.type === 'success' || response.type === 'fail'"
@@ -105,6 +121,7 @@ import { getStatusCodeReasonPhrase } from "~/helpers/utils/statusCodes"
import { useService } from "dioc/vue"
import { InspectionService } from "~/services/inspection"
import { RESTTabService } from "~/services/tab/rest"
import IconExternalLink from "~icons/lucide/external-link"
const t = useI18n()
const colorMode = useColorMode()
@@ -112,6 +129,7 @@ const tabs = useService(RESTTabService)
const props = defineProps<{
response: HoppRESTResponse | null | undefined
isEmbed?: boolean
}>()
/**

View File

@@ -41,7 +41,7 @@
<div class="divide-y divide-dividerLight">
<div
v-if="noEnvSelected && !globalHasAdditions"
class="flex bg-info p-4 text-secondaryDark"
class="flex bg-bannerInfo p-4 text-secondaryDark"
role="alert"
>
<icon-lucide-alert-triangle class="svg-icons mr-4" />
@@ -159,8 +159,7 @@
:alt="`${t('error.test_script_fail')}`"
:heading="t('error.test_script_fail')"
:text="t('helpers.test_script_fail')"
>
</HoppSmartPlaceholder>
/>
<HoppSmartPlaceholder
v-else
:src="`/images/states/${colorMode.value}/validation.svg`"
@@ -168,15 +167,16 @@
:heading="t('empty.tests')"
:text="t('helpers.tests')"
>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
class="my-4"
/>
<template #body>
<HoppButtonSecondary
outline
:label="`${t('action.learn_more')}`"
to="https://docs.hoppscotch.io/documentation/getting-started/rest/tests"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<EnvironmentsMyDetails
:show="showMyEnvironmentDetailsModal"

View File

@@ -149,12 +149,14 @@
:alt="`${t('empty.body')}`"
:text="t('empty.body')"
>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
<template #body>
<HoppButtonSecondary
filled
:label="`${t('add.new')}`"
:icon="IconPlus"
@click="addUrlEncodedParam"
/>
</template>
</HoppSmartPlaceholder>
</div>
</div>

View File

@@ -5,55 +5,57 @@
:heading="t('error.network_fail')"
large
>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
</div>
<div class="flex flex-col 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>
<template #body>
<div class="my-1 flex flex-col items-center text-secondaryLight">
<span>
{{ t("error.please_install_extension") }}
</span>
<span>
{{ t("error.check_how_to_add_origin") }}
<HoppSmartLink
blank
to="https://docs.hoppscotch.io/documentation/features/interceptor#browser-extension"
class="text-accent hover:text-accentDark"
>
here
</HoppSmartLink>
</span>
</div>
</div>
<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>
</template>

View File

@@ -26,7 +26,7 @@
</div>
<div
v-else-if="myTeams.length"
class="bg-info flex flex-col space-y-2 rounded-lg border border-red-500 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">
{{ t("error.danger_zone") }}
@@ -45,7 +45,7 @@
</div>
<div v-else>
<div
class="bg-info mb-4 flex flex-col space-y-2 rounded-lg border border-red-500 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">
{{ t("error.danger_zone") }}

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider"
class="border divide-y rounded divide-divider border-divider"
>
<div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }}
@@ -10,17 +10,17 @@
{{ t("shared_requests.description") }}
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="flex flex-col space-y-4 p-4">
<div class="flex flex-col p-4 space-y-4">
<div
v-for="widget in widgets"
:key="widget.value"
class="flex cursor-pointer flex-col space-y-2 rounded border border-divider px-4 py-3 hover:bg-dividerLight"
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="text-md font-bold">
<span class="mb-1 font-bold text-secondaryDark">
{{ widget.label }}
</span>
<span class="text-tiny">
@@ -28,9 +28,13 @@
</span>
</div>
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="px-4 py-3">{{ t("shared_requests.preview") }}</div>
<div class="flex flex-col items-center justify-center px-4 py-10">
<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"
@@ -132,7 +136,7 @@ const embedOption = ref<EmbedOption>({
{
value: "authorization",
label: t("tab.authorization"),
enabled: true,
enabled: false,
},
],
theme: "system",

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="selectedWidget"
class="divide-y divide-divider rounded border border-divider"
class="border divide-y rounded divide-divider border-divider"
>
<div v-if="loading" class="px-4 py-2">
{{ t("shared_requests.creating_widget") }}
@@ -14,7 +14,7 @@
<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 space-y-4 p-4">
<div class="flex flex-col p-2 space-y-2">
<HoppSmartRadioGroup
v-model="selectedWidget.value"
:radios="widgets"
@@ -22,9 +22,9 @@
/>
</div>
<div class="flex flex-col divide-y divide-divider">
<div class="flex items-center justify-center px-4 py-8">
<div v-if="selectedWidget.value === 'embed'" class="w-full flex-1">
<div class="flex flex-col pb-8">
<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"
@@ -36,7 +36,8 @@
<HoppSmartCheckbox
:on="option.enabled"
@change="removeEmbedOption(option.value)"
/>
>
</HoppSmartCheckbox>
</div>
<div class="flex items-center justify-between">
<span>
@@ -49,13 +50,11 @@
theme="popover"
:on-shown="() => tippyActions!.focus()"
>
<span class="select-wrapper">
<HoppButtonSecondary
class="ml-2 rounded-none pr-8 capitalize"
:label="embedOptions.theme"
:icon="embedThemeIcon"
/>
</span>
<HoppButtonSecondary
class="!py-2 !px-0 capitalize"
:label="embedOptions.theme"
:icon="embedThemeIcon"
/>
<template #content="{ hide }">
<div
ref="tippyActions"
@@ -102,14 +101,20 @@
</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 py-4">
<div class="flex items-center justify-center">
<HoppButtonSecondary
:label="t('shared_requests.copy_html')"
class="underline text-secondaryDark"
@click="
copyContent({
widget: 'embed',
@@ -126,12 +131,18 @@
<div
v-for="variant in buttonVariants"
:key="variant.id"
class="flex flex-col space-y-4"
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',
@@ -142,6 +153,7 @@
/>
<HoppButtonSecondary
:label="t('shared_requests.copy_markdown')"
class="underline text-secondaryDark"
@click="
copyContent({
widget: 'button',
@@ -157,12 +169,17 @@
<div
v-for="variant in linkVariants"
:key="variant.type"
class="flex flex-col items-center justify-center space-y-2"
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',
@@ -205,6 +222,35 @@ const props = defineProps({
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<{
@@ -220,6 +266,7 @@ const emit = defineEmits<{
}>()
const selectedWidget = useVModel(props, "modelValue")
const embedOptions = useVModel(props, "embedOptions")
type WidgetID = "embed" | "button" | "link"
@@ -254,42 +301,13 @@ type EmbedOption = {
}[]
theme: "light" | "dark" | "system"
}
const embedOptions = 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: true,
},
],
theme: "system",
})
const embedThemeIcon = computed(() => {
if (embedOptions.value.theme === "system") {
return IconMonitor
} else if (embedOptions.value.theme === "light") {
return IconSun
} else {
return IconMoon
}
return IconMoon
})
const removeEmbedOption = (option: EmbedTabs) => {
@@ -352,15 +370,11 @@ const linkVariants: LinkVariant[] = [
},
]
const baseURL = import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const copyEmbed = () => {
const options = embedOptions.value
const enabledEmbedOptions = options.tabs
.filter((tab) => tab.enabled)
.map((tab) => tab.value)
.toString()
return `<iframe src="${baseURL}/e/${props.request?.id}/${enabledEmbedOptions}' style='width: 100%; height: 500px; border: 0; border-radius: 4px; overflow: hidden;'></iframe>`
return `<iframe src="${shortcodeBaseURL}/e/${props.request?.id}" title="Hoppscotch Embed" style="width: 100%; height: 480px; border-radius: 4px; border: 1px solid rgba(0, 0, 0, 0.1);"></iframe>`
}
const copyButton = (
@@ -377,20 +391,18 @@ const copyButton = (
}
if (type === "markdown") {
return `[![Run in Hoppscotch](${baseURL}/${badge})](${baseURL}/r/${props.request?.id})`
} else {
return `<a href="${baseURL}/r/${props.request?.id}"><img src="${baseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
return `[![Run in Hoppscotch](${shortcodeBaseURL}/${badge})](${shortcodeBaseURL}/r/${props.request?.id})`
}
return `<a href="${shortcodeBaseURL}/r/${props.request?.id}"><img src="${shortcodeBaseURL}/${badge}" alt="Run in Hoppscotch" /></a>`
}
const copyLink = (variationID: string) => {
if (variationID === "link1") {
return `${baseURL}/r/${props.request?.id}`
return `${shortcodeBaseURL}/r/${props.request?.id}`
} else if (variationID === "link2") {
return `<a href="${baseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
} else {
return `[Run in Hoppscotch](${baseURL}/r/${props.request?.id})`
return `<a href="${shortcodeBaseURL}/r/${props.request?.id}">Run in Hoppscotch</a>`
}
return `[Run in Hoppscotch](${shortcodeBaseURL}/r/${props.request?.id})`
}
const copyContent = ({
@@ -410,7 +422,10 @@ const copyContent = ({
} else {
content = copyEmbed()
}
const copyContent = { sharedRequestID: props.request?.id, content }
const copyContent = {
sharedRequestID: props.request?.id,
content,
}
emit("copy-shared-request", copyContent)
}

View File

@@ -2,7 +2,9 @@
<HoppSmartModal
v-if="show"
dialog
:title="t('modal.share_request')"
:title="
step === 1 ? t('modal.share_request') : t('modal.customize_request')
"
styles="sm:max-w-md"
@close="hideModal"
>
@@ -21,14 +23,14 @@
<ShareCustomizeModal
v-else-if="step === 2"
v-model="selectedWidget"
v-model:embed-options="embedOptions"
:request="request"
:loading="loading"
@copy-shared-request="copySharedRequest"
/>
</template>
<template #footer>
<div v-if="step === 1" class="flex justify-end">
<template v-if="step === 1" #footer>
<div class="flex justify-start flex-1">
<HoppButtonPrimary
:label="t('action.create')"
:loading="loading"
@@ -36,11 +38,12 @@
/>
<HoppButtonSecondary
:label="t('action.cancel')"
class="mr-2"
class="ml-2"
filled
outline
@click="hideModal"
/>
</div>
<HoppButtonPrimary v-else :label="t('action.close')" @click="hideModal" />
</template>
</HoppSmartModal>
</template>
@@ -53,6 +56,18 @@ 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>,
@@ -75,6 +90,35 @@ const props = defineProps({
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"
@@ -86,6 +130,7 @@ type Widget = {
}
const selectedWidget = useVModel(props, "modelValue")
const embedOptions = useVModel(props, "embedOptions")
const emit = defineEmits<{
(e: "create-shared-request", request: HoppRESTRequest | null): void
@@ -94,7 +139,7 @@ const emit = defineEmits<{
(e: "update:step", value: number): void
(
e: "copy-shared-request",
request: {
payload: {
sharedRequestID: string | undefined
content: string | undefined
}
@@ -105,11 +150,11 @@ const createSharedRequest = () => {
emit("create-shared-request", props.request as HoppRESTRequest)
}
const copySharedRequest = (request: {
const copySharedRequest = (payload: {
sharedRequestID: string | undefined
content: string | undefined
}) => {
emit("copy-shared-request", request)
emit("copy-shared-request", payload)
}
const hideModal = () => {

View File

@@ -1,31 +1,31 @@
<template>
<div
class="group flex items-stretch"
class="flex items-stretch group"
@contextmenu.prevent="options!.tippy.show()"
>
<div
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
class="pointer-events-auto flex min-w-0 flex-1 cursor-pointer items-center justify-center py-2"
class="flex items-center justify-center flex-1 min-w-0 py-2 cursor-pointer pointer-events-auto"
:title="`${timeStamp}`"
@click="openInNewTab"
>
<span
class="pointer-events-none flex w-16 items-center justify-center truncate px-2"
class="flex items-center justify-center w-16 px-2 truncate pointer-events-none"
:style="{ color: requestLabelColor }"
>
<span class="truncate text-tiny font-semibold">
<span class="font-semibold truncate text-tiny">
{{ parseRequest.method }}
</span>
</span>
<span
class="pointer-events-none flex min-w-0 flex-1 items-center pr-2 transition group-hover:text-secondaryDark"
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-1 truncate border-l border-dividerDark px-2 text-secondaryLight group-hover:text-secondaryDark"
class="flex px-2 truncate text-secondaryLight group-hover:text-secondaryDark"
>
{{ parseRequest.name }}
</span>
@@ -69,7 +69,7 @@
/>
<HoppSmartItem
ref="customizeAction"
:icon="IconFileEdit"
:icon="IconCustomize"
:label="`${t('shared_requests.customize')}`"
:shortcut="['E']"
@click="
@@ -110,7 +110,7 @@ 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 IconFileEdit from "~icons/lucide/file-edit"
import IconCustomize from "~icons/lucide/settings-2"
import IconTrash2 from "~icons/lucide/trash-2"
import { shortDateTime } from "~/helpers/utils/date"
@@ -121,7 +121,12 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
(e: "customize-shared-request", request: HoppRESTRequest, id: string): void
(
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
}>()
@@ -130,6 +135,7 @@ 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)
@@ -144,7 +150,13 @@ const openInNewTab = () => {
}
const customizeSharedRequest = () => {
emit("customize-shared-request", parseRequest.value, props.request.id)
const embedProperties = props.request.properties
emit(
"customize-shared-request",
parseRequest.value,
props.request.id,
embedProperties
)
}
const deleteSharedRequest = () => {

View File

@@ -13,7 +13,7 @@
>
<HoppButtonSecondary
v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/documentation/features/shared-request"
to="https://docs.hoppscotch.io/documentation/features/share-requests"
blank
:title="t('app.wiki')"
:icon="IconHelpCircle"
@@ -32,10 +32,12 @@
:alt="`${t('empty.shared_requests_logout')}`"
:text="`${t('empty.shared_requests_logout')}`"
>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
<template #body>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</template>
</HoppSmartPlaceholder>
<template v-else-if="sharedRequests.length">
@@ -80,11 +82,12 @@
/>
<ShareModal
v-model="selectedWidget"
v-model:embed-options="embedOptions"
:step="step"
:request="requestToShare"
:show="showShareRequestModal"
:loading="shareRequestCreatingLoading"
:step="step"
@hide-modal="displayCustomizeRequestModal(false)"
@hide-modal="displayCustomizeRequestModal(false, null)"
@copy-shared-request="copySharedRequest"
@create-shared-request="createSharedRequest"
/>
@@ -105,6 +108,7 @@ 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"
@@ -114,6 +118,7 @@ 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()
@@ -130,6 +135,70 @@ 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(
@@ -139,6 +208,18 @@ const currentUser = useReadonlyStream(
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 = {
@@ -218,15 +299,73 @@ const displayShareRequestModal = (show: boolean) => {
showShareRequestModal.value = show
step.value = 1
}
const displayCustomizeRequestModal = (show: boolean) => {
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)()
const sharedRequestResult = await createShortcode(
request,
selectedWidget.value.value === "embed"
? JSON.stringify(properties)
: undefined
)()
platform.analytics?.logEvent({
type: "HOPP_SHORTCODE_CREATED",
@@ -243,6 +382,23 @@ const createSharedRequest = async (request: HoppRESTRequest | null) => {
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,
}
}
}
}
}
@@ -250,21 +406,22 @@ const createSharedRequest = async (request: HoppRESTRequest | null) => {
const customizeSharedRequest = (
request: HoppRESTRequest,
shredRequestID: string
shredRequestID: string,
embedProperties?: string | null
) => {
requestToShare.value = {
...request,
id: shredRequestID,
}
displayCustomizeRequestModal(true)
displayCustomizeRequestModal(true, embedProperties)
}
const copySharedRequest = (request: {
const copySharedRequest = (payload: {
sharedRequestID: string | undefined
content: string | undefined
}) => {
if (request.content) {
copyToClipboard(request.content)
if (payload.content) {
copyToClipboard(payload.content)
toast.success(t("state.copied_to_clipboard"))
}
}
@@ -291,13 +448,12 @@ const resolveConfirmModal = (title: string | null) => {
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("shared_request.not_found")
default:
return t("error.something_went_wrong")
}
}
switch (err.error) {
case "shortcode/not_found":
return t("shared_request.not_found")
default:
return t("error.something_went_wrong")
}
}

View File

@@ -1,10 +1,6 @@
<template>
<div
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5"
>
<a href="/" target="_blank">
<img :src="img" :alt="t('shared_requests.run_in_hoppscotch')" />
</a>
<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>

View File

@@ -1,32 +1,32 @@
<template>
<div
class="flex flex-col rounded border border-dotted border-divider p-5"
class="flex flex-col p-4 border rounded border-dividerDark"
:class="{
'bg-accentContrast': isEmbedThemeLight,
}"
>
<div
class="flex items-stretch space-x-4 rounded border-divider"
class="flex items-stretch space-x-2"
:class="{
'bg-accentContrast': isEmbedThemeLight,
}"
>
<span
class="flex max-w-[4rem] items-center justify-center rounded border border-divider px-1 py-2 text-tiny"
:class="{
'!border-dividerLight bg-accentContrast text-primary':
isEmbedThemeLight,
}"
>
<span class="truncate">
{{ method }}
</span>
</span>
<span
class="flex max-w-46 items-center rounded border border-divider p-2"
class="flex items-center flex-1 min-w-0 border rounded border-divider"
>
<span
class="min-w-0 truncate"
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,
}"
@@ -35,7 +35,7 @@
</span>
</span>
<button
class="flex items-center justify-center rounded border border-dividerDark bg-primaryDark px-3 py-2 font-semibold text-secondary"
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,
}"
@@ -44,10 +44,10 @@
</button>
</div>
<div
class="flex border-divider"
class="flex"
:class="{
'bg-accentContrast text-primary': isEmbedThemeLight,
'border-b pt-2 ': !noActiveTab,
'border-b border-divider pt-2': !noActiveTab,
}"
>
<span
@@ -57,7 +57,8 @@
class="px-2 py-2"
:class="{
'border-b border-dividerDark':
embedOptions.selectedTab === option.value,
embedOptions.tabs.filter((tab) => tab.enabled)[0]?.value ===
option.value,
}"
>
{{ option.label }}

View File

@@ -1,7 +1,5 @@
<template>
<div
class="flex items-center justify-center rounded border border-dotted border-dividerDark p-5"
>
<div class="flex flex-col items-center p-4 border rounded border-dividerDark">
<span
:class="{
'border-b border-secondary': label,
@@ -23,7 +21,12 @@ const props = defineProps<{
label?: string | undefined
}>()
const shortcodeBaseURL =
import.meta.env.VITE_SHORTCODE_BASE_URL ?? "https://hopp.sh"
const text = computed(() => {
return props.label ? t(props.label) : `hopp.sh/r/${props.link ?? "xxxx"}`
return props.label
? t(props.label)
: `${shortcodeBaseURL}/r/${props.link ?? "xxxx"}`
})
</script>

View File

@@ -60,7 +60,7 @@
:text="`${t('state.nothing_found')}${searchQuery}`"
>
<template #icon>
<icon-lucide-search class="svg-icons pb-2 opacity-75" />
<icon-lucide-search class="svg-icons opacity-75" />
</template>
</HoppSmartPlaceholder>
</div>

View File

@@ -45,15 +45,17 @@
:alt="`${t('empty.members')}`"
:text="t('empty.members')"
>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('team.invite')"
@click="
() => {
emit('invite-team')
}
"
/>
<template #body>
<HoppButtonSecondary
:icon="IconUserPlus"
:label="t('team.invite')"
@click="
() => {
emit('invite-team')
}
"
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="divide-y divide-dividerLight">
<div

View File

@@ -125,8 +125,7 @@
pendingInvites.data.right.team?.teamInvitations.length === 0
"
:text="t('empty.pending_invites')"
>
</HoppSmartPlaceholder>
/>
<div
v-if="!pendingInvites.loading && E.isLeft(pendingInvites.data)"
class="flex flex-col items-center p-4"
@@ -245,11 +244,13 @@
:alt="`${t('empty.invites')}`"
:text="`${t('empty.invites')}`"
>
<HoppButtonSecondary
:label="t('add.new')"
filled
@click="addNewInvitee"
/>
<template #body>
<HoppButtonSecondary
:label="t('add.new')"
filled
@click="addNewInvitee"
/>
</template>
</HoppSmartPlaceholder>
</div>
<div

View File

@@ -16,11 +16,13 @@
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
<template #body>
<HoppButtonSecondary
:label="`${t('team.create_new')}`"
filled
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div
v-else-if="!loading"

View File

@@ -21,13 +21,15 @@
:alt="`${t('empty.teams')}`"
:text="`${t('empty.teams')}`"
>
<HoppButtonSecondary
:label="t('team.create_new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
<template #body>
<HoppButtonSecondary
:label="t('team.create_new')"
filled
outline
:icon="IconPlus"
@click="displayModalAdd(true)"
/>
</template>
</HoppSmartPlaceholder>
<div v-else-if="!loading" class="flex flex-col">
<div

View File

@@ -1,6 +1,7 @@
import { flow } from "fp-ts/function"
import { useI18n as _useI18n } from "vue-i18n"
export const useI18n = flow(_useI18n, (x) => x.t)
export function useI18n() {
return _useI18n().t
}
export const useFullI18n = _useI18n

View File

@@ -1,26 +1,15 @@
import { Environment } from "@hoppscotch/data"
import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox"
import { runTestScript } from "@hoppscotch/js-sandbox/web"
import * as A from "fp-ts/Array"
import * as E from "fp-ts/Either"
import * as O from "fp-ts/Option"
import { flow, pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import { Observable, Subject } from "rxjs"
import { filter } from "rxjs/operators"
import { flow, pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { Environment } from "@hoppscotch/data"
import {
SandboxTestResult,
runTestScript,
TestDescriptor,
} from "@hoppscotch/js-sandbox"
import * as E from "fp-ts/Either"
import { cloneDeep } from "lodash-es"
import {
getCombinedEnvVariables,
getFinalEnvsFromPreRequest,
} from "./preRequest"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { createRESTNetworkRequestStream } from "./network"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { isJSONContentType } from "./utils/contenttypes"
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
import { Ref } from "vue"
import {
environmentsStore,
getCurrentEnvironment,
@@ -29,9 +18,18 @@ import {
setGlobalEnvVariables,
updateEnvironment,
} from "~/newstore/environments"
import { Ref } from "vue"
import { HoppTab } from "~/services/tab"
import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment"
import { createRESTNetworkRequestStream } from "./network"
import {
getCombinedEnvVariables,
getFinalEnvsFromPreRequest,
} from "./preRequest"
import { HoppRESTDocument } from "./rest/document"
import { HoppRESTResponse } from "./types/HoppRESTResponse"
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
import { isJSONContentType } from "./utils/contenttypes"
const getTestableBody = (
res: HoppRESTResponse & { type: "success" | "fail" }
@@ -89,7 +87,7 @@ export function runRESTRequest$(
const res = getFinalEnvsFromPreRequest(
tab.value.document.request.preRequestScript,
getCombinedEnvVariables()
)().then((envs) => {
).then((envs) => {
if (cancelCalled) return E.left("cancellation" as const)
if (E.isLeft(envs)) {
@@ -125,7 +123,7 @@ export function runRESTRequest$(
body: getTestableBody(res),
headers: res.headers,
}
)()
)
if (E.isRight(runResult)) {
tab.value.document.testResults = translateToSandboxTestResults(

View File

@@ -1,5 +1,5 @@
mutation CreateShortcode($request: String!) {
createShortcode(request: $request) {
mutation CreateShortcode($request: String!, $properties: String) {
createShortcode(request: $request, properties: $properties) {
id
request
createdOn

View File

@@ -0,0 +1,8 @@
mutation UpdateEmbedProperties($code: ID!, $properties: String!) {
updateEmbedProperties(code: $code, properties: $properties) {
id
request
properties
createdOn
}
}

View File

@@ -2,5 +2,6 @@ query ResolveShortcode($code: ID!) {
shortcode(code: $code) {
id
request
properties
}
}
}

View File

@@ -0,0 +1,8 @@
subscription ShortcodeUpdated {
myShortcodesUpdated {
id
request
createdOn
properties
}
}

View File

@@ -7,15 +7,22 @@ import {
DeleteShortcodeDocument,
DeleteShortcodeMutation,
DeleteShortcodeMutationVariables,
UpdateEmbedPropertiesDocument,
UpdateEmbedPropertiesMutation,
UpdateEmbedPropertiesMutationVariables,
} from "../graphql"
type DeleteShortcodeErrors = "shortcode/not_found"
export const createShortcode = (request: HoppRESTRequest) =>
export const createShortcode = (
request: HoppRESTRequest,
properties?: string
) =>
runMutation<CreateShortcodeMutation, CreateShortcodeMutationVariables, "">(
CreateShortcodeDocument,
{
request: JSON.stringify(request),
properties,
}
)
@@ -27,3 +34,13 @@ export const deleteShortcode = (code: string) =>
>(DeleteShortcodeDocument, {
code,
})
export const updateEmbedProperties = (code: string, properties: string) =>
runMutation<
UpdateEmbedPropertiesMutation,
UpdateEmbedPropertiesMutationVariables,
""
>(UpdateEmbedPropertiesDocument, {
code,
properties,
})

View File

@@ -1,10 +1,13 @@
import { runPreRequestScript } from "@hoppscotch/js-sandbox"
import * as E from "fp-ts/Either"
import { runPreRequestScript } from "@hoppscotch/js-sandbox/web"
import { Environment } from "@hoppscotch/data"
import { cloneDeep } from "lodash-es"
import {
getCurrentEnvironment,
getGlobalVariables,
} from "~/newstore/environments"
import { TestResult } from "@hoppscotch/js-sandbox"
export const getCombinedEnvVariables = () => ({
global: cloneDeep(getGlobalVariables()),
@@ -17,4 +20,5 @@ export const getFinalEnvsFromPreRequest = (
global: Environment["variables"]
selected: Environment["variables"]
}
) => runPreRequestScript(script, envs)
): Promise<E.Either<string, TestResult["envs"]>> =>
runPreRequestScript(script, envs)

View File

@@ -4,6 +4,6 @@
export interface Shortcode {
id: string
request: string
properties?: string | null | undefined
properties?: string | null
createdOn: Date
}

View File

@@ -11,6 +11,7 @@ import {
GetUserShortcodesDocument,
ShortcodeCreatedDocument,
ShortcodeDeletedDocument,
ShortcodeUpdatedDocument,
} from "../backend/graphql"
import { BACKEND_PAGE_SIZE } from "../backend/helpers"
import { Shortcode } from "./Shortcode"
@@ -25,9 +26,11 @@ export default class ShortcodeListAdapter {
private shortcodeCreated: Subscription | null
private shortcodeRevoked: Subscription | null
private shortcodeUpdated: Subscription | null
private shortcodeCreatedSub: WSubscription | null
private shortcodeRevokedSub: WSubscription | null
private shortcodeUpdatedSub: WSubscription | null
constructor(deferInit = false) {
this.error$ = new BehaviorSubject<GQLError<string> | null>(null)
@@ -39,8 +42,10 @@ export default class ShortcodeListAdapter {
this.isDispose = true
this.shortcodeCreated = null
this.shortcodeRevoked = null
this.shortcodeUpdated = null
this.shortcodeCreatedSub = null
this.shortcodeRevokedSub = null
this.shortcodeUpdatedSub = null
if (!deferInit) this.initialize()
}
@@ -48,8 +53,10 @@ export default class ShortcodeListAdapter {
unsubscribeSubscriptions() {
this.shortcodeCreated?.unsubscribe()
this.shortcodeRevoked?.unsubscribe()
this.shortcodeUpdated?.unsubscribe()
this.shortcodeCreatedSub?.unsubscribe()
this.shortcodeRevokedSub?.unsubscribe()
this.shortcodeUpdatedSub?.unsubscribe()
}
initialize() {
@@ -137,6 +144,14 @@ export default class ShortcodeListAdapter {
this.shortcodes$.next(newShortcode)
}
private updateSharedRequest(shortcode: Shortcode) {
const newShortcode = this.shortcodes$.value.map((oldShortcode) =>
oldShortcode.id === shortcode.id ? shortcode : oldShortcode
)
this.shortcodes$.next(newShortcode)
}
private registerSubscriptions() {
const [shortcodeCreated$, shortcodeCreatedSub] = runAuthOnlyGQLSubscription(
{
@@ -169,5 +184,21 @@ export default class ShortcodeListAdapter {
this.deleteSharedRequest(result.right.myShortcodesRevoked.id)
})
const [shortcodeUpdated$, shortcodeUpdatedSub] = runAuthOnlyGQLSubscription(
{
query: ShortcodeUpdatedDocument,
}
)
this.shortcodeUpdatedSub = shortcodeUpdatedSub
this.shortcodeUpdated = shortcodeUpdated$.subscribe((result) => {
if (E.isLeft(result)) {
console.error(result.left)
throw new Error(`Shortcode Update Error ${result.left}`)
}
this.updateSharedRequest(result.right.myShortcodesUpdated)
})
}
}

View File

@@ -1,7 +1,107 @@
<template>
<div class="flex flex-col items-center justify-between p-8">
Temporary page for Embed till the feature is ready
<div class="flex flex-col flex-1 w-full">
<Embeds
v-if="tab"
v-model:modelTab="tab"
:properties="properties"
:shared-request-i-d="sharedRequestID"
/>
</div>
</template>
<script setup lang="ts"></script>
<script setup lang="ts">
import { ref } from "vue"
import { watch } from "vue"
import { useRoute } from "vue-router"
import { useGQLQuery } from "~/composables/graphql"
import {
ResolveShortcodeDocument,
ResolveShortcodeQuery,
ResolveShortcodeQueryVariables,
} from "~/helpers/backend/graphql"
import * as E from "fp-ts/Either"
import { onMounted } from "vue"
import {
getDefaultRESTRequest,
safelyExtractRESTRequest,
} from "@hoppscotch/data"
import { HoppTab } from "~/services/tab"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { applySetting } from "~/newstore/settings"
const route = useRoute()
const sharedRequestID = ref("")
const invalidLink = ref(false)
const properties = ref([])
const sharedRequestDetails = useGQLQuery<
ResolveShortcodeQuery,
ResolveShortcodeQueryVariables,
""
>({
query: ResolveShortcodeDocument,
variables: {
code: route.params.id.toString(),
},
})
const tab = ref<HoppTab<HoppRESTDocument>>({
id: "0",
document: {
request: getDefaultRESTRequest(),
response: null,
isDirty: false,
},
})
watch(
() => sharedRequestDetails.data,
() => {
if (sharedRequestDetails.loading) return
const data = sharedRequestDetails.data
if (E.isRight(data)) {
if (!data.right.shortcode?.request) {
invalidLink.value = true
return
}
const request: unknown = JSON.parse(
data.right.shortcode?.request as string
)
tab.value.document.request = safelyExtractRESTRequest(
request,
getDefaultRESTRequest()
)
if (data.right.shortcode && data.right.shortcode.properties) {
const parsedProperties = JSON.parse(data.right.shortcode.properties)
if (parsedProperties.theme === "dark") {
applySetting("BG_COLOR", "dark")
} else if (parsedProperties.theme === "light") {
applySetting("BG_COLOR", "light")
} else if (parsedProperties.theme === "auto") {
applySetting("BG_COLOR", "system")
}
properties.value = parsedProperties.options
}
}
}
)
onMounted(() => {
if (typeof route.params.id === "string") {
sharedRequestID.value = route.params.id
sharedRequestDetails.execute()
}
invalidLink.value = !sharedRequestID.value
})
</script>
<route lang="yaml">
meta:
layout: empty
</route>

View File

@@ -14,10 +14,12 @@
:alt="`${t('empty.profile')}`"
:text="`${t('empty.profile')}`"
>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
<template #body>
<HoppButtonPrimary
:label="t('auth.login')"
@click="invokeAction('modals.login.toggle')"
/>
</template>
</HoppSmartPlaceholder>
<div v-else class="space-y-8">
<div

View File

@@ -5,10 +5,11 @@
content-styles="!h-[calc(100%-var(--sidebar-primary-sticky-fold)-1px)] !flex"
>
<HoppSmartTab
v-for="{ target, title } in REALTIME_NAVIGATION"
:id="target"
:key="target"
:label="title"
v-for="(navigation, index) in REALTIME_NAVIGATION"
:id="navigation.target"
:key="index"
:label="navigation.title"
:icon="navigation.icon"
>
<RouterView />
</HoppSmartTab>
@@ -20,6 +21,10 @@ import { watch, ref, computed } from "vue"
import { RouterView, useRouter, useRoute } from "vue-router"
import { usePageHead } from "~/composables/head"
import { useI18n } from "~/composables/i18n"
import IconWebsocket from "~icons/hopp/websocket"
import IconSocketio from "~icons/hopp/socketio"
import IconMqtt from "~icons/hopp/mqtt"
import IconSse from "~icons/lucide/satellite-dish"
const t = useI18n()
const router = useRouter()
@@ -29,18 +34,22 @@ const REALTIME_NAVIGATION = [
{
target: "websocket",
title: t("tab.websocket"),
icon: IconWebsocket,
},
{
target: "sse",
title: t("tab.sse"),
icon: IconSse,
},
{
target: "socketio",
title: t("tab.socketio"),
icon: IconSocketio,
},
{
target: "mqtt",
title: t("tab.mqtt"),
icon: IconMqtt,
},
] as const

View File

@@ -142,12 +142,14 @@
:alt="`${t('empty.subscription')}`"
:text="`${t('empty.subscription')}`"
>
<HoppButtonSecondary
:label="t('mqtt.new')"
filled
outline
@click="showSubscriptionModal(true)"
/>
<template #body>
<HoppButtonSecondary
:label="t('mqtt.new')"
filled
outline
@click="showSubscriptionModal(true)"
/>
</template>
</HoppSmartPlaceholder>
<div v-else>
<div

View File

@@ -197,14 +197,16 @@
:alt="`${t('socketio.connection_not_authorized')}`"
:text="`${t('socketio.connection_not_authorized')}`"
>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
<template #body>
<HoppButtonSecondary
outline
:label="t('app.documentation')"
to="https://docs.hoppscotch.io/documentation/features/authorization"
blank
:icon="IconExternalLink"
reverse
/>
</template>
</HoppSmartPlaceholder>
<div
v-if="authType === 'Bearer'"

View File

@@ -163,8 +163,7 @@
:src="`/images/states/${colorMode.value}/add_category.svg`"
:alt="`${t('empty.protocols')}`"
:text="`${t('empty.protocols')}`"
>
</HoppSmartPlaceholder>
/>
</HoppSmartTab>
</HoppSmartTabs>
</template>

View File

@@ -11,6 +11,7 @@ import { HoppModule } from "~/modules"
import { InspectorsPlatformDef } from "./inspectors"
import { Service } from "dioc"
import { IOPlatformDef } from "./io"
import { SpotlightPlatformDef } from "./spotlight"
export type PlatformDef = {
ui?: UIPlatformDef
@@ -28,6 +29,7 @@ export type PlatformDef = {
}
interceptors: InterceptorsPlatformDef
additionalInspectors?: InspectorsPlatformDef
spotlight?: SpotlightPlatformDef
platformFeatureFlags: {
exportAsGIST: boolean
hasTelemetry: boolean

View File

@@ -0,0 +1,10 @@
import { Service } from "dioc"
import { SpotlightSearcher } from "~/services/spotlight"
export type SpotlightPlatformDef = {
additionalSearchers?: Array<
typeof Service<unknown> & { ID: string } & {
new (): Service & SpotlightSearcher
}
>
}

View File

@@ -26,10 +26,9 @@ export type Banner = {
const getBannerWithHighestScore = (list: Banner[]) => {
if (list.length === 0) return null
else if (list.length === 1) return list[0]
else {
const highestScore = Math.max(...list.map((banner) => banner.content.score))
return list.find((banner) => banner.content.score === highestScore)
}
const highestScore = Math.max(...list.map((banner) => banner.content.score))
return list.find((banner) => banner.content.score === highestScore)
}
/**

View File

@@ -57,6 +57,7 @@ export type SpotlightSearcherResult = {
* The keyboard shortcut to trigger the result
*/
keyboardShortcut?: string[]
additionalInfo?: unknown
}
}

View File

@@ -0,0 +1,2 @@
export { default } from "./dist/types/index.d.ts"
export * from "./dist/types/index.d.ts"

View File

@@ -1,6 +1,10 @@
module.exports = {
export default {
preset: "ts-jest",
testEnvironment: "jsdom",
collectCoverage: true,
setupFilesAfterEnv: ["./jest.setup.ts"],
moduleNameMapper: {
"~/(.*)": "<rootDir>/src/$1",
"^lodash-es$": "lodash",
},
}

View File

@@ -0,0 +1,2 @@
export { default } from "./dist/node.d.ts"
export * from "./dist/node.d.ts"

View File

@@ -2,16 +2,27 @@
"name": "@hoppscotch/js-sandbox",
"version": "2.1.0",
"description": "JavaScript sandboxes for running external scripts used by Hoppscotch clients",
"main": "./lib/index.js",
"module": "./lib/index.mjs",
"type": "commonjs",
"type": "module",
"files": [
"dist",
"index.d.ts"
],
"exports": {
".": {
"require": "./lib/index.js",
"default": "./lib/index.mjs"
"types": "./dist/types/index.d.ts"
},
"./web": {
"import": "./dist/web.js",
"require": "./dist/web.cjs",
"types": "./dist/web.d.ts"
},
"./node": {
"import": "./dist/node.js",
"require": "./dist/node.cjs",
"types": "./dist/node.d.ts"
}
},
"types": "./lib/",
"types": "./index.d.ts",
"engines": {
"node": ">=14",
"pnpm": ">=3"
@@ -20,7 +31,7 @@
"lint": "eslint --ext .ts,.js --ignore-path .gitignore .",
"lintfix": "eslint --fix --ext .ts,.js --ignore-path .gitignore .",
"test": "pnpm exec jest",
"build": "pnpm exec tsup",
"build": "vite build && tsc --emitDeclarationOnly",
"clean": "pnpm tsc --build --clean",
"postinstall": "pnpm run build",
"prepublish": "pnpm run build",
@@ -41,10 +52,10 @@
"license": "MIT",
"dependencies": {
"@hoppscotch/data": "workspace:^",
"@types/lodash-es": "^4.17.12",
"fp-ts": "^2.11.10",
"lodash": "^4.17.21",
"quickjs-emscripten": "^0.15.0",
"tsup": "^5.12.5"
"lodash-es": "^4.17.21"
},
"devDependencies": {
"@digitak/esrun": "^3.1.2",
@@ -61,6 +72,7 @@
"jest": "^27.5.1",
"prettier": "^2.8.4",
"ts-jest": "^27.1.4",
"typescript": "^4.6.3"
"typescript": "^4.6.3",
"vite": "^5.0.4"
}
}

View File

@@ -1,10 +1,10 @@
import { execPreRequestScript } from "../preRequest"
import { runPreRequestScript } from "~/pre-request/node-vm"
import "@relmify/jest-fp-ts"
describe("execPreRequestScript", () => {
test("returns the updated envirionment properly", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob", "newbob")
`,
@@ -27,7 +27,7 @@ describe("execPreRequestScript", () => {
test("fails if the key is not a string", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set(10, "newbob")
`,
@@ -44,7 +44,7 @@ describe("execPreRequestScript", () => {
test("fails if the value is not a string", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob", 10)
`,
@@ -61,7 +61,7 @@ describe("execPreRequestScript", () => {
test("fails for invalid syntax", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("bob",
`,
@@ -78,7 +78,7 @@ describe("execPreRequestScript", () => {
test("creates new env variable if doesn't exist", () => {
return expect(
execPreRequestScript(
runPreRequestScript(
`
pw.env.set("foo", "bar")
`,

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,8 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -12,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import { pipe } from "fp-ts/function"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse, TestResult } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse, TestResult } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,13 +12,13 @@ const fakeResponse: TestResponse = {
const func = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.envs)
)
const funcTest = (script: string, envs: TestResult["envs"]) =>
pipe(
execTestScript(script, envs, fakeResponse),
runTestScript(script, envs, fakeResponse),
TE.map((x) => x.tests)
)

View File

@@ -1,7 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,7 +1,9 @@
import "@relmify/jest-fp-ts"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import "@relmify/jest-fp-ts"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -11,7 +13,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,6 +1,8 @@
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { execTestScript, TestResponse } from "../../test-runner"
import { runTestScript } from "~/test-runner/node-vm"
import { TestResponse } from "~/types"
const fakeResponse: TestResponse = {
status: 200,
@@ -10,7 +12,7 @@ const fakeResponse: TestResponse = {
const func = (script: string, res: TestResponse) =>
pipe(
execTestScript(script, { global: [], selected: [] }, res),
runTestScript(script, { global: [], selected: [] }, res),
TE.map((x) => x.tests)
)

View File

@@ -1,40 +1,15 @@
import { match } from "fp-ts/lib/Either"
import { pipe } from "fp-ts/lib/function"
import * as QuickJS from "quickjs-emscripten"
import { marshalObjectToVM } from "../utils"
import { preventCyclicObjects } from "~/utils"
let vm: QuickJS.QuickJSVm
beforeAll(async () => {
const qjs = await QuickJS.getQuickJS()
vm = qjs.createVm()
})
afterAll(() => {
vm.dispose()
})
describe("marshalObjectToVM", () => {
test("successfully marshals simple object into the vm", () => {
describe("preventCyclicObjects", () => {
test("succeeds with a simple object", () => {
const testObj = {
a: 1,
}
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
marshalObjectToVM(vm, testObj),
match(
() => null,
(result) => result
)
)
expect(objVMHandle).not.toBeNull()
expect(vm.dump(objVMHandle!)).toEqual(testObj)
objVMHandle!.dispose()
expect(preventCyclicObjects(testObj)).toBeRight()
})
test("fails marshalling cyclic object into vm", () => {
test("fails with a cyclic object", () => {
const testObj = {
a: 1,
b: null as any,
@@ -42,14 +17,6 @@ describe("marshalObjectToVM", () => {
testObj.b = testObj
const objVMHandle: QuickJS.QuickJSHandle | null = pipe(
marshalObjectToVM(vm, testObj),
match(
() => null,
(result) => result
)
)
expect(objVMHandle).toBeNull()
expect(preventCyclicObjects(testObj)).toBeLeft()
})
})

View File

@@ -1,43 +0,0 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { execPreRequestScript } from "./preRequest"
import {
execTestScript,
TestResponse as _TestResponse,
TestDescriptor as _TestDescriptor,
TestResult,
} from "./test-runner"
export * from "./test-runner"
export type TestResponse = _TestResponse
export type TestDescriptor = _TestDescriptor
export type SandboxTestResult = TestResult & { tests: TestDescriptor }
/**
* Executes a given test script on the test-runner sandbox
* @param testScript The string of the script to run
* @returns A TaskEither with an error message or a TestDescriptor with the final status
*/
export const runTestScript = (
testScript: string,
envs: TestResult["envs"],
response: TestResponse
) =>
pipe(
execTestScript(testScript, envs, response),
TE.chain((results) =>
TE.right(<SandboxTestResult>{
envs: results.envs,
tests: results.tests[0],
})
) // execTestScript returns an array of descriptors with a single element (extract that)
)
/**
* Executes a given pre-request script on the sandbox
* @param preRequestScript The script to run
* @param env The environment variables active
* @returns A TaskEither with an error message or an array of the final environments with the all the script values applied
*/
export const runPreRequestScript = execPreRequestScript

View File

@@ -0,0 +1,2 @@
export * from "./pre-request/node-vm"
export * from "./test-runner/node-vm"

View File

@@ -0,0 +1,36 @@
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/lib/TaskEither"
import { createContext, runInContext } from "vm"
import { TestResult } from "~/types"
import { getPreRequestScriptMethods } from "~/utils"
export const runPreRequestScript = (
preRequestScript: string,
envs: TestResult["envs"]
): TE.TaskEither<string, TestResult["envs"]> =>
pipe(
TE.tryCatch(
async () => {
return createContext()
},
(reason) => `Context initialization failed: ${reason}`
),
TE.chain((context) =>
TE.tryCatch(
() =>
new Promise((resolve) => {
const { pw, updatedEnvs } = getPreRequestScriptMethods(envs)
// Expose pw to the context
context.pw = pw
// Run the pre-request script in the provided context
runInContext(preRequestScript, context)
resolve(updatedEnvs)
}),
(reason) => `Script execution failed: ${reason}`
)
)
)

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