diff --git a/.gitignore b/.gitignore index 71152c46..bdfa619c 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pids *.pid *.seed *.pid.lock +*.env # Directory for instrumented libs generated by jscoverage/JSCover lib-cov diff --git a/package.json b/package.json index b3568633..a4656c8b 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "devDependencies": { "@commitlint/cli": "19.5.0", "@commitlint/config-conventional": "19.5.0", - "@hoppscotch/ui": "0.2.1", + "@hoppscotch/ui": "0.2.2", "@types/node": "22.7.6", "cross-env": "7.0.3", "http-server": "14.1.1", diff --git a/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts b/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts index daee18f6..6c79deae 100644 --- a/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts +++ b/packages/hoppscotch-cli/src/__tests__/unit/fixtures/workspace-access.mock.ts @@ -79,7 +79,7 @@ export const WORKSPACE_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: Workspa collectionID: "clx1ldkzs005t10f8rp5u60q7", teamID: "clws3hg58000011o8h07glsb1", title: "RequestA", - request: `{"v":"${RESTReqSchemaVersion}","id":"clpttpdq00003qp16kut6doqv","auth":{"authType":"inherit","authActive":true},"body":{"body":null,"contentType":null},"name":"RequestA","method":"GET","params":[],"headers":[],"endpoint":"https://echo.hoppscotch.io","testScript":"pw.test(\\"Correctly inherits auth and headers from the root collection\\", ()=> {\\n pw.expect(pw.response.body.headers[\\"x-test-header\\"]).toBe(\\"Set at root collection\\");\\n pw.expect(pw.response.body.headers[\\"authorization\\"]).toBe(\\"Bearer BearerToken\\");\\n});","preRequestScript":"","requestVariables":[]}`, + request: `{"v":"${RESTReqSchemaVersion}","id":"clpttpdq00003qp16kut6doqv","auth":{"authType":"inherit","authActive":true},"body":{"body":null,"contentType":null},"name":"RequestA","method":"GET","params":[],"headers":[],"endpoint":"https://echo.hoppscotch.io","testScript":"pw.test(\\"Correctly inherits auth and headers from the root collection\\", ()=> {\\n pw.expect(pw.response.body.headers[\\"x-test-header\\"]).toBe(\\"Set at root collection\\");\\n pw.expect(pw.response.body.headers[\\"authorization\\"]).toBe(\\"Bearer BearerToken\\");\\n});","preRequestScript":"","requestVariables":[],"responses":{}}`, }, ], }, @@ -233,6 +233,7 @@ export const TRANSFORMED_DEEPLY_NESTED_COLLECTIONS_WITH_AUTH_HEADERS_MOCK: HoppC 'pw.test("Correctly inherits auth and headers from the root collection", ()=> {\n pw.expect(pw.response.body.headers["x-test-header"]).toBe("Set at root collection");\n pw.expect(pw.response.body.headers["authorization"]).toBe("Bearer BearerToken");\n});', preRequestScript: "", requestVariables: [], + responses: {}, }, ], auth: { diff --git a/packages/hoppscotch-cli/src/__tests__/unit/workspace-access.spec.ts b/packages/hoppscotch-cli/src/__tests__/unit/workspace-access.spec.ts index da38e914..c119a1c5 100644 --- a/packages/hoppscotch-cli/src/__tests__/unit/workspace-access.spec.ts +++ b/packages/hoppscotch-cli/src/__tests__/unit/workspace-access.spec.ts @@ -31,12 +31,14 @@ const migrateCollections = (collections: unknown[]): HoppCollection[] => { ); } - return collectionSchemaParsedResult.data.map((collection) => { - return { - ...collection, - folders: migrateCollections(collection.folders), - }; - }); + return collectionSchemaParsedResult.data.map( + ({ _ref_id, folders, ...rest }) => { + return { + ...rest, + folders: migrateCollections(folders), + }; + } + ); }; describe("workspace-access", () => { diff --git a/packages/hoppscotch-common/assets/scss/styles.scss b/packages/hoppscotch-common/assets/scss/styles.scss index d62c88be..e2b37d80 100644 --- a/packages/hoppscotch-common/assets/scss/styles.scss +++ b/packages/hoppscotch-common/assets/scss/styles.scss @@ -344,26 +344,44 @@ pre.ace_editor { .info-response { color: var(--status-info-color); + &.outlined { + border: 1px solid var(--status-info-color); + } } .success-response { color: var(--status-success-color); + &.outlined { + border: 1px solid var(--status-success-color); + } } .redirect-response { color: var(--status-redirect-color); + &.outlined { + border: 1px solid var(--status-redirect-color); + } } .critical-error-response { color: var(--status-critical-error-color); + &.outlined { + border: 1px solid var(--status-critical-error-color); + } } .server-error-response { color: var(--status-server-error-color); + &.outlined { + border: 1px solid var(--status-server-error-color); + } } .missing-data-response { color: var(--status-missing-data-color); + &.outlined { + border: 1px solid var(--status-missing-data-color); + } } .toasted-container { diff --git a/packages/hoppscotch-common/assets/themes/base-themes.scss b/packages/hoppscotch-common/assets/themes/base-themes.scss index 9bc0a4f3..b5efbcec 100644 --- a/packages/hoppscotch-common/assets/themes/base-themes.scss +++ b/packages/hoppscotch-common/assets/themes/base-themes.scss @@ -5,7 +5,9 @@ --font-size-tiny: 0.625rem; --line-height-body: 1rem; --upper-primary-sticky-fold: 4.125rem; + --upper-runner-sticky-fold: 4.125rem; --upper-secondary-sticky-fold: 6.188rem; + --upper-runner-sticky-fold: 4.5rem; --upper-tertiary-sticky-fold: 8.25rem; --upper-fourth-sticky-fold: 10.2rem; --upper-mobile-primary-sticky-fold: 6.75rem; diff --git a/packages/hoppscotch-common/locales/en.json b/packages/hoppscotch-common/locales/en.json index 772b7a21..cce7e974 100644 --- a/packages/hoppscotch-common/locales/en.json +++ b/packages/hoppscotch-common/locales/en.json @@ -231,6 +231,8 @@ } }, "collection": { + "title": "Collection", + "run": "Run Collection", "created": "Collection created", "different_parent": "Cannot reorder collection with different parent", "edit": "Edit Collection", @@ -341,6 +343,7 @@ "response": "No response received" }, "environment": { + "heading": "Environment", "add_to_global": "Add to Global", "added": "Environment addition", "create_new": "Create new environment", @@ -448,6 +451,7 @@ "invalid_name": "Please provide a name for the folder", "name_length_insufficient": "Folder name should be at least 3 characters long", "new": "New Folder", + "run": "Run Folder", "renamed": "Folder renamed" }, "graphql": { @@ -1069,7 +1073,10 @@ "tests": "Tests", "types": "Types", "variables": "Variables", - "websocket": "WebSocket" + "websocket": "WebSocket", + "all_tests": "All Tests", + "passed": "Passed", + "failed": "Failed" }, "team": { "already_member": "This email is associated with an existing user.", @@ -1137,6 +1144,8 @@ "not_found": "Environment not found." }, "test": { + "requests": "Requests", + "selection": "Selection", "failed": "test failed", "javascript_code": "JavaScript Code", "learn": "Read documentation", @@ -1144,7 +1153,14 @@ "report": "Test Report", "results": "Test Results", "script": "Script", - "snippets": "Snippets" + "snippets": "Snippets", + "run": "Run", + "run_again": "Run again", + "stop": "Stop", + "new_run": "New Run", + "iterations": "Iterations", + "duration": "Duration", + "avg_resp": "Avg. Response Time" }, "websocket": { "communication": "Communication", @@ -1194,7 +1210,17 @@ "cli_environment_id_description": "This environment ID will be used by the CLI collection runner for Hoppscotch.", "include_active_environment": "Include active environment:", "cli": "CLI", - "ui": "Runner (coming soon)", + "delay": "Delay", + "ui": "Runner", + "running_collection": "Running collection", + "run_config": "Run Configuration", + "advanced_settings": "Advanced Settings", + "stop_on_error": "Stop run if an error occurs", + "persist_responses": "Persist responses", + "collection_not_found": "Collection not found. May be deleted or moved.", + "empty_collection": "Collection is empty. Add requests to run.", + "no_response_persist": "The collection runner is presently configured not to persist responses. This setting prevents showing the response data. To modify this behavior, initiate a new run configuration.", + "select_request": "Select a request to see response and test results", "cli_command_generation_description_cloud": "Copy the below command and run it from the CLI. Please specify a personal access token.", "cli_command_generation_description_sh": "Copy the below command and run it from the CLI. Please specify a personal access token and verify the generated SH instance server URL.", "cli_command_generation_description_sh_with_server_url_placeholder": "Copy the below command and run it from the CLI. Please specify a personal access token and the SH instance server URL.", diff --git a/packages/hoppscotch-common/package.json b/packages/hoppscotch-common/package.json index 51e3be7c..b1454e64 100644 --- a/packages/hoppscotch-common/package.json +++ b/packages/hoppscotch-common/package.json @@ -38,7 +38,7 @@ "@hoppscotch/data": "workspace:^", "@hoppscotch/httpsnippet": "3.0.6", "@hoppscotch/js-sandbox": "workspace:^", - "@hoppscotch/ui": "0.2.1", + "@hoppscotch/ui": "0.2.2", "@hoppscotch/vue-toasted": "0.1.0", "@lezer/highlight": "1.2.0", "@noble/curves": "1.6.0", diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index a7779547..8f59d99c 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -7,6 +7,9 @@ export {} /* prettier-ignore */ declare module 'vue' { export interface GlobalComponents { + '(chore': fix broken runner for user collection) + '(feat': collection runner config in modal) + '(fix': run again function) AccessTokens: typeof import('./components/accessTokens/index.vue')['default'] AccessTokensGenerateModal: typeof import('./components/accessTokens/GenerateModal.vue')['default'] AccessTokensList: typeof import('./components/accessTokens/List.vue')['default'] @@ -14,6 +17,7 @@ declare module 'vue' { AiexperimentsMergeView: typeof import('./components/aiexperiments/MergeView.vue')['default'] AiexperimentsModifyBodyModal: typeof import('./components/aiexperiments/ModifyBodyModal.vue')['default'] AppActionHandler: typeof import('./components/app/ActionHandler.vue')['default'] + AppAnnouncement: (typeof import("./components/app/Announcement.vue"))["default"] AppBanner: typeof import('./components/app/Banner.vue')['default'] AppContextMenu: typeof import('./components/app/ContextMenu.vue')['default'] AppDeveloperOptions: typeof import('./components/app/DeveloperOptions.vue')['default'] @@ -41,6 +45,8 @@ declare module 'vue' { AppSpotlightSearch: typeof import('./components/app/SpotlightSearch.vue')['default'] AppSupport: typeof import('./components/app/Support.vue')['default'] AppWhatsNewDialog: typeof import('./components/app/WhatsNewDialog.vue')['default'] + ButtonPrimary: (typeof import("./../../hoppscotch-ui/src/components/button/Primary.vue"))["default"] + ButtonSecondary: (typeof import("./../../hoppscotch-ui/src/components/button/Secondary.vue"))["default"] Collections: typeof import('./components/collections/index.vue')['default'] CollectionsAdd: typeof import('./components/collections/Add.vue')['default'] CollectionsAddFolder: typeof import('./components/collections/AddFolder.vue')['default'] @@ -66,7 +72,7 @@ declare module 'vue' { CollectionsMyCollections: typeof import('./components/collections/MyCollections.vue')['default'] CollectionsProperties: typeof import('./components/collections/Properties.vue')['default'] CollectionsRequest: typeof import('./components/collections/Request.vue')['default'] - CollectionsRunner: typeof import('./components/collections/Runner.vue')['default'] + CollectionsRunner: (typeof import("./components/collections/Runner.vue"))["default"] CollectionsSaveRequest: typeof import('./components/collections/SaveRequest.vue')['default'] CollectionsTeamCollections: typeof import('./components/collections/TeamCollections.vue')['default'] CookiesAllModal: typeof import('./components/cookies/AllModal.vue')['default'] @@ -107,8 +113,10 @@ declare module 'vue' { HoppButtonPrimary: typeof import('@hoppscotch/ui')['HoppButtonPrimary'] HoppButtonSecondary: typeof import('@hoppscotch/ui')['HoppButtonSecondary'] HoppSmartAnchor: typeof import('@hoppscotch/ui')['HoppSmartAnchor'] + HoppSmartAutoComplete: (typeof import("@hoppscotch/ui"))["HoppSmartAutoComplete"] HoppSmartCheckbox: typeof import('@hoppscotch/ui')['HoppSmartCheckbox'] HoppSmartConfirmModal: typeof import('@hoppscotch/ui')['HoppSmartConfirmModal'] + HoppSmartExpand: (typeof import("@hoppscotch/ui"))["HoppSmartExpand"] HoppSmartFileChip: typeof import('@hoppscotch/ui')['HoppSmartFileChip'] HoppSmartInput: typeof import('@hoppscotch/ui')['HoppSmartInput'] HoppSmartIntersection: typeof import('@hoppscotch/ui')['HoppSmartIntersection'] @@ -129,6 +137,8 @@ declare module 'vue' { HoppSmartTree: typeof import('@hoppscotch/ui')['HoppSmartTree'] HoppSmartWindow: typeof import('@hoppscotch/ui')['HoppSmartWindow'] HoppSmartWindows: typeof import('@hoppscotch/ui')['HoppSmartWindows'] + HoppTestEnv: (typeof import("@hoppscotch/ui"))["HoppTestEnv"] + HoppTestRunnerModal: (typeof import("@hoppscotch/ui"))["HoppTestRunnerModal"] HttpAuthorization: typeof import('./components/http/Authorization.vue')['default'] HttpAuthorizationAkamaiEG: typeof import('./components/http/authorization/AkamaiEG.vue')['default'] HttpAuthorizationApiKey: typeof import('./components/http/authorization/ApiKey.vue')['default'] @@ -143,6 +153,7 @@ declare module 'vue' { HttpBodyParameters: typeof import('./components/http/BodyParameters.vue')['default'] HttpCodegen: typeof import('./components/http/Codegen.vue')['default'] HttpCodegenModal: typeof import('./components/http/CodegenModal.vue')['default'] + HttpCollectionRunner: (typeof import("./components/http/CollectionRunner.vue"))["default"] HttpExampleLenseBodyRenderer: typeof import('./components/http/example/LenseBodyRenderer.vue')['default'] HttpExampleResponse: typeof import('./components/http/example/Response.vue')['default'] HttpExampleResponseMeta: typeof import('./components/http/example/ResponseMeta.vue')['default'] @@ -151,6 +162,7 @@ declare module 'vue' { HttpHeaders: typeof import('./components/http/Headers.vue')['default'] HttpImportCurl: typeof import('./components/http/ImportCurl.vue')['default'] HttpKeyValue: typeof import('./components/http/KeyValue.vue')['default'] + HttpOAuth2Authorization: (typeof import("./components/http/OAuth2Authorization.vue"))["default"] HttpParameters: typeof import('./components/http/Parameters.vue')['default'] HttpPreRequestScript: typeof import('./components/http/PreRequestScript.vue')['default'] HttpRawBody: typeof import('./components/http/RawBody.vue')['default'] @@ -162,19 +174,36 @@ declare module 'vue' { HttpResponse: typeof import('./components/http/Response.vue')['default'] HttpResponseInterface: typeof import('./components/http/ResponseInterface.vue')['default'] HttpResponseMeta: typeof import('./components/http/ResponseMeta.vue')['default'] + HttpRunner: (typeof import("./components/http/Runner.vue"))["default"] HttpSaveResponseName: typeof import('./components/http/SaveResponseName.vue')['default'] HttpSidebar: typeof import('./components/http/Sidebar.vue')['default'] HttpTabHead: typeof import('./components/http/TabHead.vue')['default'] + HttpTestEnv: typeof import('./components/http/test/Env.vue')['default'] + HttpTestFolder: typeof import('./components/http/test/Folder.vue')['default'] + HttpTestRequest: typeof import('./components/http/test/Request.vue')['default'] + HttpTestResponse: typeof import('./components/http/test/Response.vue')['default'] HttpTestResult: typeof import('./components/http/TestResult.vue')['default'] HttpTestResultEntry: typeof import('./components/http/TestResultEntry.vue')['default'] HttpTestResultEnv: typeof import('./components/http/TestResultEnv.vue')['default'] + HttpTestResultFolder: typeof import('./components/http/test/ResultFolder.vue')['default'] HttpTestResultReport: typeof import('./components/http/TestResultReport.vue')['default'] + HttpTestResultRequest: typeof import('./components/http/test/ResultRequest.vue')['default'] + HttpTestRunner: typeof import('./components/http/test/Runner.vue')['default'] + HttpTestRunnerConfig: typeof import('./components/http/test/RunnerConfig.vue')['default'] + HttpTestRunnerMeta: typeof import('./components/http/test/RunnerMeta.vue')['default'] + HttpTestRunnerModal: typeof import('./components/http/test/RunnerModal.vue')['default'] + HttpTestRunnerResult: typeof import('./components/http/test/RunnerResult.vue')['default'] HttpTests: typeof import('./components/http/Tests.vue')['default'] + HttpTestSelector: (typeof import("./components/http/test/Selector.vue"))["default"] + HttpTestSelectRequest: (typeof import("./components/http/test/SelectRequest.vue"))["default"] + HttpTestTestResult: typeof import('./components/http/test/TestResult.vue')['default'] HttpURLEncodedParams: typeof import('./components/http/URLEncodedParams.vue')['default'] IconLucideActivity: typeof import('~icons/lucide/activity')['default'] + IconLucideAlertCircle: (typeof import("~icons/lucide/alert-circle"))["default"] IconLucideAlertTriangle: typeof import('~icons/lucide/alert-triangle')['default'] IconLucideArrowLeft: typeof import('~icons/lucide/arrow-left')['default'] IconLucideArrowUpRight: typeof import('~icons/lucide/arrow-up-right')['default'] + IconLucideBrush: (typeof import("~icons/lucide/brush"))["default"] IconLucideCheckCircle: typeof import('~icons/lucide/check-circle')['default'] IconLucideChevronRight: typeof import('~icons/lucide/chevron-right')['default'] IconLucideGlobe: typeof import('~icons/lucide/globe')['default'] @@ -184,10 +213,12 @@ declare module 'vue' { IconLucideLayers: typeof import('~icons/lucide/layers')['default'] IconLucideListEnd: typeof import('~icons/lucide/list-end')['default'] IconLucideMinus: typeof import('~icons/lucide/minus')['default'] - IconLucideRss: typeof import('~icons/lucide/rss')['default'] + IconLucidePlay: (typeof import("~icons/lucide/play"))["default"] + IconLucidePlaySquare: (typeof import("~icons/lucide/play-square"))["default"] + IconLucideRss: (typeof import("~icons/lucide/rss"))["default"] IconLucideSearch: typeof import('~icons/lucide/search')['default'] IconLucideUsers: typeof import('~icons/lucide/users')['default'] - IconLucideVerified: typeof import('~icons/lucide/verified')['default'] + 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'] @@ -214,6 +245,8 @@ declare module 'vue' { LensesRenderersVideoLensRenderer: typeof import('./components/lenses/renderers/VideoLensRenderer.vue')['default'] LensesRenderersXMLLensRenderer: typeof import('./components/lenses/renderers/XMLLensRenderer.vue')['default'] LensesResponseBodyRenderer: typeof import('./components/lenses/ResponseBodyRenderer.vue')['default'] + ProfileShortcode: (typeof import("./components/profile/Shortcode.vue"))["default"] + ProfileShortcodes: (typeof import("./components/profile/Shortcodes.vue"))["default"] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default'] @@ -228,14 +261,43 @@ declare module 'vue' { ShareCustomizeModal: typeof import('./components/share/CustomizeModal.vue')['default'] ShareModal: typeof import('./components/share/Modal.vue')['default'] ShareRequest: typeof import('./components/share/Request.vue')['default'] + ShareRequestModal: (typeof import("./components/share/RequestModal.vue"))["default"] + ShareShareRequestModal: (typeof import("./components/share/ShareRequestModal.vue"))["default"] ShareTemplatesButton: typeof import('./components/share/templates/Button.vue')['default'] ShareTemplatesEmbeds: typeof import('./components/share/templates/Embeds.vue')['default'] ShareTemplatesLink: typeof import('./components/share/templates/Link.vue')['default'] SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default'] + SmartAnchor: (typeof import("./../../hoppscotch-ui/src/components/smart/Anchor.vue"))["default"] + SmartAutoComplete: (typeof import("./../../hoppscotch-ui/src/components/smart/AutoComplete.vue"))["default"] SmartChangeLanguage: typeof import('./components/smart/ChangeLanguage.vue')['default'] + SmartCheckbox: (typeof import("./../../hoppscotch-ui/src/components/smart/Checkbox.vue"))["default"] SmartColorModePicker: typeof import('./components/smart/ColorModePicker.vue')['default'] + SmartConfirmModal: (typeof import("./../../hoppscotch-ui/src/components/smart/ConfirmModal.vue"))["default"] SmartEncodingPicker: typeof import('./components/smart/EncodingPicker.vue')['default'] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default'] + SmartExpand: (typeof import("./../../hoppscotch-ui/src/components/smart/Expand.vue"))["default"] + SmartFileChip: (typeof import("./../../hoppscotch-ui/src/components/smart/FileChip.vue"))["default"] + SmartInput: (typeof import("./../../hoppscotch-ui/src/components/smart/Input.vue"))["default"] + SmartIntersection: (typeof import("./../../hoppscotch-ui/src/components/smart/Intersection.vue"))["default"] + SmartItem: (typeof import("./../../hoppscotch-ui/src/components/smart/Item.vue"))["default"] + SmartLink: (typeof import("./../../hoppscotch-ui/src/components/smart/Link.vue"))["default"] + SmartModal: (typeof import("./../../hoppscotch-ui/src/components/smart/Modal.vue"))["default"] + SmartPicture: (typeof import("./../../hoppscotch-ui/src/components/smart/Picture.vue"))["default"] + SmartPlaceholder: (typeof import("./../../hoppscotch-ui/src/components/smart/Placeholder.vue"))["default"] + SmartProgressRing: (typeof import("./../../hoppscotch-ui/src/components/smart/ProgressRing.vue"))["default"] + SmartRadio: (typeof import("./../../hoppscotch-ui/src/components/smart/Radio.vue"))["default"] + SmartRadioGroup: (typeof import("./../../hoppscotch-ui/src/components/smart/RadioGroup.vue"))["default"] + SmartSelectWrapper: (typeof import("./../../hoppscotch-ui/src/components/smart/SelectWrapper.vue"))["default"] + SmartSlideOver: (typeof import("./../../hoppscotch-ui/src/components/smart/SlideOver.vue"))["default"] + SmartSpinner: (typeof import("./../../hoppscotch-ui/src/components/smart/Spinner.vue"))["default"] + SmartTab: (typeof import("./../../hoppscotch-ui/src/components/smart/Tab.vue"))["default"] + SmartTable: (typeof import("./../../hoppscotch-ui/src/components/smart/Table.vue"))["default"] + SmartTabs: (typeof import("./../../hoppscotch-ui/src/components/smart/Tabs.vue"))["default"] + SmartToggle: (typeof import("./../../hoppscotch-ui/src/components/smart/Toggle.vue"))["default"] + SmartTree: (typeof import("./../../hoppscotch-ui/src/components/smart/Tree.vue"))["default"] + SmartTreeBranch: (typeof import("./../../hoppscotch-ui/src/components/smart/TreeBranch.vue"))["default"] + SmartWindow: (typeof import("./../../hoppscotch-ui/src/components/smart/Window.vue"))["default"] + SmartWindows: (typeof import("./../../hoppscotch-ui/src/components/smart/Windows.vue"))["default"] TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabSecondary: typeof import('./components/tab/Secondary.vue')['default'] Teams: typeof import('./components/teams/index.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/collections/AddRequest.vue b/packages/hoppscotch-common/src/components/collections/AddRequest.vue index 26eb32a0..1b4f1987 100644 --- a/packages/hoppscotch-common/src/components/collections/AddRequest.vue +++ b/packages/hoppscotch-common/src/components/collections/AddRequest.vue @@ -157,10 +157,8 @@ watch( () => props.show, (show) => { if (show) { - if (tabs.currentActiveTab.value.document.type === "example-response") - return - - editingName.value = tabs.currentActiveTab.value.document.request.name + if (tabs.currentActiveTab.value.document.type === "request") + editingName.value = tabs.currentActiveTab.value.document.request.name } } ) diff --git a/packages/hoppscotch-common/src/components/collections/Collection.vue b/packages/hoppscotch-common/src/components/collections/Collection.vue index 4fb467ab..3de4eb42 100644 --- a/packages/hoppscotch-common/src/components/collections/Collection.vue +++ b/packages/hoppscotch-common/src/components/collections/Collection.vue @@ -74,7 +74,6 @@ @click="emit('add-folder')" /> + { + emit('run-collection', props.id) + hide() + } + " + /> - { - emit('run-collection', props.id) - hide() - } - " - /> @@ -298,6 +296,7 @@ const emit = defineEmits<{ (event: "toggle-children"): void (event: "add-request"): void (event: "add-folder"): void + (event: "run-collection"): void (event: "edit-collection"): void (event: "edit-properties"): void (event: "duplicate-collection"): void diff --git a/packages/hoppscotch-common/src/components/collections/EditRequest.vue b/packages/hoppscotch-common/src/components/collections/EditRequest.vue index 7ab69bf2..ed45e7ad 100644 --- a/packages/hoppscotch-common/src/components/collections/EditRequest.vue +++ b/packages/hoppscotch-common/src/components/collections/EditRequest.vue @@ -114,7 +114,7 @@ const t = useI18n() const props = withDefaults( defineProps<{ show: boolean - loadingState: boolean + loadingState?: boolean modelValue?: string requestContext: HoppRESTRequest | null }>(), diff --git a/packages/hoppscotch-common/src/components/collections/MyCollections.vue b/packages/hoppscotch-common/src/components/collections/MyCollections.vue index fcf1706d..d9072954 100644 --- a/packages/hoppscotch-common/src/components/collections/MyCollections.vue +++ b/packages/hoppscotch-common/src/components/collections/MyCollections.vue @@ -64,6 +64,12 @@ folder: node.data.data.data, }) " + @run-collection=" + emit('run-collection', { + collectionIndex: node.id, + collection: node.data.data.data, + }) + " @edit-collection=" node.data.type === 'collections' && emit('edit-collection', { @@ -133,6 +139,12 @@ }) " folder-type="folder" + @run-collection=" + emit('run-collection', { + collectionIndex: node.id, + collection: node.data.data.data, + }) + " @add-request=" node.data.type === 'folders' && emit('add-request', { @@ -493,6 +505,13 @@ const emit = defineEmits<{ folder: HoppCollection } ): void + ( + event: "run-collection", + payload: { + collectionIndex: string + collection: HoppCollection + } + ): void ( event: "edit-collection", payload: { diff --git a/packages/hoppscotch-common/src/components/collections/Runner.vue b/packages/hoppscotch-common/src/components/collections/Runner.vue deleted file mode 100644 index 5ffcdffc..00000000 --- a/packages/hoppscotch-common/src/components/collections/Runner.vue +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - {{ cliCommandGenerationDescription }} - - - - - {{ t("collection_runner.include_active_environment") }} - {{ - activeEnvironment - }} - - - - {{ generatedCLICommand }} - - - - - - - - - - - - - - - - - - diff --git a/packages/hoppscotch-common/src/components/collections/index.vue b/packages/hoppscotch-common/src/components/collections/index.vue index 2363feef..65ad9976 100644 --- a/packages/hoppscotch-common/src/components/collections/index.vue +++ b/packages/hoppscotch-common/src/components/collections/index.vue @@ -33,6 +33,13 @@ :filter-text="filterTexts" :save-request="saveRequest" :picked="picked" + @run-collection=" + runCollectionHandler({ + type: 'my-collections', + collectionID: $event.collection._ref_id, + collectionIndex: $event.collectionIndex, + }) + " @add-folder="addFolder" @add-request="addRequest" @edit-request="editRequest" @@ -99,7 +106,12 @@ @remove-folder="removeFolder" @remove-request="removeRequest" @remove-response="removeResponse" - @run-collection="runCollectionHandler" + @run-collection=" + runCollectionHandler({ + type: 'team-collections', + collectionID: $event, + }) + " @share-request="shareRequest" @select-request="selectRequest" @select-response="selectResponse" @@ -193,11 +205,9 @@ /> - @@ -207,6 +217,7 @@ import { useI18n } from "@composables/i18n" import { useToast } from "@composables/toast" import { + getDefaultRESTRequest, HoppCollection, HoppRESTAuth, HoppRESTHeaders, @@ -218,7 +229,7 @@ import * as TE from "fp-ts/TaskEither" import { pipe } from "fp-ts/function" import { cloneDeep, debounce, isEqual } from "lodash-es" import { PropType, computed, nextTick, onMounted, ref, watch } from "vue" -import { useReadonlyStream, useStream } from "~/composables/stream" +import { useReadonlyStream } from "~/composables/stream" import { defineActionHandler, invokeAction } from "~/helpers/actions" import { GQLError } from "~/helpers/backend/GQLClient" import { @@ -278,10 +289,7 @@ import { updateRESTCollectionOrder, updateRESTRequestOrder, } from "~/newstore/collections" -import { - selectedEnvironmentIndex$, - setSelectedEnvironmentIndex, -} from "~/newstore/environments" + import { useLocalState } from "~/newstore/localstate" import { currentReorderingStatus$ } from "~/newstore/reordering" import { platform } from "~/platform" @@ -292,7 +300,7 @@ import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service" import { RESTOptionTabs } from "../http/RequestOptions.vue" import { Collection as NodeCollection } from "./MyCollections.vue" import { EditingProperties } from "./Properties.vue" -import { getDefaultRESTRequest } from "~/helpers/rest/default" +import { CollectionRunnerData } from "../http/test/RunnerModal.vue" const t = useI18n() const toast = useToast() @@ -383,15 +391,6 @@ const teamLoadingCollections = useReadonlyStream( [] ) const teamEnvironmentAdapter = new TeamEnvironmentAdapter(undefined) -const teamEnvironmentList = useReadonlyStream( - teamEnvironmentAdapter.teamEnvironmentList$, - [] -) -const selectedEnvironmentIndex = useStream( - selectedEnvironmentIndex$, - { type: "NO_ENV_SELECTED" }, - setSelectedEnvironmentIndex -) const { cascadeParentCollectionForHeaderAuthForSearchResults, @@ -692,8 +691,7 @@ const showConfirmModal = ref(false) const showTeamModalAdd = ref(false) const showCollectionsRunnerModal = ref(false) -const selectedCollectionID = ref(null) -const activeEnvironmentID = ref(null) +const collectionRunnerData = ref(null) const displayModalAdd = (show: boolean) => { showModalAdd.value = show @@ -837,7 +835,9 @@ const onAddRequest = (requestName: string) => { if (!request) return const newRequest = { - ...cloneDeep(request), + ...(tabs.currentActiveTab.value.document.type === "request" + ? cloneDeep(tabs.currentActiveTab.value.document.request) + : getDefaultRESTRequest()), name: requestName, } @@ -849,9 +849,9 @@ const onAddRequest = (requestName: string) => { const { auth, headers } = cascadeParentCollectionForHeaderAuth(path, "rest") tabs.createNewTab({ + type: "request", request: newRequest, isDirty: false, - type: "request", saveContext: { originLocation: "user-collection", folderPath: path, @@ -904,9 +904,9 @@ const onAddRequest = (requestName: string) => { const { auth, headers } = teamCollectionAdapter.cascadeParentCollectionForHeaderAuth(path) tabs.createNewTab({ + type: "request", request: newRequest, isDirty: false, - type: "request", saveContext: { originLocation: "team-collection", requestID: createRequestInCollection.id, @@ -1973,13 +1973,13 @@ const selectRequest = (selectedRequest: { requestID: requestIndex, }) - if (possibleTab) { + if (possibleTab && possibleTab.value.document.type === "request") { tabs.setActiveTab(possibleTab.value.id) } else { tabs.createNewTab({ + type: "request", request: cloneDeep(request), isDirty: false, - type: "request", saveContext: { originLocation: "team-collection", requestID: requestIndex, @@ -2004,9 +2004,9 @@ const selectRequest = (selectedRequest: { } else { // If not, open the request in a new tab tabs.createNewTab({ + type: "request", request: cloneDeep(request), isDirty: false, - type: "request", saveContext: { originLocation: "user-collection", folderPath: folderPath!, @@ -2866,25 +2866,9 @@ const setCollectionProperties = (newCollection: { displayModalEditProperties(false) } -const runCollectionHandler = (collectionID: string) => { - selectedCollectionID.value = collectionID +const runCollectionHandler = (payload: CollectionRunnerData) => { + collectionRunnerData.value = payload showCollectionsRunnerModal.value = true - - const activeWorkspace = workspace.value - const currentEnv = selectedEnvironmentIndex.value - - if (["NO_ENV_SELECTED", "MY_ENV"].includes(currentEnv.type)) { - activeEnvironmentID.value = null - return - } - - if (activeWorkspace.type === "team" && currentEnv.type === "TEAM_ENV") { - activeEnvironmentID.value = teamEnvironmentList.value.find( - (env) => - env.teamID === activeWorkspace.teamID && - env.environment.id === currentEnv.environment.id - )?.environment.id - } } const resolveConfirmModal = (title: string | null) => { diff --git a/packages/hoppscotch-common/src/components/history/index.vue b/packages/hoppscotch-common/src/components/history/index.vue index 04f82a0a..750994a4 100644 --- a/packages/hoppscotch-common/src/components/history/index.vue +++ b/packages/hoppscotch-common/src/components/history/index.vue @@ -298,9 +298,9 @@ const clearHistory = () => { const tabs = useService(RESTTabService) const useHistory = (entry: RESTHistoryEntry) => { tabs.createNewTab({ + type: "request", request: entry.request, isDirty: false, - type: "request", }) } diff --git a/packages/hoppscotch-common/src/components/http/TestResult.vue b/packages/hoppscotch-common/src/components/http/TestResult.vue index ca15e5ba..84da5ba0 100644 --- a/packages/hoppscotch-common/src/components/http/TestResult.vue +++ b/packages/hoppscotch-common/src/components/http/TestResult.vue @@ -135,9 +135,7 @@ :key="`result-${index}`" class="flex items-center px-4 py-2" > - + () +const props = withDefaults( + defineProps<{ + modelValue: HoppTestResult | null | undefined + showEmptyMessage?: boolean + }>(), + { + showEmptyMessage: true, + } +) const emit = defineEmits<{ (e: "update:modelValue", val: HoppTestResult | null | undefined): void diff --git a/packages/hoppscotch-common/src/components/http/TestResultEntry.vue b/packages/hoppscotch-common/src/components/http/TestResultEntry.vue index 4464b9ad..1367e608 100644 --- a/packages/hoppscotch-common/src/components/http/TestResultEntry.vue +++ b/packages/hoppscotch-common/src/components/http/TestResultEntry.vue @@ -8,57 +8,87 @@ - - - - - {{ result.message }} - - - - {{ - result.status === "pass" ? t("test.passed") : t("test.failed") - }} - + + + + + {{ result.message }} + + + + {{ + result.status === "pass" ? t("test.passed") : t("test.failed") + }} + + - + diff --git a/packages/hoppscotch-common/src/components/http/TestResultEnv.vue b/packages/hoppscotch-common/src/components/http/TestResultEnv.vue index fc942d0f..e34de824 100644 --- a/packages/hoppscotch-common/src/components/http/TestResultEnv.vue +++ b/packages/hoppscotch-common/src/components/http/TestResultEnv.vue @@ -1,6 +1,6 @@ - + - + {{ env.key }} @@ -51,7 +49,7 @@ type Props = { previousValue?: string } status: Status - global: boolean + global?: boolean } withDefaults(defineProps(), { diff --git a/packages/hoppscotch-common/src/components/http/test/Env.vue b/packages/hoppscotch-common/src/components/http/test/Env.vue new file mode 100644 index 00000000..2d1eb808 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/Env.vue @@ -0,0 +1,104 @@ + + + {{ envName ?? t("filter.none") }} + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/Folder.vue b/packages/hoppscotch-common/src/components/http/test/Folder.vue new file mode 100644 index 00000000..b942d1f6 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/Folder.vue @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + {{ collectionName }} + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/Request.vue b/packages/hoppscotch-common/src/components/http/test/Request.vue new file mode 100644 index 00000000..2e51206c --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/Request.vue @@ -0,0 +1,86 @@ + + + + + + + + + {{ request.method }} + + + + + {{ request.name }} + + + + + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/Response.vue b/packages/hoppscotch-common/src/components/http/test/Response.vue new file mode 100644 index 00000000..f561aaa1 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/Response.vue @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/ResultFolder.vue b/packages/hoppscotch-common/src/components/http/test/ResultFolder.vue new file mode 100644 index 00000000..180f62ba --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/ResultFolder.vue @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + {{ collectionName }} + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/ResultRequest.vue b/packages/hoppscotch-common/src/components/http/test/ResultRequest.vue new file mode 100644 index 00000000..7c857361 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/ResultRequest.vue @@ -0,0 +1,113 @@ + + + + + + + {{ request.method }} + + + + {{ request.name }} + + + {{ `${request.response?.statusCode}` }} + + + + + + + + {{ request.endpoint }} + + + + + {{ request.error }} + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/Runner.vue b/packages/hoppscotch-common/src/components/http/test/Runner.vue new file mode 100644 index 00000000..1499ca3d --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/Runner.vue @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ t("collection_runner.running_collection") }}... + + + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/RunnerMeta.vue b/packages/hoppscotch-common/src/components/http/test/RunnerMeta.vue new file mode 100644 index 00000000..90f56064 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/RunnerMeta.vue @@ -0,0 +1,21 @@ + + + + {{ heading }} + + + + {{ text }} + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/RunnerModal.vue b/packages/hoppscotch-common/src/components/http/test/RunnerModal.vue new file mode 100644 index 00000000..a1f37a39 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/RunnerModal.vue @@ -0,0 +1,370 @@ + + + + + + + + + {{ t("collection_runner.run_config") }} + + + + + + + ms + + + + + + + + + {{ t("collection_runner.advanced_settings") }} + + + + + + {{ t("collection_runner.stop_on_error") }} + + + + + + {{ t("collection_runner.persist_responses") }} + + + + + + + + + + + + + + + {{ cliCommandGenerationDescription }} + + + + + {{ t("collection_runner.include_active_environment") }} + + {{ currentEnv?.name }} + + + + + + {{ CLICommand }} + + + + + + + + + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/RunnerResult.vue b/packages/hoppscotch-common/src/components/http/test/RunnerResult.vue new file mode 100644 index 00000000..9bbc8e39 --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/RunnerResult.vue @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/http/test/TestResult.vue b/packages/hoppscotch-common/src/components/http/test/TestResult.vue new file mode 100644 index 00000000..5b400c4f --- /dev/null +++ b/packages/hoppscotch-common/src/components/http/test/TestResult.vue @@ -0,0 +1,303 @@ + + + + + + + + + + + {{ t("environment.title") }} + + + + + + + + + {{ t("environment.no_environment_description") }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ result.message }} + + + + {{ + result.status === "pass" + ? t("test.passed") + : t("test.failed") + }} + + + + + + + + + + + + {{ t("empty.tests") }} + + + + + + + + diff --git a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue index 061cf897..844bd973 100644 --- a/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue +++ b/packages/hoppscotch-common/src/components/lenses/ResponseBodyRenderer.vue @@ -50,9 +50,10 @@ import { import { useI18n } from "@composables/i18n" import { useVModel } from "@vueuse/core" import { HoppRequestDocument } from "~/helpers/rest/document" +import { TestRunnerRequest } from "~/services/test-runner/test-runner.service" const props = defineProps<{ - document: HoppRequestDocument + document: HoppRequestDocument | TestRunnerRequest isEditable: boolean }>() @@ -64,6 +65,7 @@ const emit = defineEmits<{ const doc = useVModel(props, "document", emit) const isSavable = computed(() => { + if (doc.value.type === "test-response") return false return doc.value.response?.type === "success" && doc.value.saveContext }) @@ -118,6 +120,8 @@ watch( "results", ] + if (doc.value.type === "test-response") return + const { responseTabPreference } = doc.value if ( @@ -133,6 +137,7 @@ watch( ) watch(selectedLensTab, (newLensID) => { + if (doc.value.type === "test-response") return doc.value.responseTabPreference = newLensID }) diff --git a/packages/hoppscotch-common/src/components/smart/EnvInput.vue b/packages/hoppscotch-common/src/components/smart/EnvInput.vue index e8f0ea4b..32a720a7 100644 --- a/packages/hoppscotch-common/src/components/smart/EnvInput.vue +++ b/packages/hoppscotch-common/src/components/smart/EnvInput.vue @@ -381,7 +381,9 @@ const envVars = computed(() => { tabs.currentActiveTab.value.document.type === "example-response" ? tabs.currentActiveTab.value.document.response.originalRequest .requestVariables - : tabs.currentActiveTab.value.document.request.requestVariables + : tabs.currentActiveTab.value.document.type === "request" + ? tabs.currentActiveTab.value.document.request.requestVariables + : [] return [ ...requestVariables.map(({ active, key, value }) => diff --git a/packages/hoppscotch-common/src/helpers/RequestRunner.ts b/packages/hoppscotch-common/src/helpers/RequestRunner.ts index 66551ac7..a97ae5ab 100644 --- a/packages/hoppscotch-common/src/helpers/RequestRunner.ts +++ b/packages/hoppscotch-common/src/helpers/RequestRunner.ts @@ -1,6 +1,7 @@ import { Environment, HoppRESTHeaders, + HoppRESTRequest, HoppRESTRequestVariable, } from "@hoppscotch/data" import { SandboxTestResult, TestDescriptor } from "@hoppscotch/js-sandbox" @@ -14,6 +15,7 @@ import { Observable, Subject } from "rxjs" import { filter } from "rxjs/operators" import { Ref } from "vue" +import { getService } from "~/modules/dioc" import { environmentsStore, getCurrentEnvironment, @@ -22,6 +24,10 @@ import { setGlobalEnvVariables, updateEnvironment, } from "~/newstore/environments" +import { + SecretEnvironmentService, + SecretVariable, +} from "~/services/secret-environment.service" import { HoppTab } from "~/services/tab" import { updateTeamEnvironment } from "./backend/mutations/TeamEnvironment" import { createRESTNetworkRequestStream } from "./network" @@ -34,15 +40,10 @@ import { HoppRESTResponse } from "./types/HoppRESTResponse" import { HoppTestData, HoppTestResult } from "./types/HoppTestResult" import { getEffectiveRESTRequest } from "./utils/EffectiveURL" import { isJSONContentType } from "./utils/contenttypes" -import { - SecretEnvironmentService, - SecretVariable, -} from "~/services/secret-environment.service" -import { getService } from "~/modules/dioc" const secretEnvironmentService = getService(SecretEnvironmentService) -const getTestableBody = ( +export const getTestableBody = ( res: HoppRESTResponse & { type: "success" | "fail" } ) => { const contentTypeHeader = res.headers.find( @@ -69,7 +70,7 @@ const getTestableBody = ( return x } -const combineEnvVariables = (variables: { +export const combineEnvVariables = (variables: { environments: { selected: Environment["variables"] global: Environment["variables"] @@ -279,70 +280,12 @@ export function runRESTRequest$( ) if (E.isRight(runResult)) { - const updatedGlobalEnvVariables = updateEnvironmentsWithSecret( - cloneDeep(runResult.right.envs.global), - "global" - ) - - const updatedSelectedEnvVariables = updateEnvironmentsWithSecret( - cloneDeep(runResult.right.envs.selected), - "selected" - ) - // set the response in the tab so that multiple tabs can run request simultaneously tab.value.document.response = res - - const updatedRunResult = { - ...runResult.right, - envs: { - global: updatedGlobalEnvVariables, - selected: updatedSelectedEnvVariables, - }, - } - + const updatedRunResult = updateEnvsAfterTestScript(runResult) tab.value.document.testResults = + // @ts-expect-error Typescript can't figure out this inference for some reason translateToSandboxTestResults(updatedRunResult) - - const globalEnvVariables = updateEnvironmentsWithSecret( - runResult.right.envs.global, - "global" - ) - - setGlobalEnvVariables({ - v: 1, - variables: globalEnvVariables, - }) - if ( - environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV" - ) { - const env = getEnvironment({ - type: "MY_ENV", - index: environmentsStore.value.selectedEnvironmentIndex.index, - }) - updateEnvironment( - environmentsStore.value.selectedEnvironmentIndex.index, - { - name: env.name, - v: 1, - id: "id" in env ? env.id : "", - variables: updatedRunResult.envs.selected, - } - ) - } else if ( - environmentsStore.value.selectedEnvironmentIndex.type === - "TEAM_ENV" - ) { - const env = getEnvironment({ - type: "TEAM_ENV", - }) - pipe( - updateTeamEnvironment( - JSON.stringify(updatedRunResult.envs.selected), - environmentsStore.value.selectedEnvironmentIndex.teamEnvID, - env.name - ) - )() - } } else { tab.value.document.testResults = { description: "", @@ -374,6 +317,160 @@ export function runRESTRequest$( return [cancel, res] } +function updateEnvsAfterTestScript(runResult: E.Right) { + const updatedGlobalEnvVariables = updateEnvironmentsWithSecret( + // @ts-expect-error Typescript can't figure out this inference for some reason + cloneDeep(runResult.right.envs.global), + "global" + ) + + const updatedSelectedEnvVariables = updateEnvironmentsWithSecret( + // @ts-expect-error Typescript can't figure out this inference for some reason + cloneDeep(runResult.right.envs.selected), + "selected" + ) + + const updatedRunResult = { + ...runResult.right, + envs: { + global: updatedGlobalEnvVariables, + selected: updatedSelectedEnvVariables, + }, + } + + const globalEnvVariables = updateEnvironmentsWithSecret( + // @ts-expect-error Typescript can't figure out this inference for some reason + runResult.right.envs.global, + "global" + ) + + setGlobalEnvVariables({ + v: 1, + variables: globalEnvVariables, + }) + if (environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV") { + const env = getEnvironment({ + type: "MY_ENV", + index: environmentsStore.value.selectedEnvironmentIndex.index, + }) + updateEnvironment(environmentsStore.value.selectedEnvironmentIndex.index, { + name: env.name, + v: 1, + id: "id" in env ? env.id : "", + variables: updatedRunResult.envs.selected, + }) + } else if ( + environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV" + ) { + const env = getEnvironment({ + type: "TEAM_ENV", + }) + pipe( + updateTeamEnvironment( + JSON.stringify(updatedRunResult.envs.selected), + environmentsStore.value.selectedEnvironmentIndex.teamEnvID, + env.name + ) + )() + } + + return updatedRunResult +} + +export function runTestRunnerRequest(request: HoppRESTRequest): Promise< + | E.Left<"script_fail"> + | E.Right<{ + response: HoppRESTResponse + testResult: HoppTestResult + }> + | undefined +> { + return getFinalEnvsFromPreRequest( + request.preRequestScript, + getCombinedEnvVariables() + ).then(async (envs) => { + if (E.isLeft(envs)) { + console.error(envs.left) + return E.left("script_fail" as const) + } + + const effectiveRequest = await getEffectiveRESTRequest(request, { + id: "env-id", + v: 1, + name: "Env", + variables: combineEnvVariables({ + environments: envs.right, + requestVariables: [], + }), + }) + + const [stream] = createRESTNetworkRequestStream(effectiveRequest) + + const requestResult = stream + .pipe(filter((res) => res.type === "success" || res.type === "fail")) + .toPromise() + .then(async (res) => { + if (res?.type === "success" || res?.type === "fail") { + executedResponses$.next( + // @ts-expect-error Typescript can't figure out this inference for some reason + res + ) + + const runResult = await runTestScript( + res.req.testScript, + envs.right, + { + status: res.statusCode, + body: getTestableBody(res), + headers: res.headers, + } + ) + + if (E.isRight(runResult)) { + const sandboxTestResult = translateToSandboxTestResults( + runResult.right + ) + + updateEnvsAfterTestScript(runResult) + + return E.right({ + response: res, + testResult: sandboxTestResult, + }) + } + const sandboxTestResult = { + description: "", + expectResults: [], + tests: [], + envDiff: { + global: { + additions: [], + deletions: [], + updations: [], + }, + selected: { + additions: [], + deletions: [], + updations: [], + }, + }, + scriptError: true, + } + return E.right({ + response: res, + testResult: sandboxTestResult, + }) + } + }) + + if (requestResult) { + return requestResult + } + + return E.left("script_fail") + }) +} + const getAddedEnvVariables = ( current: Environment["variables"], updated: Environment["variables"] diff --git a/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts index 07f1796e..94dfc938 100644 --- a/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts +++ b/packages/hoppscotch-common/src/helpers/editor/extensions/HoppEnvironment.ts @@ -281,15 +281,19 @@ export class HoppEnvironmentPlugin { const currentTab = restTabs.currentActiveTab.value const currentTabRequest = - currentTab.document.type === "request" - ? currentTab.document.request - : currentTab.document.response.originalRequest + currentTab.document.type === "example-response" + ? currentTab.document.response.originalRequest + : currentTab.document.request watch( currentTabRequest, - (reqVariables) => { + (request) => { + const requestVariables = request?.requestVariables + ? request.requestVariables + : [] + this.envs = [ - ...reqVariables.requestVariables.map(({ key, value }) => ({ + ...requestVariables.map(({ key, value }) => ({ key, value, sourceEnv: "RequestVariable", @@ -308,9 +312,11 @@ export class HoppEnvironmentPlugin { { immediate: true, deep: true } ) + const requestVariables = currentTabRequest?.requestVariables ?? [] + subscribeToStream(aggregateEnvsWithSecrets$, (envs) => { this.envs = [ - ...currentTabRequest.requestVariables.map(({ key, value }) => ({ + ...requestVariables.map(({ key, value }) => ({ key, value, sourceEnv: "RequestVariable", diff --git a/packages/hoppscotch-common/src/helpers/rest/document.ts b/packages/hoppscotch-common/src/helpers/rest/document.ts index 09e35da1..517d631f 100644 --- a/packages/hoppscotch-common/src/helpers/rest/document.ts +++ b/packages/hoppscotch-common/src/helpers/rest/document.ts @@ -1,8 +1,13 @@ -import { HoppRESTRequest, HoppRESTRequestResponse } from "@hoppscotch/data" -import { HoppRESTResponse } from "../types/HoppRESTResponse" -import { HoppTestResult } from "../types/HoppTestResult" +import { + HoppCollection, + HoppRESTRequest, + HoppRESTRequestResponse, +} from "@hoppscotch/data" import { RESTOptionTabs } from "~/components/http/RequestOptions.vue" import { HoppInheritedProperty } from "../types/HoppInheritedProperties" +import { HoppRESTResponse } from "../types/HoppRESTResponse" +import { HoppTestResult } from "../types/HoppTestResult" +import { TestRunnerRequest } from "~/services/test-runner/test-runner.service" export type HoppRESTSaveContext = | { @@ -50,9 +55,122 @@ export type HoppRESTSaveContext = /** * Defines a live 'document' (something that is open and being edited) in the app */ + +export type HoppCollectionSaveContext = + | { + /** + * The origin source of the request + */ + originLocation: "user-collection" + /** + * Path to the request folder + */ + folderPath: string + } + | { + /** + * The origin source of the request + */ + originLocation: "team-collection" + /** + * ID of the team + */ + teamID?: string + /** + * ID of the collection loaded + */ + collectionID?: string + /** + * ID of the request in the team + */ + requestID: string + } + | null + +export type TestRunnerConfig = { + iterations: number + delay: number + stopOnError: boolean + persistResponses: boolean + keepVariableValues: boolean +} + +export type HoppTestRunnerDocument = { + /** + * The document type + */ + type: "test-runner" + + /** + * The test runner configuration + */ + config: TestRunnerConfig + + /** + * initiate test runner on tab open + */ + status: "idle" | "running" | "stopped" | "error" + + /** + * The collection as it is in the document + */ + collection: HoppCollection + + /** + * The type of the collection + */ + collectionType: "my-collections" | "team-collections" + + /** + * collection ID to be used for team collections + * (if it's my-collections, the _ref_id will be used as collectionID) + */ + collectionID: string + + /** + * The request as it is in the document + */ + resultCollection?: HoppCollection + + /** + * The test runner meta information + */ + testRunnerMeta: { + totalRequests: number + completedRequests: number + totalTests: number + passedTests: number + failedTests: number + totalTime: number + } + + /** + * Selected test runner request + */ + request: TestRunnerRequest | null + + /** + * The response of the selected request in collections after running the test + * (if any) + */ + response?: HoppRESTResponse | null + + /** + * The test results of the selected request in collections after running the test + * (if any) + */ + testResults?: HoppTestResult | null + + /** + * Whether the request has any unsaved changes + * (atleast as far as we can say) + */ + isDirty: boolean +} + export type HoppRequestDocument = { /** - * The type of the document + * The document type */ type: "request" @@ -134,4 +252,7 @@ export type HoppSavedExampleDocument = { /** * Defines a live 'document' (something that is open and being edited) in the app */ -export type HoppTabDocument = HoppSavedExampleDocument | HoppRequestDocument +export type HoppTabDocument = + | HoppSavedExampleDocument + | HoppRequestDocument + | HoppTestRunnerDocument diff --git a/packages/hoppscotch-common/src/helpers/runner/adapter.ts b/packages/hoppscotch-common/src/helpers/runner/adapter.ts new file mode 100644 index 00000000..1674801d --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/runner/adapter.ts @@ -0,0 +1,179 @@ +import { HoppCollection } from "@hoppscotch/data" +import { ChildrenResult, SmartTreeAdapter } from "@hoppscotch/ui/helpers" +import { computed, Ref } from "vue" +import { TestRunnerRequest } from "~/services/test-runner/test-runner.service" + +export type Collection = { + type: "collections" + isLastItem: boolean + data: { + parentIndex: null + data: HoppCollection + } +} + +type Folder = { + type: "folders" + isLastItem: boolean + data: { + parentIndex: string + data: HoppCollection + } +} + +type Requests = { + type: "requests" + isLastItem: boolean + data: { + parentIndex: string + data: TestRunnerRequest + } +} + +export type CollectionNode = Collection | Folder | Requests + +export class TestRunnerCollectionsAdapter + implements SmartTreeAdapter +{ + constructor( + public data: Ref, + private show: Ref<"all" | "passed" | "failed"> + ) {} + + private shouldShowRequest(request: TestRunnerRequest): boolean { + // Always show requests that are still loading or haven't run yet + if (!request.testResults || request.isLoading) return true + + const { passed, failed } = this.countTestResults(request.testResults) + + switch (this.show.value) { + case "passed": + return passed > 0 + case "failed": + return failed > 0 + default: + return true + } + } + + private countTestResults(testResult: any) { + let passed = 0 + let failed = 0 + + // Count direct expect results + if (testResult.expectResults) { + for (const result of testResult.expectResults) { + if (result.status === "pass") passed++ + else if (result.status === "fail") failed++ + } + } + + // Count nested test results + if (testResult.tests) { + for (const test of testResult.tests) { + const counts = this.countTestResults(test) + passed += counts.passed + failed += counts.failed + } + } + + return { passed, failed } + } + + navigateToFolderWithIndexPath( + collections: HoppCollection[], + indexPaths: number[] + ) { + if (indexPaths.length === 0) return null + + let target = collections[indexPaths.shift() as number] + + while (indexPaths.length > 0) + target = target.folders[indexPaths.shift() as number] + + return target !== undefined ? target : null + } + + getChildren(id: string | null): Ref> { + return computed(() => { + if (id === null) { + const data = this.data.value.map((item, index) => ({ + id: `folder-${index.toString()}`, + data: { + type: "collections", + isLastItem: index === this.data.value.length - 1, + data: { + parentIndex: null, + data: item, + }, + }, + })) + return { + status: "loaded", + data: data, + } as ChildrenResult + } + + const childType = id.split("-")[0] + + if (childType === "request") { + return { + status: "loaded", + data: [], + } + } + + const folderId = id.split("-")[1] + const indexPath = folderId.split("/").map((x) => parseInt(x)) + const item = this.navigateToFolderWithIndexPath( + this.data.value, + indexPath + ) + + if (item && Object.keys(item).length) { + // Always include all folders for smooth transitions + const folderData = item.folders.map((folder, index) => ({ + id: `folder-${folderId}/${index}`, + data: { + isLastItem: + index === item.folders.length - 1 && item.requests.length === 0, + type: "folders", + isSelected: true, + data: { + parentIndex: id, + data: folder, + }, + }, + })) + + const requestData = item.requests.map((request, index) => { + const shouldShow = this.shouldShowRequest( + request as TestRunnerRequest + ) + return { + id: `request-${id}/${index}`, + data: { + isLastItem: index === item.requests.length - 1, + type: "requests", + isSelected: true, + hidden: !shouldShow, + data: { + parentIndex: id, + data: request, + }, + }, + } + }) + + return { + status: "loaded", + data: [...folderData, ...requestData], + } as ChildrenResult + } + return { + status: "loaded", + data: [], + } + }) + } +} diff --git a/packages/hoppscotch-common/src/helpers/runner/collection-tree.ts b/packages/hoppscotch-common/src/helpers/runner/collection-tree.ts new file mode 100644 index 00000000..57102cd1 --- /dev/null +++ b/packages/hoppscotch-common/src/helpers/runner/collection-tree.ts @@ -0,0 +1,42 @@ +import { ComposerTranslation } from "vue-i18n" +import { GQLError } from "../backend/GQLClient" + +export const getErrorMessage = ( + err: GQLError, + t: ComposerTranslation +) => { + console.error(err) + if (err.type === "network_error") { + return t("error.network_error") + } + switch (err.error) { + case "team_coll/short_title": + return t("collection.name_length_insufficient") + case "team/invalid_coll_id": + case "bug/team_coll/no_coll_id": + case "team_req/invalid_target_id": + return t("team.invalid_coll_id") + case "team/not_required_role": + return t("profile.no_permission") + case "team_req/not_required_role": + return t("profile.no_permission") + case "Forbidden resource": + return t("profile.no_permission") + case "team_req/not_found": + return t("team.no_request_found") + case "bug/team_req/no_req_id": + return t("team.no_request_found") + case "team/collection_is_parent_coll": + return t("team.parent_coll_move") + case "team/target_and_destination_collection_are_same": + return t("team.same_target_destination") + case "team/target_collection_is_already_root_collection": + return t("collection.invalid_root_move") + case "team_req/requests_not_from_same_collection": + return t("request.different_collection") + case "team/team_collections_have_different_parents": + return t("collection.different_parent") + default: + return t("error.something_went_wrong") + } +} diff --git a/packages/hoppscotch-common/src/newstore/collections.ts b/packages/hoppscotch-common/src/newstore/collections.ts index 4513a6ad..ea8a6d59 100644 --- a/packages/hoppscotch-common/src/newstore/collections.ts +++ b/packages/hoppscotch-common/src/newstore/collections.ts @@ -1,4 +1,5 @@ import { + generateUniqueRefId, HoppCollection, HoppGQLAuth, HoppGQLRequest, @@ -514,6 +515,21 @@ const restCollectionDispatchers = defineDispatchers({ if (collection) { const name = `${collection.name} - ${t("action.duplicate")}` + function recursiveChangeRefIdToAvoidConflicts( + collection: HoppCollection + ): HoppCollection { + const newCollection = { + ...collection, + _ref_id: generateUniqueRefId("coll"), + } + + newCollection.folders = newCollection.folders.map((folder) => + recursiveChangeRefIdToAvoidConflicts(folder) + ) + + return newCollection + } + const duplicatedCollection = { ...cloneDeep(collection), name, @@ -522,15 +538,18 @@ const restCollectionDispatchers = defineDispatchers({ : {}), } + const duplicatedCollectionWithNewRefId = + recursiveChangeRefIdToAvoidConflicts(duplicatedCollection) + if (isRootCollection) { - newState.push(duplicatedCollection) + newState.push(duplicatedCollectionWithNewRefId) } else { const parentCollectionIndexPath = indexPaths.slice(0, -1) const parentCollection = navigateToFolderWithIndexPath(state, [ ...parentCollectionIndexPath, ]) - parentCollection?.folders.push(duplicatedCollection) + parentCollection?.folders.push(duplicatedCollectionWithNewRefId) } } @@ -1198,6 +1217,30 @@ export function getRESTCollection(collectionIndex: number) { return restCollectionStore.value.state[collectionIndex] } +export function getRESTCollectionByRefId(ref_id: string) { + function findCollection( + collection: HoppCollection, + ref_id: string + ): HoppCollection | null { + if (collection._ref_id === ref_id) { + return collection + } + for (const folder of collection.folders) { + const found = findCollection(folder, ref_id) + if (found) { + return found + } + } + return null + } + for (const collection of restCollectionStore.value.state) { + const found = findCollection(collection, ref_id) + if (found) { + return found + } + } +} + export function editRESTCollection( collectionIndex: number, partialCollection: Partial diff --git a/packages/hoppscotch-common/src/pages/graphql.vue b/packages/hoppscotch-common/src/pages/graphql.vue index 3a6d3f7f..8bdffb80 100644 --- a/packages/hoppscotch-common/src/pages/graphql.vue +++ b/packages/hoppscotch-common/src/pages/graphql.vue @@ -88,7 +88,6 @@ import { usePageHead } from "@composables/head" import { useI18n } from "@composables/i18n" import { useService } from "dioc/vue" import { computed, onBeforeUnmount, ref } from "vue" - import { defineActionHandler } from "~/helpers/actions" import { connection, disconnect } from "~/helpers/graphql/connection" import { getDefaultGQLRequest } from "~/helpers/graphql/default" diff --git a/packages/hoppscotch-common/src/pages/index.vue b/packages/hoppscotch-common/src/pages/index.vue index 4a3743e9..e423f532 100644 --- a/packages/hoppscotch-common/src/pages/index.vue +++ b/packages/hoppscotch-common/src/pages/index.vue @@ -18,7 +18,7 @@ :is-removable="activeTabs.length > 1" :close-visibility="'hover'" > - + + + + + - + @@ -211,9 +219,9 @@ const onTabUpdate = (tab: HoppTab) => { const addNewTab = () => { const tab = tabs.createNewTab({ + type: "request", request: getDefaultRESTRequest(), isDirty: false, - type: "request", }) tabs.setActiveTab(tab.id) @@ -222,6 +230,18 @@ const sortTabs = (e: { oldIndex: number; newIndex: number }) => { tabs.updateTabOrdering(e.oldIndex, e.newIndex) } +const getTabName = (tab: HoppTab) => { + if (tab.document.type === "request") { + return tab.document.request.name + } else if (tab.document.type === "test-runner") { + return tab.document.collection.name + } else if (tab.document.type === "example-response") { + return tab.document.response.name + } + + return "Unnamed tab" +} + const inspectionService = useService(InspectionService) const removeTab = (tabID: string) => { @@ -255,9 +275,9 @@ const duplicateTab = (tabID: string) => { const tab = tabs.getTabRef(tabID) if (tab.value && tab.value.document.type === "request") { const newTab = tabs.createNewTab({ + type: "request", request: cloneDeep(tab.value.document.request), isDirty: true, - type: "request", }) tabs.setActiveTab(newTab.id) } @@ -268,14 +288,6 @@ const onResolveConfirmCloseAllTabs = () => { confirmingCloseAllTabs.value = false } -const getTabName = (tab: HoppTab) => { - if (tab.document.type === "request") { - return tab.document.request.name - } else if (tab.document.type === "example-response") { - return tab.document.response.name - } -} - const requestToRename = computed(() => { if (!renameTabID.value) return null const tab = tabs.getTabRef(renameTabID.value) @@ -386,7 +398,10 @@ defineActionHandler("rest.request.open", ({ doc }) => { tabs.createNewTab(doc) }) -defineActionHandler("request.rename", openReqRenameModal) +defineActionHandler("request.rename", () => { + if (tabs.currentActiveTab.value.document.type === "request") + openReqRenameModal(tabs.currentActiveTab.value.id) +}) defineActionHandler("tab.duplicate-tab", ({ tabID }) => { duplicateTab(tabID ?? currentTabID.value) }) diff --git a/packages/hoppscotch-common/src/pages/r/_id.vue b/packages/hoppscotch-common/src/pages/r/_id.vue index 94ab6668..aae2f795 100644 --- a/packages/hoppscotch-common/src/pages/r/_id.vue +++ b/packages/hoppscotch-common/src/pages/r/_id.vue @@ -136,6 +136,7 @@ const addRequestToTab = () => { const request: unknown = JSON.parse(data.right.shortcode?.request as string) tabs.createNewTab({ + type: "request", request: safelyExtractRESTRequest(request, getDefaultRESTRequest()), isDirty: false, type: "request", diff --git a/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts b/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts index d62489ce..933cff47 100644 --- a/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts +++ b/packages/hoppscotch-common/src/platform/std/inspections/extension.inspector.ts @@ -35,7 +35,7 @@ export class ExtensionInspectorService extends Service implements Inspector { this.inspection.registerInspector(this) } - getInspections(req: Readonly>) { + getInspections(req: Readonly>) { const currentExtensionStatus = this.extensionService.extensionStatus const isExtensionInstalled = computed( @@ -55,6 +55,8 @@ export class ExtensionInspectorService extends Service implements Inspector { return computed(() => { const results: InspectorResult[] = [] + if (!req.value) return results + const url = req.value.endpoint const localHostURLs = ["localhost", "127.0.0.1"] diff --git a/packages/hoppscotch-common/src/platform/tab.ts b/packages/hoppscotch-common/src/platform/tab.ts new file mode 100644 index 00000000..83e6a667 --- /dev/null +++ b/packages/hoppscotch-common/src/platform/tab.ts @@ -0,0 +1,11 @@ +import { PersistableTabState } from "~/services/tab" +import { HoppUser } from "./auth" +import { HoppTabDocument } from "~/helpers/rest/document" + +export type TabStatePlatformDef = { + loadTabStateFromSync: () => Promise | null> + writeCurrentTabState: ( + user: HoppUser, + persistableTabState: PersistableTabState + ) => Promise +} diff --git a/packages/hoppscotch-common/src/services/context-menu/menu/parameter.menu.ts b/packages/hoppscotch-common/src/services/context-menu/menu/parameter.menu.ts index 286f9c26..74b1d88f 100644 --- a/packages/hoppscotch-common/src/services/context-menu/menu/parameter.menu.ts +++ b/packages/hoppscotch-common/src/services/context-menu/menu/parameter.menu.ts @@ -89,6 +89,9 @@ export class ParameterMenuService extends Service implements ContextMenu { const tabService = getService(RESTTabService) + if (tabService.currentActiveTab.value.document.type === "test-runner") + return + const currentActiveRequest = tabService.currentActiveTab.value.document.type === "request" ? tabService.currentActiveTab.value.document.request diff --git a/packages/hoppscotch-common/src/services/context-menu/menu/url.menu.ts b/packages/hoppscotch-common/src/services/context-menu/menu/url.menu.ts index eac27ed6..dcc961ac 100644 --- a/packages/hoppscotch-common/src/services/context-menu/menu/url.menu.ts +++ b/packages/hoppscotch-common/src/services/context-menu/menu/url.menu.ts @@ -55,9 +55,9 @@ export class URLMenuService extends Service implements ContextMenu { } this.restTab.createNewTab({ + type: "request", request: request, isDirty: false, - type: "request", }) } diff --git a/packages/hoppscotch-common/src/services/inspection/index.ts b/packages/hoppscotch-common/src/services/inspection/index.ts index 6c15a525..4e564635 100644 --- a/packages/hoppscotch-common/src/services/inspection/index.ts +++ b/packages/hoppscotch-common/src/services/inspection/index.ts @@ -8,7 +8,6 @@ import { computed, markRaw, reactive } from "vue" import { Component, Ref, ref, watch } from "vue" import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" import { RESTTabService } from "../tab/rest" - /** * Defines how to render the text in an Inspector Result */ @@ -127,18 +126,24 @@ export class InspectionService extends Service { watch( () => [this.inspectors.entries(), this.restTab.currentActiveTab.value.id], () => { - const currentTabRequest = computed(() => - this.restTab.currentActiveTab.value.document.type === "request" + const currentTabRequest = computed(() => { + if ( + this.restTab.currentActiveTab.value.document.type === "test-runner" + ) + return null + + return this.restTab.currentActiveTab.value.document.type === "request" ? this.restTab.currentActiveTab.value.document.request : this.restTab.currentActiveTab.value.document.response .originalRequest - ) + }) - const currentTabResponse = computed(() => - this.restTab.currentActiveTab.value.document.type === "request" - ? this.restTab.currentActiveTab.value.document.response - : null - ) + const currentTabResponse = computed(() => { + if (this.restTab.currentActiveTab.value.document.type === "request") { + return this.restTab.currentActiveTab.value.document.response + } + return null + }) const reqRef = computed(() => currentTabRequest.value) const resRef = computed(() => currentTabResponse.value) @@ -147,6 +152,7 @@ export class InspectionService extends Service { const debouncedRes = refDebounced(resRef, 1000, { maxWait: 2000 }) const inspectorRefs = Array.from(this.inspectors.values()).map((x) => + // @ts-expect-error - This is a valid call x.getInspections(debouncedReq, debouncedRes) ) diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/authorization.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/authorization.inspector.ts index 73373c09..38159f85 100644 --- a/packages/hoppscotch-common/src/services/inspection/inspectors/authorization.inspector.ts +++ b/packages/hoppscotch-common/src/services/inspection/inspectors/authorization.inspector.ts @@ -43,7 +43,10 @@ export class AuthorizationInspectorService const activeTabDocument = this.restTabService.currentActiveTab.value.document - if (activeTabDocument.type === "example-response") { + if ( + activeTabDocument.type === "example-response" || + activeTabDocument.type === "test-runner" + ) { return null } @@ -60,6 +63,7 @@ export class AuthorizationInspectorService req: Readonly> ) { return computed(() => { + if (!req.value) return [] const currentInterceptorIDValue = this.interceptorService.currentInterceptorID.value diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts index 85bbfc15..58ab7e73 100644 --- a/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts +++ b/packages/hoppscotch-common/src/services/inspection/inspectors/environment.inspector.ts @@ -77,10 +77,12 @@ export class EnvironmentInspectorService extends Service implements Inspector { const currentTabRequest = currentTab.document.type === "request" ? currentTab.document.request - : currentTab.document.response.originalRequest + : currentTab.document.type === "example-response" + ? currentTab.document.response.originalRequest + : null const environmentVariables = [ - ...currentTabRequest.requestVariables, + ...(currentTabRequest?.requestVariables ?? []), ...this.aggregateEnvsWithSecrets.value, ] @@ -191,11 +193,13 @@ export class EnvironmentInspectorService extends Service implements Inspector { const currentTabRequest = currentTab.document.type === "request" ? currentTab.document.request - : currentTab.document.response.originalRequest + : currentTab.document.type === "example-response" + ? currentTab.document.response.originalRequest + : null const environmentVariables = this.filterNonEmptyEnvironmentVariables([ - ...currentTabRequest.requestVariables.map((env) => ({ + ...(currentTabRequest?.requestVariables ?? []).map((env) => ({ ...env, secret: false, sourceEnv: "RequestVariable", @@ -300,6 +304,8 @@ export class EnvironmentInspectorService extends Service implements Inspector { return computed(() => { const results: InspectorResult[] = [] + if (!req.value) return results + const headers = req.value.headers const params = req.value.params diff --git a/packages/hoppscotch-common/src/services/inspection/inspectors/header.inspector.ts b/packages/hoppscotch-common/src/services/inspection/inspectors/header.inspector.ts index df12f1f8..cfcb48a0 100644 --- a/packages/hoppscotch-common/src/services/inspection/inspectors/header.inspector.ts +++ b/packages/hoppscotch-common/src/services/inspection/inspectors/header.inspector.ts @@ -40,6 +40,9 @@ export class HeaderInspectorService extends Service implements Inspector { ) { return computed(() => { const results: InspectorResult[] = [] + + if (!req.value) return results + const headers = req.value.headers const headerKeys = Object.values(headers).map((header) => header.key) diff --git a/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts b/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts index acfccfae..77f1d9a1 100644 --- a/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/__tests__/__mocks__/index.ts @@ -25,7 +25,7 @@ const DEFAULT_SETTINGS = getDefaultSettings() export const REST_COLLECTIONS_MOCK: HoppCollection[] = [ { - v: 4, + v: 5, name: "Echo", folders: [], requests: [ @@ -51,7 +51,7 @@ export const REST_COLLECTIONS_MOCK: HoppCollection[] = [ export const GQL_COLLECTIONS_MOCK: HoppCollection[] = [ { - v: 4, + v: 5, name: "Echo", folders: [], requests: [ diff --git a/packages/hoppscotch-common/src/services/persistence/index.ts b/packages/hoppscotch-common/src/services/persistence/index.ts index dc883284..dcd462ba 100644 --- a/packages/hoppscotch-common/src/services/persistence/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/index.ts @@ -698,22 +698,22 @@ export class PersistenceService extends Service { try { if (restTabStateData) { - let parsedGqlTabStateData = JSON.parse(restTabStateData) + let parsedRESTTabStateData = JSON.parse(restTabStateData) // Validate data read from localStorage - const result = REST_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData) + const result = REST_TAB_STATE_SCHEMA.safeParse(parsedRESTTabStateData) if (result.success) { - parsedGqlTabStateData = result.data + parsedRESTTabStateData = result.data } else { this.showErrorToast(restTabStateKey) window.localStorage.setItem( `${restTabStateKey}-backup`, - JSON.stringify(parsedGqlTabStateData) + JSON.stringify(parsedRESTTabStateData) ) } - this.restTabService.loadTabsFromPersistedState(parsedGqlTabStateData) + this.restTabService.loadTabsFromPersistedState(parsedRESTTabStateData) } } catch (e) { console.error( diff --git a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts index 506fb21b..d65bc146 100644 --- a/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts +++ b/packages/hoppscotch-common/src/services/persistence/validation-schemas/index.ts @@ -7,6 +7,7 @@ import { HoppRESTRequest, HoppRESTHeaders, HoppRESTRequestResponse, + HoppCollection, } from "@hoppscotch/data" import { entityReference } from "verzod" import { z } from "zod" @@ -75,36 +76,13 @@ const SettingsDefSchema = z.object({ ENABLE_AI_EXPERIMENTS: z.optional(z.boolean()), }) -// Common properties shared across REST & GQL collections -const HoppCollectionSchemaCommonProps = z - .object({ - v: z.number(), - name: z.string(), - id: z.optional(z.string()), - }) - .strict() - const HoppRESTRequestSchema = entityReference(HoppRESTRequest) const HoppGQLRequestSchema = entityReference(HoppGQLRequest) -// @ts-expect-error recursive schema -const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({ - folders: z.array(z.lazy(() => HoppRESTCollectionSchema)), - requests: z.optional(z.array(HoppRESTRequestSchema)), +const HoppRESTCollectionSchema = entityReference(HoppCollection) - auth: z.optional(HoppRESTAuth), - headers: z.optional(HoppRESTHeaders), -}).strict() - -// @ts-expect-error recursive schema -const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({ - folders: z.array(z.lazy(() => HoppGQLCollectionSchema)), - requests: z.optional(z.array(HoppGQLRequestSchema)), - - auth: z.optional(HoppGQLAuth), - headers: z.optional(z.array(GQLHeader)), -}).strict() +const HoppGQLCollectionSchema = entityReference(HoppCollection) export const VUEX_SCHEMA = z.object({ postwoman: z.optional( @@ -551,6 +529,33 @@ export const REST_TAB_STATE_SCHEMA = z saveContext: z.optional(HoppRESTSaveContextSchema), isDirty: z.boolean(), }), + z.object({ + type: z.literal("test-runner").catch("test-runner"), + config: z.object({ + delay: z.number(), + iterations: z.number(), + keepVariableValues: z.boolean(), + persistResponses: z.boolean(), + stopOnError: z.boolean(), + }), + status: z.enum(["idle", "running", "stopped", "error"]), + collection: HoppRESTCollectionSchema, + collectionType: z.enum(["my-collections", "team-collections"]), + collectionID: z.optional(z.string()), + resultCollection: z.optional(HoppRESTCollectionSchema), + testRunnerMeta: z.object({ + totalRequests: z.number(), + completedRequests: z.number(), + totalTests: z.number(), + passedTests: z.number(), + failedTests: z.number(), + totalTime: z.number(), + }), + request: z.nullable(entityReference(HoppRESTRequest)), + response: z.nullable(HoppRESTResponseSchema), + testResults: z.optional(z.nullable(HoppTestResultSchema)), + isDirty: z.boolean(), + }), ]), }) ), diff --git a/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts b/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts index a0038262..cb195b0b 100644 --- a/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts +++ b/packages/hoppscotch-common/src/services/spotlight/searchers/collections.searcher.ts @@ -325,9 +325,9 @@ export class CollectionsSpotlightSearcherService this.restTab.createNewTab( { + type: "request", request: req, isDirty: false, - type: "request", saveContext: { originLocation: "user-collection", folderPath: folderPath.join("/"), diff --git a/packages/hoppscotch-common/src/services/tab/rest.ts b/packages/hoppscotch-common/src/services/tab/rest.ts index 2bab729f..b1ba262f 100644 --- a/packages/hoppscotch-common/src/services/tab/rest.ts +++ b/packages/hoppscotch-common/src/services/tab/rest.ts @@ -1,9 +1,9 @@ +import { Container } from "dioc" import { isEqual } from "lodash-es" import { computed } from "vue" import { getDefaultRESTRequest } from "~/helpers/rest/default" import { HoppRESTSaveContext, HoppTabDocument } from "~/helpers/rest/document" import { TabService } from "./tab" -import { Container } from "dioc" export class RESTTabService extends TabService { public static readonly ID = "REST_TAB_SERVICE" @@ -52,6 +52,8 @@ export class RESTTabService extends TabService { public getTabRefWithSaveContext(ctx: HoppRESTSaveContext) { for (const tab of this.tabMap.values()) { // For `team-collection` request id can be considered unique + if (tab.document.type === "test-runner") continue + if (ctx?.originLocation === "team-collection") { if ( tab.document.saveContext?.originLocation === "team-collection" && diff --git a/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts b/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts new file mode 100644 index 00000000..95019465 --- /dev/null +++ b/packages/hoppscotch-common/src/services/test-runner/test-runner.service.ts @@ -0,0 +1,355 @@ +import { + HoppCollection, + HoppRESTHeaders, + HoppRESTRequest, +} from "@hoppscotch/data" +import { Service } from "dioc" +import * as E from "fp-ts/Either" +import { cloneDeep } from "lodash-es" +import { Ref } from "vue" +import { runTestRunnerRequest } from "~/helpers/RequestRunner" +import { + HoppTestRunnerDocument, + TestRunnerConfig, +} from "~/helpers/rest/document" +import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse" +import { HoppTestData, HoppTestResult } from "~/helpers/types/HoppTestResult" +import { HoppTab } from "../tab" + +export type TestRunnerOptions = { + stopRef: Ref +} & TestRunnerConfig + +export type TestRunnerRequest = HoppRESTRequest & { + type: "test-response" + response?: HoppRESTResponse | null + testResults?: HoppTestResult | null + isLoading?: boolean + error?: string + renderResults?: boolean + passedTests: number + failedTests: number +} + +function delay(timeMS: number) { + return new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, timeMS) + return () => { + clearTimeout(timeout) + reject(new Error("Operation cancelled")) + } + }) +} + +export class TestRunnerService extends Service { + public static readonly ID = "TEST_RUNNER_SERVICE" + + public runTests( + tab: Ref>, + collection: HoppCollection, + options: TestRunnerOptions + ) { + // Reset the result collection + tab.value.document.status = "running" + tab.value.document.resultCollection = { + v: collection.v, + id: collection.id, + name: collection.name, + auth: collection.auth, + headers: collection.headers, + folders: [], + requests: [], + } + + this.runTestCollection(tab, collection, options) + .then(() => { + tab.value.document.status = "stopped" + }) + .catch((error) => { + if ( + error instanceof Error && + error.message === "Test execution stopped" + ) { + tab.value.document.status = "stopped" + } else { + tab.value.document.status = "error" + console.error("Test runner failed:", error) + } + }) + .finally(() => { + tab.value.document.status = "stopped" + }) + } + + private async runTestCollection( + tab: Ref>, + collection: HoppCollection, + options: TestRunnerOptions, + parentPath: number[] = [], + parentHeaders?: HoppRESTHeaders, + parentAuth?: HoppRESTRequest["auth"] + ) { + try { + // Compute inherited auth and headers for this collection + const inheritedAuth = + collection.auth?.authType === "inherit" && collection.auth.authActive + ? parentAuth || { authType: "none", authActive: false } + : collection.auth || { authType: "none", authActive: false } + + const inheritedHeaders: HoppRESTHeaders = [ + ...(parentHeaders || []), + ...collection.headers, + ] + + // Process folders progressively + for (let i = 0; i < collection.folders.length; i++) { + if (options.stopRef?.value) { + tab.value.document.status = "stopped" + throw new Error("Test execution stopped") + } + + const folder = collection.folders[i] + const currentPath = [...parentPath, i] + + // Add folder to the result collection + this.addFolderToPath( + tab.value.document.resultCollection!, + currentPath, + { + ...cloneDeep(folder), + folders: [], + requests: [], + } + ) + + // Pass inherited headers and auth to the folder + await this.runTestCollection( + tab, + folder, + options, + currentPath, + inheritedHeaders, + inheritedAuth + ) + } + + // Process requests progressively + for (let i = 0; i < collection.requests.length; i++) { + if (options.stopRef?.value) { + tab.value.document.status = "stopped" + throw new Error("Test execution stopped") + } + + const request = collection.requests[i] as TestRunnerRequest + const currentPath = [...parentPath, i] + + // Add request to the result collection before execution + this.addRequestToPath( + tab.value.document.resultCollection!, + currentPath, + cloneDeep(request) + ) + + // Update the request with inherited headers and auth before execution + const finalRequest = { + ...request, + auth: + request.auth.authType === "inherit" && request.auth.authActive + ? inheritedAuth + : request.auth, + headers: [...inheritedHeaders, ...request.headers], + } + + await this.runTestRequest( + tab, + finalRequest, + collection, + options, + currentPath + ) + + if (options.delay && options.delay > 0) { + try { + await delay(options.delay) + } catch (error) { + if (options.stopRef?.value) { + tab.value.document.status = "stopped" + throw new Error("Test execution stopped") + } + } + } + } + } catch (error) { + if ( + error instanceof Error && + error.message === "Test execution stopped" + ) { + throw error + } + tab.value.document.status = "error" + console.error("Collection execution failed:", error) + throw error + } + } + + private addFolderToPath( + collection: HoppCollection, + path: number[], + folder: HoppCollection + ) { + let current = collection + + // Navigate to the parent folder + for (let i = 0; i < path.length - 1; i++) { + current = current.folders[path[i]] + } + + // Add the folder at the specified index + if (path.length > 0) { + current.folders[path[path.length - 1]] = folder + } + } + + private addRequestToPath( + collection: HoppCollection, + path: number[], + request: TestRunnerRequest + ) { + let current = collection + + // Navigate to the parent folder + for (let i = 0; i < path.length - 1; i++) { + current = current.folders[path[i]] + } + + // Add the request at the specified index + if (path.length > 0) { + current.requests[path[path.length - 1]] = request + } + } + + private updateRequestAtPath( + collection: HoppCollection, + path: number[], + updates: Partial + ) { + let current = collection + + // Navigate to the parent folder + for (let i = 0; i < path.length - 1; i++) { + current = current.folders[path[i]] + } + + // Update the request at the specified index + if (path.length > 0) { + const index = path[path.length - 1] + current.requests[index] = { + ...current.requests[index], + ...updates, + } as TestRunnerRequest + } + } + + private async runTestRequest( + tab: Ref>, + request: TestRunnerRequest, + collection: HoppCollection, + options: TestRunnerOptions, + path: number[] + ) { + if (options.stopRef?.value) { + throw new Error("Test execution stopped") + } + + try { + // Update request status in the result collection + this.updateRequestAtPath(tab.value.document.resultCollection!, path, { + isLoading: true, + error: undefined, + }) + + const results = await runTestRunnerRequest(request) + + if (options.stopRef?.value) { + throw new Error("Test execution stopped") + } + + if (results && E.isRight(results)) { + const { response, testResult } = results.right + const { passed, failed } = this.getTestResultInfo(testResult) + + tab.value.document.testRunnerMeta.totalTests += passed + failed + tab.value.document.testRunnerMeta.passedTests += passed + tab.value.document.testRunnerMeta.failedTests += failed + + // Update request with results in the result collection + this.updateRequestAtPath(tab.value.document.resultCollection!, path, { + testResults: testResult, + response: options.persistResponses ? response : null, + isLoading: false, + }) + + if (response.type === "success" || response.type === "fail") { + tab.value.document.testRunnerMeta.totalTime += + response.meta.responseDuration + tab.value.document.testRunnerMeta.completedRequests += 1 + } + } else { + const errorMsg = "Request execution failed" + + // Update request with error in the result collection + this.updateRequestAtPath(tab.value.document.resultCollection!, path, { + error: errorMsg, + isLoading: false, + }) + + if (options.stopOnError) { + tab.value.document.status = "stopped" + throw new Error("Test execution stopped due to error") + } + } + } catch (error) { + if ( + error instanceof Error && + error.message === "Test execution stopped" + ) { + throw error + } + + const errorMsg = + error instanceof Error ? error.message : "Unknown error occurred" + + // Update request with error in the result collection + this.updateRequestAtPath(tab.value.document.resultCollection!, path, { + error: errorMsg, + isLoading: false, + }) + + if (options.stopOnError) { + tab.value.document.status = "stopped" + throw new Error("Test execution stopped due to error") + } + } + } + + private getTestResultInfo(testResult: HoppTestData) { + let passed = 0 + let failed = 0 + + for (const result of testResult.expectResults) { + if (result.status === "pass") { + passed++ + } else if (result.status === "fail") { + failed++ + } + } + + for (const nestedTest of testResult.tests) { + const nestedResult = this.getTestResultInfo(nestedTest) + passed += nestedResult.passed + failed += nestedResult.failed + } + + return { passed, failed } + } +} diff --git a/packages/hoppscotch-data/package.json b/packages/hoppscotch-data/package.json index 0ff81208..7349f99c 100644 --- a/packages/hoppscotch-data/package.json +++ b/packages/hoppscotch-data/package.json @@ -36,6 +36,7 @@ "homepage": "https://github.com/hoppscotch/hoppscotch#readme", "devDependencies": { "@types/lodash": "4.17.10", + "@types/uuid": "10.0.0", "typescript": "5.6.3", "vite": "5.4.9" }, @@ -44,6 +45,7 @@ "io-ts": "2.2.21", "lodash": "4.17.21", "parser-ts": "0.7.0", + "uuid": "10.0.0", "verzod": "0.2.2", "zod": "3.23.8" } diff --git a/packages/hoppscotch-data/src/collection/index.ts b/packages/hoppscotch-data/src/collection/index.ts index 0dca4cd2..bb5540d3 100644 --- a/packages/hoppscotch-data/src/collection/index.ts +++ b/packages/hoppscotch-data/src/collection/index.ts @@ -4,22 +4,25 @@ import V1_VERSION from "./v/1" import V2_VERSION from "./v/2" import V3_VERSION from "./v/3" import V4_VERSION from "./v/4" +import V5_VERSION from "./v/5" import { z } from "zod" import { translateToNewRequest } from "../rest" import { translateToGQLRequest } from "../graphql" +import { generateUniqueRefId } from "../utils/collection" const versionedObject = z.object({ v: z.number(), }) export const HoppCollection = createVersionedEntity({ - latestVersion: 4, + latestVersion: 5, versionMap: { 1: V1_VERSION, 2: V2_VERSION, 3: V3_VERSION, 4: V4_VERSION, + 5: V5_VERSION, }, getVersion(data) { const versionCheck = versionedObject.safeParse(data) @@ -35,7 +38,7 @@ export const HoppCollection = createVersionedEntity({ export type HoppCollection = InferredEntity -export const CollectionSchemaVersion = 4 +export const CollectionSchemaVersion = 5 /** * Generates a Collection object. This ignores the version number object @@ -46,6 +49,7 @@ export function makeCollection(x: Omit): HoppCollection { return { v: CollectionSchemaVersion, ...x, + _ref_id: x._ref_id ? x._ref_id : generateUniqueRefId("coll"), } } @@ -72,6 +76,7 @@ export function translateToNewRESTCollection(x: any): HoppCollection { }) if (x.id) obj.id = x.id + if (x._ref_id) obj._ref_id = x._ref_id return obj } @@ -99,6 +104,7 @@ export function translateToNewGQLCollection(x: any): HoppCollection { }) if (x.id) obj.id = x.id + if (x._ref_id) obj._ref_id = x._ref_id return obj } diff --git a/packages/hoppscotch-data/src/collection/v/4.ts b/packages/hoppscotch-data/src/collection/v/4.ts index b6607683..a8996ff4 100644 --- a/packages/hoppscotch-data/src/collection/v/4.ts +++ b/packages/hoppscotch-data/src/collection/v/4.ts @@ -6,7 +6,7 @@ import { HoppRESTAuth } from "../../rest/v/8" import { V3_SCHEMA, v3_baseCollectionSchema } from "./3" -const v4_baseCollectionSchema = v3_baseCollectionSchema.extend({ +export const v4_baseCollectionSchema = v3_baseCollectionSchema.extend({ v: z.literal(4), auth: z.union([HoppRESTAuth, HoppGQLAuth]), }) @@ -19,7 +19,7 @@ type Output = z.output & { folders: Output[] } -const V4_SCHEMA: z.ZodType = +export const V4_SCHEMA: z.ZodType = v4_baseCollectionSchema.extend({ folders: z.lazy(() => z.array(V4_SCHEMA)), }) diff --git a/packages/hoppscotch-data/src/collection/v/5.ts b/packages/hoppscotch-data/src/collection/v/5.ts new file mode 100644 index 00000000..00eea6e3 --- /dev/null +++ b/packages/hoppscotch-data/src/collection/v/5.ts @@ -0,0 +1,36 @@ +import { defineVersion } from "verzod" +import { z } from "zod" + +import { V4_SCHEMA, v4_baseCollectionSchema } from "./4" +import { generateUniqueRefId } from "../../utils/collection" + +const v5_baseCollectionSchema = v4_baseCollectionSchema.extend({ + v: z.literal(5), + _ref_id: z.string().optional(), +}) + +type Input = z.input & { + folders: Input[] +} + +type Output = z.output & { + folders: Output[] +} + +const V5_SCHEMA: z.ZodType = + v5_baseCollectionSchema.extend({ + folders: z.lazy(() => z.array(V5_SCHEMA)), + }) + +export default defineVersion({ + initial: false, + schema: V5_SCHEMA, + // @ts-expect-error + up(old: z.infer) { + return { + ...old, + v: 5 as const, + _ref_id: generateUniqueRefId("coll"), + } + }, +}) diff --git a/packages/hoppscotch-data/src/index.ts b/packages/hoppscotch-data/src/index.ts index 4576e456..fe1a5771 100644 --- a/packages/hoppscotch-data/src/index.ts +++ b/packages/hoppscotch-data/src/index.ts @@ -5,3 +5,4 @@ export * from "./rawKeyValue" export * from "./environment" export * from "./global-environment" export * from "./predefinedVariables" +export * from "./utils/collection" diff --git a/packages/hoppscotch-data/src/utils/collection.ts b/packages/hoppscotch-data/src/utils/collection.ts new file mode 100644 index 00000000..78d75c47 --- /dev/null +++ b/packages/hoppscotch-data/src/utils/collection.ts @@ -0,0 +1,13 @@ +import { v4 as uuidV4 } from "uuid" + +/** + * Generate a unique reference ID + * @param prefix Prefix to add to the generated ID + * @returns The generated reference ID + */ +export const generateUniqueRefId = (prefix = "") => { + const timestamp = Date.now().toString(36) + const randomPart = uuidV4() + + return `${prefix}_${timestamp}_${randomPart}` +} diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts index 2726b747..52c29a36 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.platform.ts @@ -48,6 +48,7 @@ import { updateRESTRequestOrder, } from "@hoppscotch/common/newstore/collections" import { + generateUniqueRefId, GQLHeader, HoppCollection, HoppGQLRequest, @@ -70,6 +71,7 @@ function initCollectionsSync() { gqlCollectionsSyncer.startStoreSync() + // TODO: fix collection schema transformation on backend maybe? loadUserCollections("REST") loadUserCollections("GQL") @@ -94,6 +96,7 @@ function initCollectionsSync() { type ExportedUserCollectionREST = { id?: string + _ref_id?: string folders: ExportedUserCollectionREST[] requests: Array name: string @@ -102,6 +105,7 @@ type ExportedUserCollectionREST = { type ExportedUserCollectionGQL = { id?: string + _ref_id?: string folders: ExportedUserCollectionGQL[] requests: Array name: string @@ -130,11 +134,13 @@ function exportedCollectionToHoppCollection( : { auth: { authType: "inherit", authActive: false }, headers: [], + _ref_id: generateUniqueRefId("coll"), } return { id: restCollection.id, - v: 4, + _ref_id: data._ref_id ?? generateUniqueRefId("coll"), + v: 5, name: restCollection.name, folders: restCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -192,11 +198,13 @@ function exportedCollectionToHoppCollection( : { auth: { authType: "inherit", authActive: false }, headers: [], + _ref_id: generateUniqueRefId("coll"), } return { id: gqlCollection.id, - v: 4, + _ref_id: data._ref_id ?? generateUniqueRefId("coll"), + v: 5, name: gqlCollection.name, folders: gqlCollection.folders.map((folder) => exportedCollectionToHoppCollection(folder, collectionType) @@ -366,6 +374,7 @@ function setupUserCollectionCreatedSubscription() { : { auth: { authType: "inherit", authActive: false }, headers: [], + _ref_id: generateUniqueRefId("coll"), } runDispatchWithOutSyncing(() => { @@ -374,7 +383,8 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 4, + v: 5, + _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), }) @@ -382,7 +392,8 @@ function setupUserCollectionCreatedSubscription() { name: res.right.userCollectionCreated.title, folders: [], requests: [], - v: 4, + v: 5, + _ref_id: data._ref_id, auth: data.auth, headers: addDescriptionField(data.headers), }) @@ -587,12 +598,13 @@ function setupUserCollectionDuplicatedSubscription() { ) // Incoming data transformed to the respective internal representations - const { auth, headers } = + const { auth, headers, _ref_id } = data && data != "null" ? JSON.parse(data) : { auth: { authType: "inherit", authActive: false }, headers: [], + _ref_id: generateUniqueRefId("coll"), } const folders = transformDuplicatedCollections(childCollectionsJSONStr) @@ -607,7 +619,8 @@ function setupUserCollectionDuplicatedSubscription() { name, folders, requests, - v: 3, + v: 5, + _ref_id, auth, headers: addDescriptionField(headers), } @@ -1037,7 +1050,7 @@ function transformDuplicatedCollections( name, folders, requests, - v: 3, + v: 5, auth, headers: addDescriptionField(headers), } diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.sync.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.sync.ts index d16819af..c54ccfce 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/collections.sync.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/collections.sync.ts @@ -9,7 +9,11 @@ import { settingsStore, } from "@hoppscotch/common/newstore/settings" -import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data" +import { + generateUniqueRefId, + HoppCollection, + HoppRESTRequest, +} from "@hoppscotch/data" import { getSyncInitFunction, StoreSyncDefinitionOf } from "../../lib/sync" import { createMapper } from "../../lib/sync/mapper" @@ -53,6 +57,7 @@ const recursivelySyncCollections = async ( authActive: true, }, headers: collection.headers ?? [], + _ref_id: collection._ref_id, } const res = await createRESTRootUserCollection( collection.name, @@ -69,9 +74,11 @@ const recursivelySyncCollections = async ( authActive: true, }, headers: [], + _ref_id: generateUniqueRefId("coll"), } collection.id = parentCollectionID + collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll") collection.auth = returnedData.auth collection.headers = returnedData.headers removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath) @@ -86,6 +93,7 @@ const recursivelySyncCollections = async ( authActive: true, }, headers: collection.headers ?? [], + _ref_id: collection._ref_id, } const res = await createRESTChildUserCollection( @@ -105,9 +113,11 @@ const recursivelySyncCollections = async ( authActive: true, }, headers: [], + _ref_id: generateUniqueRefId("coll"), } collection.id = childCollectionId + collection._ref_id = returnedData._ref_id ?? generateUniqueRefId("coll") collection.auth = returnedData.auth collection.headers = returnedData.headers diff --git a/packages/hoppscotch-selfhost-web/src/platform/tabState/tabState.platform.ts b/packages/hoppscotch-selfhost-web/src/platform/tabState/tabState.platform.ts new file mode 100644 index 00000000..4c1386af --- /dev/null +++ b/packages/hoppscotch-selfhost-web/src/platform/tabState/tabState.platform.ts @@ -0,0 +1,38 @@ +import { PersistableTabState } from "@hoppscotch/common/services/tab" +import { HoppTabDocument } from "@hoppscotch/common/helpers/rest/document" +import { HoppUser } from "@hoppscotch/common/platform/auth" +import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab" +import { def as platformAuth } from "@platform/auth" +import { getCurrentRestSession, updateUserSession } from "./tabState.api" +import { SessionType } from "../../api/generated/graphql" +import * as E from "fp-ts/Either" + +async function writeCurrentTabState( + _: HoppUser, + persistableTabState: PersistableTabState +) { + await updateUserSession(JSON.stringify(persistableTabState), SessionType.Rest) +} + +async function loadTabStateFromSync(): Promise | null> { + const currentUser = platformAuth.getCurrentUser() + + if (!currentUser) + throw new Error("Cannot load request from sync without login") + + const res = await getCurrentRestSession() + + if (E.isRight(res)) { + const currentRESTSession = res.right.me.currentRESTSession + + return currentRESTSession ? JSON.parse(currentRESTSession) : null + } else { + } + + return null +} + +export const def: TabStatePlatformDef = { + loadTabStateFromSync, + writeCurrentTabState, +} diff --git a/packages/hoppscotch-selfhost-web/tailwind.config.ts b/packages/hoppscotch-selfhost-web/tailwind.config.ts index eea6fe87..ea807269 100644 --- a/packages/hoppscotch-selfhost-web/tailwind.config.ts +++ b/packages/hoppscotch-selfhost-web/tailwind.config.ts @@ -11,6 +11,7 @@ export default { upperSecondaryStickyFold: "var(--upper-secondary-sticky-fold)", upperTertiaryStickyFold: "var(--upper-tertiary-sticky-fold)", upperFourthStickyFold: "var(--upper-fourth-sticky-fold)", + upperRunnerStickyFold: "var(--upper-runner-sticky-fold)", upperMobilePrimaryStickyFold: "var(--upper-mobile-primary-sticky-fold)", upperMobileSecondaryStickyFold: "var(--upper-mobile-secondary-sticky-fold)", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1b708ae..4cdda24c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: 19.5.0 version: 19.5.0 '@hoppscotch/ui': - specifier: 0.2.1 - version: 0.2.1(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3)) + specifier: 0.2.2 + version: 0.2.2(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3)) '@types/node': specifier: 22.7.6 version: 22.7.6 @@ -499,8 +499,8 @@ importers: specifier: workspace:^ version: link:../hoppscotch-js-sandbox '@hoppscotch/ui': - specifier: 0.2.1 - version: 0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3)) + specifier: 0.2.2 + version: 0.2.2(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3)) '@hoppscotch/vue-toasted': specifier: 0.1.0 version: 0.1.0(vue@3.5.12(typescript@5.3.3)) @@ -904,6 +904,9 @@ importers: parser-ts: specifier: 0.7.0 version: 0.7.0(fp-ts@2.16.9) + uuid: + specifier: 10.0.0 + version: 10.0.0 verzod: specifier: 0.2.2 version: 0.2.2(zod@3.23.8) @@ -914,6 +917,9 @@ importers: '@types/lodash': specifier: 4.17.10 version: 4.17.10 + '@types/uuid': + specifier: 10.0.0 + version: 10.0.0 typescript: specifier: 5.6.3 version: 5.6.3 @@ -3760,6 +3766,12 @@ packages: peerDependencies: vue: 3.5.12 + '@hoppscotch/ui@0.2.2': + resolution: {integrity: sha512-rDRfG9onpmlDCO2KjJZN6UIlFC5Ewif689guvtVCZh9a+soy9nUUTbwMHI9913oBIJpbZ4GTLHGpdCl1YHUiVQ==} + engines: {node: '>=16'} + peerDependencies: + vue: 3.5.12 + '@hoppscotch/vue-sonner@1.2.3': resolution: {integrity: sha512-P1gyvHHLsPeB8lsLP5SrqwQatuwOKtbsP83sKhyIV3WL2rJj3+DiFfqo2ErNBa+Sl0gM68o1V+wuOS7zbR//6g==} @@ -3882,26 +3894,26 @@ packages: resolution: {integrity: sha512-GG428DkrrWCMhxRMRQZjuS7zmSUzarYcaHJqG9VB8dXAxw4iQDoKVQ7ChJRB6ZtsCsX3Jse1PEUlHrJiyQrOTg==} engines: {node: '>= 16'} - '@intlify/message-compiler@10.0.0': - resolution: {integrity: sha512-OcaWc63NC/9p1cMdgoNKBj4d61BH8sUW1Hfs6YijTd9656ZR4rNqXAlRnBrfS5ABq0vjQjpa8VnyvH9hK49yBw==} - engines: {node: '>= 16'} - '@intlify/message-compiler@10.0.4': resolution: {integrity: sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==} engines: {node: '>= 16'} + '@intlify/message-compiler@11.0.0-beta.1': + resolution: {integrity: sha512-yMXfN4hg/EeSdtWfmoMrwB9X4TXwkBoZlTIpNydQaW9y0tSJHGnUPRoahtkbsyACCm9leSJINLY4jQ0rK6BK0Q==} + engines: {node: '>= 16'} + '@intlify/message-compiler@9.3.0-beta.20': resolution: {integrity: sha512-hwqQXyTnDzAVZ300SU31jO0+3OJbpOdfVU6iBkrmNpS7t2HRnVACo0EwcEXzJa++4EVDreqz5OeqJbt+PeSGGA==} engines: {node: '>= 16'} - '@intlify/shared@10.0.0': - resolution: {integrity: sha512-6ngLfI7DOTew2dcF9WMJx+NnMWghMBhIiHbGg+wRvngpzD5KZJZiJVuzMsUQE1a5YebEmtpTEfUrDp/NqVGdiw==} - engines: {node: '>= 16'} - '@intlify/shared@10.0.4': resolution: {integrity: sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==} engines: {node: '>= 16'} + '@intlify/shared@11.0.0-beta.1': + resolution: {integrity: sha512-Md/4T/QOx7wZ7zqVzSsMx2M/9Mx/1nsgsjXS5SFIowFKydqUhMz7K+y7pMFh781aNYz+rGXYwad8E9/+InK9SA==} + engines: {node: '>= 16'} + '@intlify/shared@9.3.0-beta.20': resolution: {integrity: sha512-RucSPqh8O9FFxlYUysQTerSw0b9HIRpyoN1Zjogpm0qLiHK+lBNSa5sh1nCJ4wSsNcjphzgpLQCyR60GZlRV8g==} engines: {node: '>= 16'} @@ -10381,6 +10393,7 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qs@6.11.0: @@ -15394,30 +15407,6 @@ snapshots: stringify-object: 3.3.0 yargs: 17.7.2 - '@hoppscotch/ui@0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))': - dependencies: - '@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3)) - '@fontsource-variable/inter': 5.1.0 - '@fontsource-variable/material-symbols-rounded': 5.1.3 - '@fontsource-variable/roboto-mono': 5.1.0 - '@hoppscotch/vue-sonner': 1.2.3 - '@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.3.3)) - '@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1)) - '@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.3.3)) - fp-ts: 2.16.9 - lodash-es: 4.17.21 - path: 0.12.7 - vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1)) - vue: 3.5.12(typescript@5.3.3) - vue-promise-modals: 0.1.0(typescript@5.3.3) - vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.3.3)) - transitivePeerDependencies: - - '@vue/composition-api' - - eslint - - terser - - typescript - - vite - '@hoppscotch/ui@0.2.1(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))': dependencies: '@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3)) @@ -15490,6 +15479,54 @@ snapshots: - typescript - vite + '@hoppscotch/ui@0.2.2(eslint@8.57.0)(terser@5.34.1)(typescript@5.3.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue@3.5.12(typescript@5.3.3))': + dependencies: + '@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.3.3)) + '@fontsource-variable/inter': 5.0.15 + '@fontsource-variable/material-symbols-rounded': 5.0.16 + '@fontsource-variable/roboto-mono': 5.0.16 + '@hoppscotch/vue-sonner': 1.2.3 + '@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.3.3)) + '@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1)) + '@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.3.3)) + fp-ts: 2.16.9 + lodash-es: 4.17.21 + path: 0.12.7 + vite-plugin-eslint: 1.8.1(eslint@8.57.0)(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1)) + vue: 3.5.12(typescript@5.3.3) + vue-promise-modals: 0.1.0(typescript@5.3.3) + vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.3.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - eslint + - terser + - typescript + - vite + + '@hoppscotch/ui@0.2.2(eslint@9.12.0(jiti@2.3.3))(terser@5.34.1)(typescript@5.6.3)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue@3.5.12(typescript@5.6.3))': + dependencies: + '@boringer-avatars/vue3': 0.2.1(vue@3.5.12(typescript@5.6.3)) + '@fontsource-variable/inter': 5.0.15 + '@fontsource-variable/material-symbols-rounded': 5.0.16 + '@fontsource-variable/roboto-mono': 5.0.16 + '@hoppscotch/vue-sonner': 1.2.3 + '@hoppscotch/vue-toasted': 0.1.0(vue@3.5.12(typescript@5.6.3)) + '@vitejs/plugin-legacy': 2.3.0(terser@5.34.1)(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1)) + '@vueuse/core': 8.9.4(vue@3.5.12(typescript@5.6.3)) + fp-ts: 2.16.9 + lodash-es: 4.17.21 + path: 0.12.7 + vite-plugin-eslint: 1.8.1(eslint@9.12.0(jiti@2.3.3))(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1)) + vue: 3.5.12(typescript@5.6.3) + vue-promise-modals: 0.1.0(typescript@5.6.3) + vuedraggable-es: 4.1.1(vue@3.5.12(typescript@5.6.3)) + transitivePeerDependencies: + - '@vue/composition-api' + - eslint + - terser + - typescript + - vite + '@hoppscotch/vue-sonner@1.2.3': {} '@hoppscotch/vue-toasted@0.1.0(vue@3.5.12(typescript@5.3.3))': @@ -15588,8 +15625,8 @@ snapshots: '@intlify/bundle-utils@3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))': dependencies: - '@intlify/message-compiler': 10.0.0 - '@intlify/shared': 10.0.0 + '@intlify/message-compiler': 11.0.0-beta.1 + '@intlify/shared': 11.0.0-beta.1 jsonc-eslint-parser: 1.4.1 source-map: 0.6.1 yaml-eslint-parser: 0.3.2 @@ -15613,8 +15650,8 @@ snapshots: '@intlify/bundle-utils@9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))': dependencies: - '@intlify/message-compiler': 10.0.0 - '@intlify/shared': 10.0.0 + '@intlify/message-compiler': 11.0.0-beta.1 + '@intlify/shared': 11.0.0-beta.1 acorn: 8.12.1 escodegen: 2.1.0 estree-walker: 2.0.2 @@ -15630,33 +15667,33 @@ snapshots: '@intlify/message-compiler': 10.0.4 '@intlify/shared': 10.0.4 - '@intlify/message-compiler@10.0.0': - dependencies: - '@intlify/shared': 10.0.0 - source-map-js: 1.2.1 - '@intlify/message-compiler@10.0.4': dependencies: '@intlify/shared': 10.0.4 source-map-js: 1.2.1 + '@intlify/message-compiler@11.0.0-beta.1': + dependencies: + '@intlify/shared': 11.0.0-beta.1 + source-map-js: 1.2.1 + '@intlify/message-compiler@9.3.0-beta.20': dependencies: '@intlify/shared': 9.3.0-beta.20 source-map-js: 1.2.1 - '@intlify/shared@10.0.0': {} - '@intlify/shared@10.0.4': {} + '@intlify/shared@11.0.0-beta.1': {} + '@intlify/shared@9.3.0-beta.20': {} '@intlify/unplugin-vue-i18n@5.2.0(@vue/compiler-dom@3.5.12)(eslint@9.12.0(jiti@2.3.3))(rollup@4.24.0)(typescript@5.6.3)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))(webpack-sources@3.2.3)': dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0(jiti@2.3.3)) '@intlify/bundle-utils': 9.0.0-beta.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3))) - '@intlify/shared': 10.0.0 - '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3)) + '@intlify/shared': 11.0.0-beta.1 + '@intlify/vue-i18n-extensions': 7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3)) '@rollup/pluginutils': 5.1.2(rollup@4.24.0) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.6.3) @@ -15682,7 +15719,7 @@ snapshots: '@intlify/vite-plugin-vue-i18n@6.0.1(vite@4.5.0(@types/node@18.18.8)(sass@1.80.3)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@4.9.5)))': dependencies: '@intlify/bundle-utils': 7.0.0(vue-i18n@10.0.4(vue@3.5.12(typescript@4.9.5))) - '@intlify/shared': 10.0.0 + '@intlify/shared': 11.0.0-beta.1 '@rollup/pluginutils': 4.2.1 debug: 4.3.7 fast-glob: 3.3.2 @@ -15696,7 +15733,7 @@ snapshots: '@intlify/vite-plugin-vue-i18n@7.0.0(vite@5.4.9(@types/node@22.7.6)(sass@1.79.5)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))': dependencies: '@intlify/bundle-utils': 3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3))) - '@intlify/shared': 10.0.0 + '@intlify/shared': 11.0.0-beta.1 '@rollup/pluginutils': 4.2.1 debug: 4.3.7 fast-glob: 3.3.2 @@ -15710,7 +15747,7 @@ snapshots: '@intlify/vite-plugin-vue-i18n@7.0.0(vite@5.4.9(@types/node@22.7.6)(sass@1.80.3)(terser@5.34.1))(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3)))': dependencies: '@intlify/bundle-utils': 3.4.0(vue-i18n@10.0.4(vue@3.5.12(typescript@5.3.3))) - '@intlify/shared': 10.0.0 + '@intlify/shared': 11.0.0-beta.1 '@rollup/pluginutils': 4.2.1 debug: 4.3.7 fast-glob: 3.3.2 @@ -15721,11 +15758,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@10.0.0)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))': + '@intlify/vue-i18n-extensions@7.0.0(@intlify/shared@11.0.0-beta.1)(@vue/compiler-dom@3.5.12)(vue-i18n@10.0.4(vue@3.5.12(typescript@5.6.3)))(vue@3.5.12(typescript@5.6.3))': dependencies: '@babel/parser': 7.25.7 optionalDependencies: - '@intlify/shared': 10.0.0 + '@intlify/shared': 11.0.0-beta.1 '@vue/compiler-dom': 3.5.12 vue: 3.5.12(typescript@5.6.3) vue-i18n: 10.0.4(vue@3.5.12(typescript@5.6.3))
- {{ cliCommandGenerationDescription }} -
+ {{ request.endpoint }} +
+ {{ cliCommandGenerationDescription }} +
+ {{ t("environment.no_environment_description") }} +
+ + +