From aa838bf39c79d02ee1841c0771a56488f3fe2959 Mon Sep 17 00:00:00 2001 From: James Gatz Date: Fri, 8 Aug 2025 11:10:28 +0200 Subject: [PATCH] chore(Architecture): react router spa mode (#8902) * first pass fix tests move all react things in dev try-package build stuff use http protocol instead of file handle refresh fix tests and routeloaderdata apths fix npm run dev fix sorts fix hidden browser window cleanup files Typesafe /auth/* routes typesafe commands route git-credentials typesafe routes import typesafe routes fix types fix hidden browser window invite and collaborators typesafe routes fix types remove workarounds fix dashboard test more types git typesafe routes fix runner test typesafe scratchpad navigation fix remove unused project route fix test routes add space request typesafe routes git credentials typescript conspiracy git typesafe routes typecheck debug bundles for inso fix test fix request group tab workspace typesafe routes feedback All routes use generated types Add typed fetchers to actions and loader Use typed fetchers in the app move git actions to the root * fix react-use usage update import source field Spawning npm fails the build Add ~ module resolution to vitest add initialEntry functionality fix update environment name requirement fix settings patch use loader for fetching the vault key and fix process.env.PLAYWRIGHT issue fix missing type Centralize useRouteLoaderData to routes Use environment for vitest tests that run browser code Update remaining fetchers to typesafe versions remove unused fetcher and add callback to sync Wrap load/submit in useCallback to keep them stable between re-renders Update deps lists with stable submit functions fix lint issue * fix ts issues * Add toaster to root * Use shell for running scripts with spawn on Windows * Move renderer bundling out of the build script * Fix request-pane test flakiness * update the url we use for internal purposes * Increase timeout for release workflow * fix flaky bundling test --------- Co-authored-by: jackkav --- .github/workflows/release-publish.yml | 2 +- .github/workflows/release-recurring.yml | 4 +- .vscode/settings.json | 2 +- eslint.config.mjs | 3 + package-lock.json | 1408 ++++++++++++-- package.json | 1 + packages/insomnia-inso/README.md | 10 + packages/insomnia-inso/esbuild.ts | 18 +- packages/insomnia-inso/tsconfig.json | 23 +- .../src/objects/interpolator.ts | 4 +- .../tsconfig.json | 20 +- .../tests/critical/bundling.test.ts | 4 + .../tests/smoke/analytics.test.ts | 96 - .../smoke/dashboard-interactions.test.ts | 4 +- .../smoke/debug-sidebar-interactions.test.ts | 1 - .../smoke/pre-request-script-window.test.ts | 4 +- .../tests/smoke/request-pane-tab.test.ts | 2 - packages/insomnia/.gitignore | 2 +- packages/insomnia/esbuild.main.ts | 30 +- packages/insomnia/package.json | 15 +- packages/insomnia/react-router.config.ts | 7 + packages/insomnia/scripts/build.ts | 19 +- packages/insomnia/src/account/session.ts | 2 +- .../src/common/__tests__/constants.test.ts | 20 +- packages/insomnia/src/common/constants.ts | 28 +- packages/insomnia/src/common/import.ts | 3 +- packages/insomnia/src/entry.client.tsx | 104 + packages/insomnia/src/entry.server.tsx | 65 + packages/insomnia/src/hidden-window.html | 2 +- packages/insomnia/src/index.html | 131 -- packages/insomnia/src/main/api.protocol.ts | 10 + packages/insomnia/src/main/git-service.ts | 171 +- packages/insomnia/src/main/ipc/main.ts | 1 + packages/insomnia/src/main/window-utils.ts | 13 +- .../src/models/__tests__/request.test.ts | 1 + packages/insomnia/src/models/environment.ts | 2 +- .../insomnia/src/models/git-repository.ts | 30 +- packages/insomnia/src/models/organization.ts | 65 + packages/insomnia/src/models/project.ts | 3 +- .../insomnia/src/models/runner-test-result.ts | 3 +- packages/insomnia/src/models/settings.ts | 4 +- packages/insomnia/src/models/workspace.ts | 7 +- packages/insomnia/src/network/network.ts | 5 +- .../src/plugins/context/__tests__/app.test.ts | 2 +- .../src/plugins/context/{app.tsx => app.ts} | 33 +- packages/insomnia/src/root.tsx | 556 ++++++ packages/insomnia/src/routes.ts | 3 + packages/insomnia/src/routes/_index.tsx | 5 + .../src/{ui => }/routes/auth.authorize.tsx | 73 +- .../{ui => }/routes/auth.clear-vault-key.tsx | 38 +- .../src/routes/auth.create-vault-key.tsx | 23 + .../routes/auth.default-browser-redirect.ts | 28 + .../src/{ui => }/routes/auth.login.tsx | 65 +- packages/insomnia/src/routes/auth.logout.tsx | 24 + .../src/routes/auth.reset-vault-key.tsx | 21 + .../insomnia/src/{ui => }/routes/auth.tsx | 20 +- .../src/routes/auth.update-vault-salt.tsx | 35 + .../src/routes/auth.validate-vault-key.tsx | 52 + ...d-credentials.$cloudCredentialId.delete.ts | 41 + ...d-credentials.$cloudCredentialId.update.ts | 66 + .../src/routes/cloud-credentials.create.tsx | 72 + .../insomnia/src/{ui => }/routes/commands.tsx | 91 +- ...it-credentials.github.complete-sign-in.tsx | 30 + .../git-credentials.github.init-sign-in.tsx | 23 + .../git-credentials.github.sign-out.tsx | 23 + .../src/routes/git-credentials.github.tsx | 25 + ...it-credentials.gitlab.complete-sign-in.tsx | 30 + .../git-credentials.gitlab.init-sign-in.tsx | 23 + .../git-credentials.gitlab.sign-out.tsx | 23 + .../src/routes/git-credentials.gitlab.tsx | 25 + .../insomnia/src/routes/git-credentials.tsx | 9 + .../src/routes/git.branch.checkout.tsx | 40 + .../insomnia/src/routes/git.branch.delete.tsx | 39 + .../insomnia/src/routes/git.branch.new.tsx | 40 + packages/insomnia/src/routes/git.branches.tsx | 41 + packages/insomnia/src/routes/git.changes.tsx | 41 + packages/insomnia/src/routes/git.clone.tsx | 57 + packages/insomnia/src/routes/git.commit.tsx | 45 + packages/insomnia/src/routes/git.diff.tsx | 56 + packages/insomnia/src/routes/git.discard.tsx | 36 + packages/insomnia/src/routes/git.fetch.tsx | 36 + .../insomnia/src/routes/git.init-clone.tsx | 50 + packages/insomnia/src/routes/git.log.tsx | 37 + ...migrate-legacy-insomnia-folder-to-file.tsx | 35 + packages/insomnia/src/routes/git.push.tsx | 33 + .../src/routes/git.remote-branches.tsx | 37 + packages/insomnia/src/routes/git.repo.tsx | 35 + .../src/routes/git.repository-tree.tsx | 32 + packages/insomnia/src/routes/git.reset.tsx | 35 + packages/insomnia/src/routes/git.stage.tsx | 35 + packages/insomnia/src/routes/git.status.tsx | 35 + packages/insomnia/src/routes/git.unstage.tsx | 36 + packages/insomnia/src/routes/git.update.tsx | 43 + .../src/{ui => }/routes/import.resources.tsx | 74 +- .../src/{ui => }/routes/import.scan.tsx | 51 +- .../{ui => }/routes/onboarding.migrate.tsx | 33 +- .../src/{ui => }/routes/onboarding.tsx | 15 +- .../organization.$organizationId._index.tsx | 18 + ...n.$organizationId.collaborators-search.tsx | 55 + ...orators.invites.$invitationId.reinvite.tsx | 53 + ...Id.collaborators.invites.$invitationId.tsx | 58 + ...nization.$organizationId.collaborators.tsx | 109 ++ ...ationId.insomnia-sync.pull-remote-file.tsx | 96 + ....$organizationId.members.$userId.roles.tsx | 60 + ...anization.$organizationId.permissions.tsx} | 37 +- ...anizationId.project.$projectId._index.tsx} | 354 +--- ...anizationId.project.$projectId.delete.tsx} | 44 +- ...nId.project.$projectId.list-workspaces.tsx | 157 ++ ...onId.project.$projectId.move-workspace.tsx | 52 + ...organizationId.project.$projectId.move.tsx | 62 + ...ion.$organizationId.project.$projectId.tsx | 22 + ...anizationId.project.$projectId.update.tsx} | 97 +- ...d.workspace.$workspaceId.cacert.delete.tsx | 52 + ...ctId.workspace.$workspaceId.cacert.new.tsx | 48 + ...d.workspace.$workspaceId.cacert.update.tsx | 56 + ...rkspace.$workspaceId.clientcert.delete.tsx | 52 + ....workspace.$workspaceId.clientcert.new.tsx | 50 + ...rkspace.$workspaceId.clientcert.update.tsx | 56 + ...d.workspace.$workspaceId.debug.reorder.tsx | 86 + ...Id.debug.request-group.$requestGroupId.tsx | 35 + ...uest-group.$requestGroupId.update-meta.tsx | 63 + ...g.request-group.$requestGroupId.update.tsx | 63 + ...workspaceId.debug.request-group.delete.tsx | 62 + ...kspaceId.debug.request-group.duplicate.tsx | 76 + ...e.$workspaceId.debug.request-group.new.tsx | 68 + ...aceId.debug.request.$requestId.connect.tsx | 147 ++ ...eId.debug.request.$requestId.duplicate.tsx | 87 + ...request.$requestId.response.delete-all.tsx | 72 + ...bug.request.$requestId.response.delete.tsx | 95 + ...kspaceId.debug.request.$requestId.send.tsx | 408 ++++ ....$workspaceId.debug.request.$requestId.tsx | 168 ++ ...d.debug.request.$requestId.update-meta.tsx | 64 + ...ebug.request.$requestId.update-payload.tsx | 59 + ...paceId.debug.request.$requestId.update.tsx | 88 + ...pace.$workspaceId.debug.request.delete.tsx | 72 + ...orkspaceId.debug.request.new-mock-send.tsx | 99 + ...rkspace.$workspaceId.debug.request.new.tsx | 170 ++ ...d.workspace.$workspaceId.debug.runner.tsx} | 111 +- ...rojectId.workspace.$workspaceId.debug.tsx} | 319 ++-- ...kspace.$workspaceId.environment.create.tsx | 60 + ...kspace.$workspaceId.environment.delete.tsx | 63 + ...ace.$workspaceId.environment.duplicate.tsx | 63 + ...kspaceId.environment.set-active-global.tsx | 64 + ...ce.$workspaceId.environment.set-active.tsx | 64 + ...Id.workspace.$workspaceId.environment.tsx} | 307 ++- ...kspace.$workspaceId.environment.update.tsx | 65 + ...kspaceId.insomnia-sync.branch.checkout.tsx | 81 + ...orkspaceId.insomnia-sync.branch.create.tsx | 67 + ...orkspaceId.insomnia-sync.branch.delete.tsx | 80 + ...workspaceId.insomnia-sync.branch.merge.tsx | 79 + ...kspaceId.insomnia-sync.create-snapshot.tsx | 87 + ...space.$workspaceId.insomnia-sync.fetch.tsx | 81 + ...kspace.$workspaceId.insomnia-sync.pull.tsx | 87 + ...kspace.$workspaceId.insomnia-sync.push.tsx | 82 + ...ace.$workspaceId.insomnia-sync.restore.tsx | 74 + ...ce.$workspaceId.insomnia-sync.rollback.tsx | 73 + ...space.$workspaceId.insomnia-sync.stage.tsx | 69 + ...e.$workspaceId.insomnia-sync.sync-data.tsx | 182 ++ ...d.workspace.$workspaceId.insomnia-sync.tsx | 91 + ...ace.$workspaceId.insomnia-sync.unstage.tsx | 74 + ...-server.mock-route.$mockRouteId.delete.tsx | 69 + ...d.mock-server.mock-route.$mockRouteId.tsx} | 116 +- ...-server.mock-route.$mockRouteId.update.tsx | 63 + ...workspaceId.mock-server.mock-route.new.tsx | 98 + ...Id.workspace.$workspaceId.mock-server.tsx} | 136 +- ...aceId.spec.generate-request-collection.tsx | 102 + ...projectId.workspace.$workspaceId.spec.tsx} | 155 +- ...tId.workspace.$workspaceId.spec.update.tsx | 76 + ...tId.workspace.$workspaceId.test._index.tsx | 39 + ...Id.test.test-suite.$testSuiteId._index.tsx | 28 + ...Id.test.test-suite.$testSuiteId.delete.tsx | 70 + ....test-suite.$testSuiteId.run-all-tests.tsx | 153 ++ ...testSuiteId.test-result.$testResultId.tsx} | 52 +- ...-suite.$testSuiteId.test-result._index.tsx | 21 + ...suite.$testSuiteId.test.$testId.delete.tsx | 69 + ...st-suite.$testSuiteId.test.$testId.run.tsx | 157 ++ ...suite.$testSuiteId.test.$testId.update.tsx | 68 + ....test.test-suite.$testSuiteId.test.new.tsx | 72 + ...kspaceId.test.test-suite.$testSuiteId.tsx} | 247 ++- ...Id.test.test-suite.$testSuiteId.update.tsx | 67 + ...space.$workspaceId.test.test-suite.new.tsx | 70 + ...projectId.workspace.$workspaceId.test.tsx} | 194 +- ...rkspace.$workspaceId.toggle-expand-all.tsx | 75 + ...ect.$projectId.workspace.$workspaceId.tsx} | 107 +- ...rkspace.$workspaceId.update-cookie-jar.tsx | 59 + ...tId.workspace.$workspaceId.update-meta.tsx | 51 + ...d.project.$projectId.workspace.delete.tsx} | 60 +- ...onId.project.$projectId.workspace.move.tsx | 110 ++ ...ionId.project.$projectId.workspace.new.tsx | 212 +++ ...d.project.$projectId.workspace.update.tsx} | 48 +- ...ization.$organizationId.project._index.tsx | 1351 +++++++++++++ ...anization.$organizationId.project.new.tsx} | 95 +- ...nization.$organizationId.storage-rules.tsx | 68 + ...nization.$organizationId.sync-projects.tsx | 38 + .../{ui => }/routes/organization._index.tsx | 18 +- ...zation.sync-organizations-and-projects.tsx | 67 +- .../insomnia/src/routes/organization.sync.tsx | 36 + .../src/{ui => }/routes/organization.tsx | 132 +- .../src/{ui => }/routes/remote-files.tsx | 36 +- .../insomnia/src/routes/settings.update.tsx | 37 + .../{ui => }/routes/untracked-projects.tsx | 32 +- packages/insomnia/src/scriptExecutor.ts | 2 +- packages/insomnia/src/sync/git/git-vcs.ts | 34 +- packages/insomnia/src/sync/git/utils.ts | 3 +- packages/insomnia/src/templating/index.ts | 2 +- .../src/templating/nunjucks.client.ts | 3 + packages/insomnia/src/templating/types.ts | 19 +- packages/insomnia/src/templating/worker.ts | 5 +- ...der.ts => auth-session-provider.client.ts} | 0 .../{ => .client}/codemirror/base-imports.ts | 0 .../{ => .client}/codemirror/code-editor.tsx | 70 +- .../codemirror/extensions/autocomplete.ts | 9 +- .../codemirror/extensions/clickable.ts | 2 +- .../codemirror/extensions/nunjucks-tags.ts | 15 +- .../codemirror/lint/javascript-async-lint.ts | 0 .../codemirror/lint/json-lint.ts | 2 +- .../{ => .client}/codemirror/modes/clojure.ts | 0 .../{ => .client}/codemirror/modes/curl.ts | 0 .../codemirror/modes/nunjucks.ts | 0 .../{ => .client}/codemirror/modes/openapi.ts | 1 + .../normalizeIrregularWhitespace.ts | 0 .../codemirror/one-line-editor.tsx | 39 +- .../ui/components/app-loading-indicator.tsx | 2 - .../src/ui/components/base/copy-button.tsx | 4 +- .../base/dropdown/dropdown-hint.tsx | 2 - .../ui/components/base/dropdown/menu-item.tsx | 1 - .../components/base/dropdown/menu-section.tsx | 1 - .../src/ui/components/command-palette.tsx | 116 +- .../src/ui/components/diff-view-editor.tsx | 8 +- .../src/ui/components/document-tab.tsx | 1 - .../dropdowns/cloud-sync-project-bar.tsx | 1 - .../dropdowns/content-type-dropdown.tsx | 9 +- .../dropdowns/git-project-sync-dropdown.tsx | 181 +- .../dropdowns/git-sync-dropdown.tsx | 165 +- .../dropdowns/local-project-bar.tsx | 1 - .../dropdowns/preview-mode-dropdown.tsx | 10 +- .../components/dropdowns/project-dropdown.tsx | 28 +- .../dropdowns/request-actions-dropdown.tsx | 40 +- .../request-group-actions-dropdown.tsx | 69 +- .../dropdowns/response-history-dropdown.tsx | 65 +- .../ui/components/dropdowns/sync-dropdown.tsx | 154 +- .../dropdowns/workspace-card-dropdown.tsx | 28 +- .../dropdowns/workspace-dropdown.tsx | 36 +- .../dropdowns/workspace-sync-dropdown.tsx | 12 +- .../__tests__/environment-editor.test.ts | 1 + .../auth/components/auth-accordion.tsx | 12 +- .../auth/components/auth-input-row.tsx | 28 +- .../auth/components/auth-private-key-row.tsx | 28 +- .../editors/auth/components/auth-row.tsx | 19 +- .../auth/components/auth-select-row.tsx | 24 +- .../auth/components/auth-toggle-row.tsx | 22 +- .../components/editors/auth/o-auth-1-auth.tsx | 15 +- .../components/editors/auth/o-auth-2-auth.tsx | 23 +- .../editors/body/graph-ql-editor.tsx | 7 +- .../ui/components/editors/body/raw-editor.tsx | 2 +- .../components/editors/environment-editor.tsx | 3 +- .../key-value-editor.tsx | 3 +- .../password-input.tsx | 2 +- .../components/editors/environment-utils.tsx | 2 - .../editors/mock-response-extractor.tsx | 71 +- .../editors/mock-response-headers-editor.tsx | 15 +- .../editors/request-headers-editor.tsx | 3 +- .../editors/request-parameters-editor.tsx | 19 +- .../editors/request-script-editor.tsx | 3 +- .../src/ui/components/encoding-picker.tsx | 1 - .../src/ui/components/environment-picker.tsx | 58 +- .../git-remote-branch-select.tsx | 42 +- .../github-repository-settings-form.tsx | 33 +- .../gitlab-repository-settings-form.tsx | 33 +- .../ui/components/github-app-config-link.tsx | 2 - .../src/ui/components/github-stars-button.tsx | 6 +- .../src/ui/components/header-user-button.tsx | 21 +- packages/insomnia/src/ui/components/icon.tsx | 1 - .../src/ui/components/insomnia-icon.tsx | 2 - .../key-value-editor/key-value-editor.tsx | 3 +- .../ui/components/key-value-editor/row.tsx | 3 +- .../src/ui/components/keydown-binder.ts | 6 +- .../src/ui/components/markdown-editor.tsx | 3 +- .../components/mocks/mock-response-pane.tsx | 22 +- .../src/ui/components/mocks/mock-url-bar.tsx | 22 +- .../add-request-to-collection-modal.tsx | 34 +- .../components/modals/cli-preview-modal.tsx | 7 +- .../cloud-credential-modal.tsx | 62 +- .../components/modals/code-prompt-modal.tsx | 3 +- .../ui/components/modals/cookies-modal.tsx | 27 +- .../modals/export-requests-modal.tsx | 19 +- .../components/modals/generate-code-modal.tsx | 3 +- .../components/modals/git-branches-modal.tsx | 187 +- .../ui/components/modals/git-log-modal.tsx | 13 +- .../modals/git-project-branches-modal.tsx | 203 +- .../modals/git-project-log-modal.tsx | 12 +- .../modals/git-project-migration-modal.tsx | 23 +- .../modals/git-project-staging-modal.tsx | 148 +- .../git-project-repo-clone-modal.tsx | 30 +- .../git-project-repository-settings-modal.tsx | 46 +- .../git-repo-clone-modal.tsx | 31 +- .../git-repository-settings-modal.tsx | 49 +- .../components/modals/git-staging-modal.tsx | 153 +- .../modals/import-modal/import-modal.tsx | 61 +- .../import-modal/import-projects-modal.tsx | 33 +- .../components/modals/import-modal/shared.tsx | 2 +- .../modals/input-vault-key-modal.tsx | 44 +- .../modals/invite-modal/invite-form.tsx | 37 +- .../modals/invite-modal/invite-modal.tsx | 101 +- .../organization-member-roles-selector.tsx | 1 - .../components/modals/new-workspace-modal.tsx | 23 +- .../ui/components/modals/paste-curl-modal.tsx | 3 +- .../ui/components/modals/project-modal.tsx | 3 +- .../modals/request-group-settings-modal.tsx | 29 +- .../modals/request-settings-modal.tsx | 28 +- .../ui/components/modals/settings-modal.tsx | 5 +- .../components/modals/sync-branches-modal.tsx | 105 +- .../components/modals/sync-delete-modal.tsx | 5 +- .../components/modals/sync-history-modal.tsx | 26 +- .../components/modals/sync-staging-modal.tsx | 91 +- .../ui/components/modals/upgrade-modal.tsx | 3 +- .../modals/variable-missing-error-modal.tsx | 1 - .../modals/workspace-certificates-modal.tsx | 115 +- .../modals/workspace-duplicate-modal.tsx | 24 +- .../workspace-environments-edit-modal.tsx | 229 +-- .../modals/workspace-settings-modal.tsx | 37 +- .../src/ui/components/monaco.client.ts | 3 + .../ui/components/panes/grpc-request-pane.tsx | 28 +- .../components/panes/grpc-response-pane.tsx | 5 +- .../ui/components/panes/no-project-view.tsx | 3 +- .../panes/placeholder-request-pane.tsx | 25 +- .../panes/placeholder-response-pane.tsx | 5 +- .../components/panes/request-group-pane.tsx | 9 +- .../src/ui/components/panes/request-pane.tsx | 20 +- .../src/ui/components/panes/response-pane.tsx | 15 +- .../src/ui/components/present-users.tsx | 16 +- .../project/project-settings-form.tsx | 133 +- .../src/ui/components/request-url-bar.tsx | 76 +- .../components/settings/boolean-setting.tsx | 5 +- .../settings/cloud-service-credentials.tsx | 19 +- .../ui/components/settings/enum-setting.tsx | 5 +- .../src/ui/components/settings/general.tsx | 5 +- .../ui/components/settings/import-export.tsx | 52 +- .../ui/components/settings/masked-setting.tsx | 9 +- .../ui/components/settings/number-setting.tsx | 5 +- .../src/ui/components/settings/plugins.tsx | 5 +- .../src/ui/components/settings/shortcuts.tsx | 5 +- .../ui/components/settings/text-setting.tsx | 5 +- .../components/settings/vault-key-panel.tsx | 56 +- .../ui/components/socket-io/body-tab-pane.tsx | 3 +- .../ui/components/socket-io/event-view.tsx | 8 +- .../ui/components/socket-io/request-pane.tsx | 22 +- .../src/ui/components/tabs/tab-list.tsx | 21 +- .../src/ui/components/tags/grpc-tag.tsx | 2 - .../src/ui/components/tags/websocket-tag.tsx | 2 - .../external-vault/hashicorp-vault-form.tsx | 9 +- .../ui/components/templating/tag-editor.tsx | 4 +- .../ui/components/themed-button/button.tsx | 1 - .../src/ui/components/time-from-now.tsx | 4 +- packages/insomnia/src/ui/components/toast.tsx | 5 +- .../ui/components/trail-lines-container.tsx | 1 - .../src/ui/components/upgrade-notice.tsx | 4 +- .../ui/components/viewers/password-viewer.tsx | 4 +- .../viewers/response-error-viewer.tsx | 5 +- .../viewers/response-pdf-viewer.tsx | 2 - .../viewers/response-timeline-viewer.tsx | 3 +- .../ui/components/viewers/response-viewer.tsx | 3 +- .../ui/components/websockets/action-bar.tsx | 24 +- .../ui/components/websockets/event-view.tsx | 11 +- .../websockets/realtime-response-pane.tsx | 12 +- .../websockets/websocket-request-pane.tsx | 22 +- .../app/insomnia-event-stream-context.tsx | 118 +- .../ui/context/app/insomnia-tab-context.tsx | 4 +- .../src/ui/context/app/runner-context.tsx | 3 +- .../src/ui/context/nunjucks/use-nunjucks.ts | 18 +- packages/insomnia/src/ui/hooks/theme.ts | 10 +- .../src/ui/hooks/use-document-title.ts | 19 +- .../src/ui/hooks/use-editor-refresh.ts | 10 +- .../ui/hooks/use-global-keyboard-shortcuts.ts | 5 +- .../ui/hooks/use-organization-features.tsx | 16 +- packages/insomnia/src/ui/hooks/use-plan.tsx | 31 +- .../hooks/use-realtime-connection-events.ts | 4 +- packages/insomnia/src/ui/hooks/use-request.ts | 113 +- .../src/ui/hooks/use-runner-request-list.tsx | 15 +- .../src/ui/hooks/use-safe-reducer-dispatch.ts | 17 - .../insomnia/src/ui/hooks/use-safe-state.ts | 4 +- .../src/ui/hooks/use-settings-side-effects.ts | 4 +- .../insomnia/src/ui/hooks/use-theme-change.ts | 5 +- .../insomnia/src/ui/hooks/use-vcs-version.ts | 16 +- packages/insomnia/src/ui/index.tsx | 1685 ----------------- .../insomnia/src/ui/{routes => }/modals.tsx | 44 +- .../insomnia/src/ui/organization-utils.ts | 180 +- .../src/ui/routes/$organizationId._index.tsx | 42 - .../$organizationId.collaborators-search.tsx | 69 - ...orators.invites.$invitationId.reinvite.tsx | 27 - ...Id.collaborators.invites.$invitationId.tsx | 33 - .../routes/$organizationId.collaborators.tsx | 75 - .../src/ui/routes/$organizationId.git.tsx | 92 - .../$organizationId.members.$userId.roles.tsx | 33 - ...$organizationId.project.$projectId.git.tsx | 473 ----- ...onId.project.$projectId.move-workspace.tsx | 27 - ...organizationId.project.$projectId.move.tsx | 25 - ....project.$projectId.remote-collections.tsx | 741 -------- ...d.workspace.$workspaceId.cacert.delete.tsx | 13 - ...ctId.workspace.$workspaceId.cacert.new.tsx | 9 - ...d.workspace.$workspaceId.cacert.update.tsx | 12 - ...rkspace.$workspaceId.clientcert.delete.tsx | 12 - ....workspace.$workspaceId.clientcert.new.tsx | 11 - ...rkspace.$workspaceId.clientcert.update.tsx | 12 - ...d.workspace.$workspaceId.debug.reorder.tsx | 46 - ...Id.debug.request-group.$requestGroupId.tsx | 91 - ....$workspaceId.debug.request.$requestId.tsx | 941 --------- ...kspace.$workspaceId.environment.create.tsx | 24 - ...kspace.$workspaceId.environment.delete.tsx | 26 - ...ace.$workspaceId.environment.duplicate.tsx | 22 - ...kspaceId.environment.set-active-global.tsx | 23 - ...ce.$workspaceId.environment.set-active.tsx | 23 - ...kspace.$workspaceId.environment.update.tsx | 26 - ....$projectId.workspace.$workspaceId.git.tsx | 474 ----- ...-server.mock-route.$mockRouteId.delete.tsx | 18 - ...-server.mock-route.$mockRouteId.update.tsx | 16 - ...workspaceId.mock-server.mock-route.new.tsx | 38 - ...aceId.spec.generate-request-collection.tsx | 57 - ...tId.workspace.$workspaceId.spec.update.tsx | 30 - ...spaceId.test-suite.$testSuiteId.delete.tsx | 22 - ....test-suite.$testSuiteId.run-all-tests.tsx | 91 - ...suite.$testSuiteId.test.$testId.delete.tsx | 23 - ...st-suite.$testSuiteId.test.$testId.run.tsx | 90 - ...suite.$testSuiteId.test.$testId.update.tsx | 20 - ...aceId.test-suite.$testSuiteId.test.new.tsx | 25 - ...spaceId.test-suite.$testSuiteId.update.tsx | 25 - ....workspace.$workspaceId.test-suite.new.tsx | 24 - ...rkspace.$workspaceId.toggle-expand-all.tsx | 36 - ...rkspace.$workspaceId.update-cookie-jar.tsx | 21 - ...tId.workspace.$workspaceId.update-meta.tsx | 13 - ...onId.project.$projectId.workspace.move.tsx | 57 - ...ionId.project.$projectId.workspace.new.tsx | 164 -- .../src/ui/routes/$organizationId.project.tsx | 18 - .../routes/$organizationId.storage-rules.tsx | 20 - .../src/ui/routes/auth.create-vault-key.tsx | 7 - .../insomnia/src/ui/routes/auth.logout.tsx | 8 - .../src/ui/routes/auth.reset-vault-key.tsx | 7 - .../src/ui/routes/auth.systemBrowserOAuth.ts | 10 - .../src/ui/routes/auth.update-vault-salt.tsx | 21 - .../src/ui/routes/auth.validate-vault-key.tsx | 27 - .../src/ui/routes/cloud-credentials-action.ts | 90 - packages/insomnia/src/ui/routes/error.tsx | 82 - .../src/ui/routes/git-credentials.tsx | 65 - .../src/ui/routes/organization.sync.tsx | 14 - packages/insomnia/src/ui/routes/root.tsx | 354 ---- .../src/ui/routes/settings.update.tsx | 13 - packages/insomnia/src/ui/sync-utils.ts | 136 ++ .../ui/{vault-key.ts => vault-key.client.ts} | 4 +- packages/insomnia/src/utils/router.ts | 46 +- packages/insomnia/src/utils/vault.ts | 16 +- packages/insomnia/tsconfig.json | 62 +- .../vite-plugin-electron-node-require.ts | 24 +- packages/insomnia/vite.config.ts | 33 +- packages/insomnia/vitest.config.ts | 6 +- patches/tinykeys+3.0.0.patch | 14 + 455 files changed, 16127 insertions(+), 11382 deletions(-) delete mode 100644 packages/insomnia-smoke-test/tests/smoke/analytics.test.ts create mode 100644 packages/insomnia/react-router.config.ts create mode 100644 packages/insomnia/src/entry.client.tsx create mode 100644 packages/insomnia/src/entry.server.tsx delete mode 100644 packages/insomnia/src/index.html rename packages/insomnia/src/plugins/context/{app.tsx => app.ts} (75%) create mode 100644 packages/insomnia/src/root.tsx create mode 100644 packages/insomnia/src/routes.ts create mode 100644 packages/insomnia/src/routes/_index.tsx rename packages/insomnia/src/{ui => }/routes/auth.authorize.tsx (77%) rename packages/insomnia/src/{ui => }/routes/auth.clear-vault-key.tsx (50%) create mode 100644 packages/insomnia/src/routes/auth.create-vault-key.tsx create mode 100644 packages/insomnia/src/routes/auth.default-browser-redirect.ts rename packages/insomnia/src/{ui => }/routes/auth.login.tsx (82%) create mode 100644 packages/insomnia/src/routes/auth.logout.tsx create mode 100644 packages/insomnia/src/routes/auth.reset-vault-key.tsx rename packages/insomnia/src/{ui => }/routes/auth.tsx (90%) create mode 100644 packages/insomnia/src/routes/auth.update-vault-salt.tsx create mode 100644 packages/insomnia/src/routes/auth.validate-vault-key.tsx create mode 100644 packages/insomnia/src/routes/cloud-credentials.$cloudCredentialId.delete.ts create mode 100644 packages/insomnia/src/routes/cloud-credentials.$cloudCredentialId.update.ts create mode 100644 packages/insomnia/src/routes/cloud-credentials.create.tsx rename packages/insomnia/src/{ui => }/routes/commands.tsx (85%) create mode 100644 packages/insomnia/src/routes/git-credentials.github.complete-sign-in.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.github.init-sign-in.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.github.sign-out.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.github.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.gitlab.complete-sign-in.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.gitlab.init-sign-in.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.gitlab.sign-out.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.gitlab.tsx create mode 100644 packages/insomnia/src/routes/git-credentials.tsx create mode 100644 packages/insomnia/src/routes/git.branch.checkout.tsx create mode 100644 packages/insomnia/src/routes/git.branch.delete.tsx create mode 100644 packages/insomnia/src/routes/git.branch.new.tsx create mode 100644 packages/insomnia/src/routes/git.branches.tsx create mode 100644 packages/insomnia/src/routes/git.changes.tsx create mode 100644 packages/insomnia/src/routes/git.clone.tsx create mode 100644 packages/insomnia/src/routes/git.commit.tsx create mode 100644 packages/insomnia/src/routes/git.diff.tsx create mode 100644 packages/insomnia/src/routes/git.discard.tsx create mode 100644 packages/insomnia/src/routes/git.fetch.tsx create mode 100644 packages/insomnia/src/routes/git.init-clone.tsx create mode 100644 packages/insomnia/src/routes/git.log.tsx create mode 100644 packages/insomnia/src/routes/git.migrate-legacy-insomnia-folder-to-file.tsx create mode 100644 packages/insomnia/src/routes/git.push.tsx create mode 100644 packages/insomnia/src/routes/git.remote-branches.tsx create mode 100644 packages/insomnia/src/routes/git.repo.tsx create mode 100644 packages/insomnia/src/routes/git.repository-tree.tsx create mode 100644 packages/insomnia/src/routes/git.reset.tsx create mode 100644 packages/insomnia/src/routes/git.stage.tsx create mode 100644 packages/insomnia/src/routes/git.status.tsx create mode 100644 packages/insomnia/src/routes/git.unstage.tsx create mode 100644 packages/insomnia/src/routes/git.update.tsx rename packages/insomnia/src/{ui => }/routes/import.resources.tsx (58%) rename packages/insomnia/src/{ui => }/routes/import.scan.tsx (76%) rename packages/insomnia/src/{ui => }/routes/onboarding.migrate.tsx (85%) rename packages/insomnia/src/{ui => }/routes/onboarding.tsx (94%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId._index.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.collaborators-search.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.collaborators.invites.$invitationId.reinvite.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.collaborators.invites.$invitationId.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.collaborators.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.insomnia-sync.pull-remote-file.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.members.$userId.roles.tsx rename packages/insomnia/src/{ui/routes/$organizationId.permissions.tsx => routes/organization.$organizationId.permissions.tsx} (61%) rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.tsx => routes/organization.$organizationId.project.$projectId._index.tsx} (84%) rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.delete.tsx => routes/organization.$organizationId.project.$projectId.delete.tsx} (63%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.list-workspaces.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move-workspace.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.update.tsx => routes/organization.$organizationId.project.$projectId.update.tsx} (74%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.new.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update-meta.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.duplicate.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-meta.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-payload.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx} (92%) rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx} (85%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx} (72%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.checkout.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.create.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.create-snapshot.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.fetch.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.pull.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.push.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.rollback.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx} (80%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx} (81%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx} (91%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test._index.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId._index.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test-result.$testResultId.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId.tsx} (63%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result._index.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.tsx} (74%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.update.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.new.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.unit-test.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.tsx} (77%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.toggle-expand-all.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.tsx => routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.tsx} (79%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.update-cookie-jar.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.update-meta.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.delete.tsx => routes/organization.$organizationId.project.$projectId.workspace.delete.tsx} (53%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.move.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.new.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.$projectId.workspace.update.tsx => routes/organization.$organizationId.project.$projectId.workspace.update.tsx} (60%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.project._index.tsx rename packages/insomnia/src/{ui/routes/$organizationId.project.new.tsx => routes/organization.$organizationId.project.new.tsx} (57%) create mode 100644 packages/insomnia/src/routes/organization.$organizationId.storage-rules.tsx create mode 100644 packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx rename packages/insomnia/src/{ui => }/routes/organization._index.tsx (75%) rename packages/insomnia/src/{ui => }/routes/organization.sync-organizations-and-projects.tsx (53%) create mode 100644 packages/insomnia/src/routes/organization.sync.tsx rename packages/insomnia/src/{ui => }/routes/organization.tsx (89%) rename packages/insomnia/src/{ui => }/routes/remote-files.tsx (68%) create mode 100644 packages/insomnia/src/routes/settings.update.tsx rename packages/insomnia/src/{ui => }/routes/untracked-projects.tsx (53%) create mode 100644 packages/insomnia/src/templating/nunjucks.client.ts rename packages/insomnia/src/ui/{auth-session-provider.ts => auth-session-provider.client.ts} (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/base-imports.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/code-editor.tsx (94%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/extensions/autocomplete.ts (98%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/extensions/clickable.ts (95%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/extensions/nunjucks-tags.ts (95%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/lint/javascript-async-lint.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/lint/json-lint.ts (95%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/modes/clojure.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/modes/curl.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/modes/nunjucks.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/modes/openapi.ts (99%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/normalizeIrregularWhitespace.ts (100%) rename packages/insomnia/src/ui/components/{ => .client}/codemirror/one-line-editor.tsx (93%) create mode 100644 packages/insomnia/src/ui/components/monaco.client.ts delete mode 100644 packages/insomnia/src/ui/hooks/use-safe-reducer-dispatch.ts delete mode 100644 packages/insomnia/src/ui/index.tsx rename packages/insomnia/src/ui/{routes => }/modals.tsx (53%) delete mode 100644 packages/insomnia/src/ui/routes/$organizationId._index.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.collaborators-search.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.collaborators.invites.$invitationId.reinvite.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.collaborators.invites.$invitationId.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.collaborators.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.git.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.members.$userId.roles.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.git.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.move-workspace.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.move.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.remote-collections.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.cacert.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.cacert.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.create.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.git.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.run-all-tests.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test.$testId.delete.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test.$testId.run.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test.$testId.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.update.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.toggle-expand-all.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.update-cookie-jar.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.update-meta.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.move.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.new.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.project.tsx delete mode 100644 packages/insomnia/src/ui/routes/$organizationId.storage-rules.tsx delete mode 100644 packages/insomnia/src/ui/routes/auth.create-vault-key.tsx delete mode 100644 packages/insomnia/src/ui/routes/auth.logout.tsx delete mode 100644 packages/insomnia/src/ui/routes/auth.reset-vault-key.tsx delete mode 100644 packages/insomnia/src/ui/routes/auth.systemBrowserOAuth.ts delete mode 100644 packages/insomnia/src/ui/routes/auth.update-vault-salt.tsx delete mode 100644 packages/insomnia/src/ui/routes/auth.validate-vault-key.tsx delete mode 100644 packages/insomnia/src/ui/routes/cloud-credentials-action.ts delete mode 100644 packages/insomnia/src/ui/routes/error.tsx delete mode 100644 packages/insomnia/src/ui/routes/git-credentials.tsx delete mode 100644 packages/insomnia/src/ui/routes/organization.sync.tsx delete mode 100644 packages/insomnia/src/ui/routes/root.tsx delete mode 100644 packages/insomnia/src/ui/routes/settings.update.tsx create mode 100644 packages/insomnia/src/ui/sync-utils.ts rename packages/insomnia/src/ui/{vault-key.ts => vault-key.client.ts} (98%) create mode 100644 patches/tinykeys+3.0.0.patch diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 623d08fa8f..5d107f50ad 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -20,7 +20,7 @@ env: jobs: publish: - timeout-minutes: 15 + timeout-minutes: 30 runs-on: ubuntu-22.04 outputs: NOTARY_REPOSITORY: ${{ env.NOTARY_REPOSITORY }} diff --git a/.github/workflows/release-recurring.yml b/.github/workflows/release-recurring.yml index 97d4f82d68..0fda957129 100644 --- a/.github/workflows/release-recurring.yml +++ b/.github/workflows/release-recurring.yml @@ -17,7 +17,7 @@ env: PR_NUMBER: ${{ github.event.number }} jobs: build-and-upload-artifacts: - timeout-minutes: 15 + timeout-minutes: 30 # Skip jobs for release PRs # windows on recurring should be portable if: ${{ !startsWith(github.head_ref, 'release/') }} @@ -62,7 +62,7 @@ jobs: shell: bash run: NODE_OPTIONS='--max_old_space_size=6144' BUILD_TARGETS='${{ matrix.build-targets }}' npm run app-package - - name: Verify secure wrapper (Windows) + - name: Build and verify secure wrapper (Windows) if: ${{ matrix.os == 'windows-latest' }} shell: bash run: NODE_OPTIONS='--max_old_space_size=6144' ./build-secure-wrapper.sh diff --git a/.vscode/settings.json b/.vscode/settings.json index 96d0f3f7e7..787793f327 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,5 @@ { - "json.schemas": [], + "typescript.preferences.importModuleSpecifier": "non-relative", "files.associations": { "*.db": "ndjson", "*.jsonl": "ndjson", diff --git a/eslint.config.mjs b/eslint.config.mjs index 1291c644d6..b5cc0ce0a9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -157,6 +157,7 @@ export default tseslint.config( '**/*.config.js', '**/*.d.ts', '**/*.min.js', + '**/*.js.map', '**/bin/*', '**/build/*', '**/coverage/*', @@ -165,6 +166,7 @@ export default tseslint.config( '**/docker/*', '**/electron/index.js', '**/fixtures', + '**/hidden-window.js', '**/hidden-window-preload.js', '**/node_modules/*', '**/preload.js', @@ -172,6 +174,7 @@ export default tseslint.config( '**/traces/*', '**/verify-pkg.js', '**/__mocks__/*', + '**/.react-router/*', ], }, ); diff --git a/package-lock.json b/package-lock.json index a1c65422ff..9cba4ff660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^59.0.1", + "globals": "^16.3.0", "patch-package": "^8.0.0", "prettier": "3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", @@ -1272,23 +1273,23 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -1296,22 +1297,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -1337,31 +1338,44 @@ } }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", - "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-validator-option": "^7.25.9", + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -1380,30 +1394,20 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", + "semver": "^6.3.1" }, "engines": { "node": ">=6.9.0" @@ -1412,20 +1416,131 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -1433,18 +1548,18 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -1452,27 +1567,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1481,6 +1596,55 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", @@ -1513,6 +1677,46 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", @@ -1527,48 +1731,48 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", + "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3611,18 +3815,14 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3635,16 +3835,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", @@ -3653,9 +3843,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3937,6 +4127,13 @@ "license": "MIT", "peer": true }, + "node_modules/@mjackson/node-fetch-server": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "dev": true, + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4038,6 +4235,62 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@npmcli/move-file": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz", @@ -4066,6 +4319,154 @@ "node": ">=10" } }, + "node_modules/@npmcli/package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^4.1.0", + "glob": "^10.2.2", + "hosted-git-info": "^6.1.1", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "proc-log": "^3.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/package-json/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", @@ -5795,6 +6196,298 @@ "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-router/dev": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.7.0.tgz", + "integrity": "sha512-z6tJ0US20pS/YpaPz59SJgSH+1BJ6xvQmQ/u4Y4HM1uLOa4b3Mleg3KujqAvwGP5wkMkNFz3Ae2g6/kDTFyuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.7", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/preset-typescript": "^7.27.1", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", + "@npmcli/package-json": "^4.0.1", + "@react-router/node": "7.7.0", + "arg": "^5.0.1", + "babel-dead-code-elimination": "^1.0.6", + "chokidar": "^4.0.0", + "dedent": "^1.5.3", + "es-module-lexer": "^1.3.1", + "exit-hook": "2.2.1", + "isbot": "^5.1.11", + "jsesc": "3.0.2", + "lodash": "^4.17.21", + "pathe": "^1.1.2", + "picocolors": "^1.1.1", + "prettier": "^2.7.1", + "react-refresh": "^0.14.0", + "semver": "^7.3.7", + "set-cookie-parser": "^2.6.0", + "tinyglobby": "^0.2.14", + "valibot": "^0.41.0", + "vite-node": "^3.2.2" + }, + "bin": { + "react-router": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/serve": "^7.7.0", + "react-router": "^7.7.0", + "typescript": "^5.1.0", + "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", + "wrangler": "^3.28.2 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@react-router/serve": { + "optional": true + }, + "typescript": { + "optional": true + }, + "wrangler": { + "optional": true + } + } + }, + "node_modules/@react-router/dev/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@react-router/dev/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@react-router/dev/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-router/dev/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-router/dev/node_modules/prettier": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/@react-router/dev/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@react-router/dev/node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@react-router/dev/node_modules/vite-node/node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-router/express": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.7.0.tgz", + "integrity": "sha512-R86v1qAbj3i/tG00gFM90P3nXR+B66qkp3bbaqm9VnTkbkqUCcHnVaQn64qBOl5g34FdJUMt84UsLS6v2mT/iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-router/node": "7.7.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "express": "^4.17.1 || ^5", + "react-router": "7.7.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/fs-routes": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-router/fs-routes/-/fs-routes-7.7.0.tgz", + "integrity": "sha512-Ckvp35DJ6Y3KNQkNas2Uzv5i0FNngKhTVZDmCeE+tZ585IH6PQq6hhXjX/aHdL4J/sSLKqS3jBbCuya5OiESsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@react-router/dev": "^7.7.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/fs-routes/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@react-router/fs-routes/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-router/node": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.7.0.tgz", + "integrity": "sha512-PTl4C+QjWsbTfp+9mybOzIH10ZM/pjZrAlcoxc/KGYxcfWDEe2GDFFBQN6nGZgJe/0SwSjHsVwqo2haMKgTbvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mjackson/node-fetch-server": "^0.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.7.0", + "typescript": "^5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@react-router/serve": { + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.7.0.tgz", + "integrity": "sha512-XvJAY4Sgv7HxdSuLgkBP8bFXxfI97HJSk+p2BisdtK6JT/nSZugEe0gju4xAkgtsncNJJBVndJTcfUtTDNLTUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@react-router/express": "7.7.0", + "@react-router/node": "7.7.0", + "compression": "^1.7.4", + "express": "^4.19.2", + "get-port": "5.1.1", + "morgan": "^1.10.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "react-router-serve": "bin.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react-router": "7.7.0" + } + }, "node_modules/@react-stately/autocomplete": { "version": "3.0.0-beta.1", "resolved": "https://registry.npmjs.org/@react-stately/autocomplete/-/autocomplete-3.0.0-beta.1.tgz", @@ -9820,16 +10513,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, - "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", - "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@vitest/expect": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.1.tgz", @@ -10910,6 +11593,19 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -12165,6 +12861,65 @@ "node": ">=0.10.0" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -12887,6 +13642,21 @@ "node": ">=0.10.0" } }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, "node_modules/deep-eql": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", @@ -14129,9 +14899,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -14638,19 +15408,6 @@ "node": ">=8" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz", - "integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -14916,6 +15673,19 @@ "node": ">= 0.8.0" } }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -15812,6 +16582,19 @@ "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", "license": "ISC" }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -15938,13 +16721,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -17589,6 +18375,15 @@ "url": "https://github.com/sponsors/gjtorikian/" } }, + "node_modules/isbot": { + "version": "5.1.28", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.28.tgz", + "integrity": "sha512-qrOp4g3xj8YNse4biorv6O5ZShwsJM0trsoda4y7j/Su7ZtTTfVXFzbKkpgcSoDrHS8FcTuUwcU04YimZlZOxw==", + "license": "Unlicense", + "engines": { + "node": ">=18" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -19538,6 +20333,63 @@ "dev": true, "license": "MIT" }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -20111,6 +20963,45 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -20143,6 +21034,104 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/hosted-git-info": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.3.tgz", + "integrity": "sha512-HVJyzUrLIL1c0QmviVh5E8VGyUS7xCFPS6yydaVd1UegW+ibV/CohqTH9MkOLDp5o+rb82DMo77PTuc9F/8GKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-package-arg/node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz", + "integrity": "sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg==", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -20707,6 +21696,16 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -22176,6 +23175,16 @@ "dev": true, "license": "MIT" }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-resizable-panels": { "version": "2.1.7", "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz", @@ -22188,9 +23197,9 @@ } }, "node_modules/react-router": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.0.tgz", - "integrity": "sha512-GGufuHIVCJDbnIAXP3P9Sxzq3UUsddG3rrI3ut1q6m0FI6vxVBF3JoPQ38+W/blslLH4a5Yutp8drkEpXoddGQ==", + "version": "7.7.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.0.tgz", + "integrity": "sha512-3FUYSwlvB/5wRJVTL/aavqHmfUKe0+Xm9MllkYgGo9eDwNdkvwlJGjpPxono1kCycLt6AnDTgjmXvK3/B4QGuw==", "dev": true, "license": "MIT", "dependencies": { @@ -23627,6 +24636,42 @@ "deprecated": "Please use @jridgewell/sourcemap-codec instead", "license": "MIT" }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/split": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", @@ -24643,13 +25688,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.3", + "fdir": "^6.4.4", "picomatch": "^4.0.2" }, "engines": { @@ -24660,9 +25705,9 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", "dev": true, "license": "MIT", "peerDependencies": { @@ -24675,9 +25720,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -24688,9 +25733,9 @@ } }, "node_modules/tinykeys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-2.1.0.tgz", - "integrity": "sha512-/MESnqBD1xItZJn5oGQ4OsNORQgJfPP96XSGoyu4eLpwpL0ifO0SYR5OD76u0YMhMXsqkb0UqvI9+yXTh4xv8Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinykeys/-/tinykeys-3.0.0.tgz", + "integrity": "sha512-nazawuGv5zx6MuDfDY0rmfXjuOGhD5XU2z0GLURQ1nzl0RUe9OuCJq+0u8xxJZINHe+mr7nw8PWYYZ9WhMFujw==", "dev": true, "license": "MIT" }, @@ -25386,6 +26431,32 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/valibot": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.41.0.tgz", + "integrity": "sha512-igDBb8CTYr8YTQlOKgaN9nSS0Be7z+WRuaeYqGf3Cjz3aKmSnqEmYnkfVjzIuumGqfHpa3fLIvMEAfhrpqN8ng==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", @@ -26819,6 +27890,7 @@ "https-proxy-agent": "^7.0.5", "httpsnippet": "^2.0.0", "iconv-lite": "^0.6.3", + "isbot": "^5", "js-yaml": "^4.1.0", "jsdom": "^25.0.1", "jshint": "^2.13.6", @@ -26850,6 +27922,10 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@getinsomnia/api-client": "0.0.10", "@getinsomnia/srp-js": "1.0.0-alpha.1", + "@react-router/dev": "^7.7.0", + "@react-router/fs-routes": "^7.7.0", + "@react-router/node": "^7.7.0", + "@react-router/serve": "^7.7.0", "@sentry/electron": "^6.5.0", "@stoplight/spectral-core": "^1.20.0", "@stoplight/spectral-formats": "^1.8.2", @@ -26924,11 +28000,11 @@ "react-aria-components": "^1.8.0", "react-dom": "^18.3.1", "react-resizable-panels": "^2.1.7", - "react-router": "^7.6.0", + "react-router": "^7.7.0", "react-stately": "3.37.0", "react-use": "^17.6.0", "tailwindcss": "^3.4.17", - "tinykeys": "^2.1.0", + "tinykeys": "^3.0.0", "type-fest": "^4.40.0", "typescript": "^5.8.3", "vite": "^6.3.1", diff --git a/package.json b/package.json index 7e7bdc61f2..47835586ef 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-unicorn": "^59.0.1", + "globals": "^16.3.0", "patch-package": "^8.0.0", "prettier": "3.5.3", "prettier-plugin-tailwindcss": "^0.6.11", diff --git a/packages/insomnia-inso/README.md b/packages/insomnia-inso/README.md index 15921dcd4e..c5a51f13dd 100644 --- a/packages/insomnia-inso/README.md +++ b/packages/insomnia-inso/README.md @@ -106,3 +106,13 @@ inso -w # using a binary ./packages/insomnia-inso/binaries/insomnia-inso -w ``` + +## How to debug the bundled assets + +```bash +DEBUG=1 npm run build +``` + +This will generate an `artifacts` directory containing information about the bundled assets. +The meta.json can be uploaded to https://esbuild.github.io/analyze/ to visualize the bundle. +The bundle-analysis.log can be used to see the dependency tree of the bundle. diff --git a/packages/insomnia-inso/esbuild.ts b/packages/insomnia-inso/esbuild.ts index c0fbf6a6fe..ccb1ad394c 100644 --- a/packages/insomnia-inso/esbuild.ts +++ b/packages/insomnia-inso/esbuild.ts @@ -1,11 +1,14 @@ -import { build, type BuildOptions, context } from 'esbuild'; +import fs from 'node:fs'; +import { analyzeMetafile, build, type BuildOptions, context } from 'esbuild'; const isProd = Boolean(process.env.NODE_ENV === 'production'); const watch = Boolean(process.env.ESBUILD_WATCH); +const isDebug = Boolean(process.env.DEBUG); const version = process.env.VERSION || 'dev'; const config: BuildOptions = { outfile: './dist/index.js', bundle: true, + metafile: isDebug, platform: 'node', minify: isProd, target: 'node22', @@ -44,5 +47,18 @@ if (watch) { } watch(); } else { + if (isDebug) { + async function buildWithDebug() { + const result = await build(config); + + if (result.metafile) { + fs.mkdirSync('./artifacts', { recursive: true }); + fs.writeFileSync('./artifacts/meta.json', JSON.stringify(result.metafile)); + fs.writeFileSync('./artifacts/bundle-analysis.log', await analyzeMetafile(result.metafile)); + } + } + + buildWithDebug(); + } build(config); } diff --git a/packages/insomnia-inso/tsconfig.json b/packages/insomnia-inso/tsconfig.json index 172649c218..023b4bc23a 100644 --- a/packages/insomnia-inso/tsconfig.json +++ b/packages/insomnia-inso/tsconfig.json @@ -10,15 +10,20 @@ "moduleResolution": "node", "isolatedModules": true, "paths": { - "electron": ["../insomnia/send-request/electron"] + "~/*": [ + "../insomnia/src/*" + ], + "electron": [ + "../insomnia/send-request/electron" + ] }, - /* remove this once react AlertModal is out of the plugins code path */ - "jsx": "react", /* Transpiling Options */ "module": "CommonJS", "sourceMap": true, - /* Runs in the DOM NOTE: this is inconsistent with reality */ - "lib": ["ES2023", "DOM", "DOM.Iterable"], + "lib": [ + "ES2023", + "DOM.Iterable" + ], /* Strictness */ "strict": true, "noImplicitReturns": true, @@ -27,7 +32,13 @@ "noFallthroughCasesInSwitch": true, "useUnknownInCatchVariables": false }, - "include": [".eslintrc.js", "esbuild.ts", "package.json", "src", "../insomnia/types"], + "include": [ + ".eslintrc.js", + "esbuild.ts", + "package.json", + "src", + "../insomnia/types", + ], "exclude": [ "**/*.test.ts", "**/__mocks__/*", diff --git a/packages/insomnia-scripting-environment/src/objects/interpolator.ts b/packages/insomnia-scripting-environment/src/objects/interpolator.ts index bf4d21f482..b0aa8b5569 100644 --- a/packages/insomnia-scripting-environment/src/objects/interpolator.ts +++ b/packages/insomnia-scripting-environment/src/objects/interpolator.ts @@ -1,12 +1,12 @@ import { fakerFunctions } from 'insomnia/src/templating/faker-functions'; -import { configure, type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks'; +import nunjucks, { type ConfigureOptions, type Environment as NunjuncksEnv } from 'nunjucks'; /** @ignore */ class Interpolator { private engine: NunjuncksEnv; constructor(config: ConfigureOptions) { - this.engine = configure(config); + this.engine = nunjucks.configure(config); } render = (template: string, context: object) => { diff --git a/packages/insomnia-scripting-environment/tsconfig.json b/packages/insomnia-scripting-environment/tsconfig.json index 34e2332196..a372b15fae 100644 --- a/packages/insomnia-scripting-environment/tsconfig.json +++ b/packages/insomnia-scripting-environment/tsconfig.json @@ -9,6 +9,11 @@ "module": "ES2022", "moduleResolution": "node", "isolatedModules": true, + "paths": { + "~/*": [ + "../insomnia/src/*" + ], + }, /* Strictness */ "strict": true, "noImplicitReturns": true, @@ -19,8 +24,17 @@ "verbatimModuleSyntax": true, "jsx": "react", /* If your code runs in the DOM: */ - "lib": ["es2023", "dom", "dom.iterable"] + "lib": [ + "es2023", + "dom", + "dom.iterable" + ] }, - "include": ["../insomnia/types"], - "exclude": ["**/__tests__", "node_modules"] + "include": [ + "../insomnia/types" + ], + "exclude": [ + "**/__tests__", + "node_modules" + ] } diff --git a/packages/insomnia-smoke-test/tests/critical/bundling.test.ts b/packages/insomnia-smoke-test/tests/critical/bundling.test.ts index 520f6f9408..315ddd8639 100644 --- a/packages/insomnia-smoke-test/tests/critical/bundling.test.ts +++ b/packages/insomnia-smoke-test/tests/critical/bundling.test.ts @@ -37,6 +37,10 @@ test('can use bundled plugins, node-libcurl, httpsnippet, hidden browser window' await page.getByRole('button', { name: 'Done' }).click(); await page.getByLabel('Request Collection').getByTestId('sends request with pre-request script').press('Enter'); + await expect + .soft(page.getByTestId('request-pane').getByTestId('OneLineEditor').getByText(`http://127.0.0.1:4010/echo`)) + .toBeVisible(); + await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); await expect.soft(statusTag).toContainText('200 OK'); await page.getByRole('tab', { name: 'Console' }).click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/analytics.test.ts b/packages/insomnia-smoke-test/tests/smoke/analytics.test.ts deleted file mode 100644 index 91cde36ca8..0000000000 --- a/packages/insomnia-smoke-test/tests/smoke/analytics.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { expect } from '@playwright/test'; - -import { test } from '../../playwright/test'; - -interface SegmentRequestData { - batch: { - timestamp: string; - integrations: {}; - type: string; - properties: {}; - name?: string; - context: { - app: { - name: string; - version: string; - }; - os: { - name: string; - version: string; - }; - library: { - name: string; - version: string; - }; - }; - anonymousId: string; - userId: string; - messageId: string; - _metadata: { - nodeVersion: string; - jsRuntime: string; - }; - event?: string; - }[]; - writeKey: string; - sentAt: string; -} - -interface SegmentLog { - url: string; - data: SegmentRequestData[]; -} - -test('analytics events are sent', async ({ page, app }) => { - await app.evaluate(async ({ session }) => { - // Capture segment requests to a global variable in main process - globalThis.segmentLogs = []; - - session.defaultSession.webRequest.onBeforeRequest((details, callback) => { - if (details.url.includes('segment')) { - globalThis.segmentLogs.push({ url: details.url, data: details.uploadData }); - } - callback({ cancel: false }); - }); - }); - - // Create a collection and requests that cause analytics events: - await page.getByRole('button', { name: 'Create document', exact: true }).click(); - await page.getByRole('button', { name: 'Create', exact: true }).click(); - - await page.getByTestId('workspace-debug').click(); - - for (let i = 0; i < 10; i++) { - await page.getByLabel('Create in collection').click(); - await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter'); - } - - const segmentLogs = await app.evaluate(() => globalThis.segmentLogs); - - const decodedLogs: SegmentLog[] = segmentLogs.map( - (log: { url: string; data: { type: string; bytes: number[] }[] }) => { - return { - url: log.url, - data: log.data.map(data => JSON.parse(Buffer.from(Object.values(data.bytes)).toString('utf-8'))), - }; - }, - ); - - const analyticsBatch = decodedLogs[0].data[0].batch; - const [appStartEvent, ...restEvents] = analyticsBatch; - - // Analytics need at least 15 events to be sent - expect.soft(analyticsBatch.length).toBeGreaterThanOrEqual(5); - - // App start event - expect.soft(appStartEvent.anonymousId).toBeTruthy(); - expect.soft(appStartEvent.event).toBe('App Started'); - - // First event should have userId and anonymousId - expect.soft(restEvents[0].anonymousId).toBeTruthy(); - expect.soft(restEvents[0].userId).toBeTruthy(); - - // Last event should have userId and anonymousId - expect.soft(restEvents.at(-1)?.anonymousId).toBeTruthy(); - expect.soft(restEvents.at(-1)?.userId).toBeTruthy(); -}); diff --git a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts index e44de79b55..f6137dbc2f 100644 --- a/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/dashboard-interactions.test.ts @@ -69,7 +69,7 @@ test.describe('Dashboard', () => { await page.getByLabel('Files').getByLabel('My Design Document').getByRole('button').click(); await page.getByRole('menuitem', { name: 'Rename' }).click(); await page.locator('text=Rename DocumentName Rename >> input[type="text"]').fill('test123'); - await page.click('#root button:has-text("Rename")'); + await page.getByRole('button', { name: 'Rename' }).click(); await expect.soft(page.locator('.app')).toContainText('test123'); // Duplicate document @@ -92,7 +92,7 @@ test.describe('Dashboard', () => { await page.click('text=CollectionMy Collectionjust now >> button'); await page.getByRole('menuitem', { name: 'Rename' }).click(); await page.locator('text=Rename CollectionName Rename >> input[type="text"]').fill('collection123'); - await page.click('#root button:has-text("Rename")'); + await page.getByRole('button', { name: 'Rename' }).click(); await expect.soft(page.locator('.app')).toContainText('collection123'); // Duplicate collection diff --git a/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts b/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts index 1d6fa877a9..5a60adbf18 100644 --- a/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/debug-sidebar-interactions.test.ts @@ -15,7 +15,6 @@ test.describe('Debug-Sidebar', () => { await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); await page.getByLabel('simple').click(); //Open Properties in Request Sidebar - const requestLocator = page.getByLabel('Request Collection').getByRole('row', { name: 'example http' }); await page.getByLabel('Request Collection').getByRole('row', { name: 'example http' }).click(); await page .getByLabel('Request Collection') diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts index a28606e285..d9d9a965d0 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts @@ -32,13 +32,13 @@ test.describe('test hidden window handling', () => { await page.click('text=Request was cancelled'); await page.getByText('Special template tag format').click(); - await expect.soft(page.getByText(`{{ _['examplehost']}}`)).toBeVisible(); + await expect.soft(page.getByText(`_['examplehost']`)).toBeVisible(); await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); await page.getByText('200 OK').click(); await page.getByText('Multiple template tags format').click(); - await expect.soft(page.getByText(`{{_['a']['b']['c']['url']}}`)).toBeVisible(); + await expect.soft(page.getByText(`_['a']['b']['c']['url']`)).toBeVisible(); await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); await page.getByText('200 OK').click(); diff --git a/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts b/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts index e59acf3476..1dffb88ff7 100644 --- a/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/request-pane-tab.test.ts @@ -4,8 +4,6 @@ test('Request tabs', async ({ page }) => { // Create new collection await page.getByRole('button', { name: 'Create request collection', exact: true }).click(); - await page.getByLabel('Create in collection').click(); - await page.getByRole('menuitemradio', { name: 'HTTP Request' }).press('Enter'); await page.getByRole('tab', { name: 'Body' }).click(); await page.getByRole('button', { name: 'Body' }).click(); await page.getByRole('option', { name: 'JSON' }).click(); diff --git a/packages/insomnia/.gitignore b/packages/insomnia/.gitignore index e8555cff07..503db24c7b 100644 --- a/packages/insomnia/.gitignore +++ b/packages/insomnia/.gitignore @@ -4,4 +4,4 @@ build # Generated src/*.js src/*.js.map - +.react-router/ diff --git a/packages/insomnia/esbuild.main.ts b/packages/insomnia/esbuild.main.ts index d4205c8b75..3ccaeffb11 100644 --- a/packages/insomnia/esbuild.main.ts +++ b/packages/insomnia/esbuild.main.ts @@ -22,7 +22,7 @@ export default async function build(options: Options) { const env: Record = __DEV__ ? { - 'process.env.APP_RENDER_URL': JSON.stringify(`http://localhost:${PORT}/index.html`), + 'process.env.APP_RENDER_URL': JSON.stringify(`http://localhost:${PORT}`), 'process.env.HIDDEN_BROWSER_WINDOW_URL': JSON.stringify(`http://localhost:${PORT}/hidden-window.html`), 'process.env.NODE_ENV': JSON.stringify('development'), 'process.env.INSOMNIA_ENV': JSON.stringify('development'), @@ -59,6 +59,22 @@ export default async function build(options: Options) { }, }; + const hiddenBrowserWindowBuildOptions: BuildOptions = { + entryPoints: ['./src/hidden-window.ts'], + // TODO: make all of these outputs use a .min.js convention to simplify ignore files + outfile: path.join(outdir, 'hidden-window.js'), + target: 'esnext', + bundle: true, + platform: 'node', + sourcemap: true, + format: 'cjs', + // TODO: remove below, This indicates that libcurl is being imported when it shouldn't be + external: ['electron'], + loader: { + '.node': 'copy', + }, + }; + const mainBuildOptions: BuildOptions = { entryPoints: ['./src/main.development.ts'], outfile: path.join(outdir, 'main.min.js'), @@ -114,6 +130,10 @@ export default async function build(options: Options) { ...preloadBuildOptions, plugins: [restartElectronPlugin('preload')], }); + const hiddenBrowserWindowContext = await esbuild.context({ + ...hiddenBrowserWindowBuildOptions, + plugins: [restartElectronPlugin('hidden-browser-window')], + }); const mainContext = await esbuild.context({ ...mainBuildOptions, plugins: [restartElectronPlugin('main')], @@ -138,14 +158,16 @@ export default async function build(options: Options) { }; const preloadWatch = await preloadContext.watch(); + const hiddenWindowWatch = await hiddenBrowserWindowContext.watch(); const mainWatch = await mainContext.watch(); - const hiddenWindowWatch = await hiddenPreloadContext.watch(); - return Promise.all([preloadWatch, mainWatch, hiddenWindowWatch]); + const hiddenWindowPreloadWatch = await hiddenPreloadContext.watch(); + return Promise.all([preloadWatch, hiddenWindowPreloadWatch, mainWatch, hiddenWindowWatch]); } const preload = esbuild.build(preloadBuildOptions); + const hiddenBrowserWindow = esbuild.build(hiddenBrowserWindowBuildOptions); const hiddenBrowserWindowPreload = esbuild.build(hiddenBrowserWindowPreloadBuildOptions); const main = esbuild.build(mainBuildOptions); - return Promise.all([main, preload, hiddenBrowserWindowPreload]).catch(err => { + return Promise.all([main, preload, hiddenBrowserWindow, hiddenBrowserWindowPreload]).catch(err => { console.error('[Build] Build failed:', err); }); } diff --git a/packages/insomnia/package.json b/packages/insomnia/package.json index 09c87d066e..3fb1623858 100644 --- a/packages/insomnia/package.json +++ b/packages/insomnia/package.json @@ -18,7 +18,8 @@ "main": "src/main.min.js", "scripts": { "verify-bundle-plugins": "esr --cache ./scripts/verify-bundle-plugins.ts", - "build": "esr --cache ./scripts/build.ts --noErrorTruncation", + "build": "react-router build && esr --cache ./scripts/build.ts --noErrorTruncation", + "build:react-router": "react-router build", "generate:schema": "esr ./src/schema.ts", "build:main.min.js": "cross-env NODE_ENV=development esr esbuild.main.ts", "lint": "eslint . --ext .js,.ts,.tsx --cache", @@ -31,8 +32,7 @@ "start:electron": "cross-env NODE_ENV=development esr esbuild.main.ts && electron --inspect=5858 .", "start:electron:autoRestart": "cross-env NODE_ENV=development esr esbuild.main.ts --autoRestart", "test": "vitest run", - "electron:dev-build": "electron ./build/main.min.js", - "type-check": "tsc --noEmit --project tsconfig.json", + "type-check": "react-router typegen && tsc --noEmit --project tsconfig.json", "type-check:watch": "npm run type-check -- --watch", "convert-svg": "npm_config_yes=true npx @svgr/cli@6.4.0 --no-index --config-file svgr.config.js --out-dir src/ui/components/assets/svgr src/ui/components/assets/" }, @@ -78,6 +78,7 @@ "https-proxy-agent": "^7.0.5", "httpsnippet": "^2.0.0", "iconv-lite": "^0.6.3", + "isbot": "^5", "js-yaml": "^4.1.0", "jsdom": "^25.0.1", "jshint": "^2.13.6", @@ -106,6 +107,10 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@getinsomnia/api-client": "0.0.10", "@getinsomnia/srp-js": "1.0.0-alpha.1", + "@react-router/node": "^7.7.0", + "@react-router/serve": "^7.7.0", + "@react-router/dev": "^7.7.0", + "@react-router/fs-routes": "^7.7.0", "@sentry/electron": "^6.5.0", "@stoplight/spectral-core": "^1.20.0", "@stoplight/spectral-formats": "^1.8.2", @@ -180,11 +185,11 @@ "react-aria-components": "^1.8.0", "react-dom": "^18.3.1", "react-resizable-panels": "^2.1.7", - "react-router": "^7.6.0", + "react-router": "^7.7.0", "react-stately": "3.37.0", "react-use": "^17.6.0", "tailwindcss": "^3.4.17", - "tinykeys": "^2.1.0", + "tinykeys": "^3.0.0", "type-fest": "^4.40.0", "typescript": "^5.8.3", "vite": "^6.3.1", diff --git a/packages/insomnia/react-router.config.ts b/packages/insomnia/react-router.config.ts new file mode 100644 index 0000000000..f89b82e7c2 --- /dev/null +++ b/packages/insomnia/react-router.config.ts @@ -0,0 +1,7 @@ +import { type Config } from '@react-router/dev/config'; + +export default { + appDirectory: 'src', + ssr: false, + serverModuleFormat: 'cjs', +} satisfies Config; diff --git a/packages/insomnia/scripts/build.ts b/packages/insomnia/scripts/build.ts index 93bb703c3e..ca2c31b2da 100644 --- a/packages/insomnia/scripts/build.ts +++ b/packages/insomnia/scripts/build.ts @@ -1,9 +1,6 @@ -import childProcess from 'node:child_process'; -import { cp, mkdir, rm } from 'node:fs/promises'; +import { cp, mkdir } from 'node:fs/promises'; import path from 'node:path'; -import * as vite from 'vite'; - import buildMainAndPreload from '../esbuild.main'; // Start build if ran from CLI @@ -21,8 +18,7 @@ if (require.main === module) { export const start = async () => { console.log('[build] Starting build'); - console.log(`[build] npm: ${childProcess.spawnSync('npm', ['--version']).stdout}`.trim()); - console.log(`[build] node: ${childProcess.spawnSync('node', ['--version']).stdout}`.trim()); + console.log(`[build] node: ${process.version}`.trim()); if (process.version.indexOf('v22.') !== 0) { console.log('[build] Node 22.x.x is required to build'); @@ -31,21 +27,11 @@ export const start = async () => { const buildFolder = path.join('../build'); - // Remove folders first - console.log('[build] Removing existing directories'); - await rm(path.resolve(__dirname, buildFolder), { recursive: true, force: true }); - console.log('[build] Building main.min.js and preload'); await buildMainAndPreload({ mode: 'production', }); - console.log('[build] Building renderer'); - - await vite.build({ - configFile: path.join(__dirname, '..', 'vite.config.ts'), - }); - // Copy necessary files console.log('[build] Copying files'); const copyFiles = async (relSource: string, relDest: string) => { @@ -58,6 +44,7 @@ export const start = async () => { await copyFiles('../src/static', path.join(buildFolder, 'static')); await copyFiles('../src/icons', buildFolder); await copyFiles('../src/main/lint-process.mjs', path.join(buildFolder, 'main/lint-process.mjs')); + await copyFiles('../src/hidden-window.html', path.join(buildFolder, 'hidden-window.html')); console.log('[build] Complete!'); }; diff --git a/packages/insomnia/src/account/session.ts b/packages/insomnia/src/account/session.ts index e0a2280396..3aef2fe883 100644 --- a/packages/insomnia/src/account/session.ts +++ b/packages/insomnia/src/account/session.ts @@ -116,7 +116,7 @@ export async function logout() { } } - _unsetSessionData(); + await _unsetSessionData(); window.main.loginStateChange(); } diff --git a/packages/insomnia/src/common/__tests__/constants.test.ts b/packages/insomnia/src/common/__tests__/constants.test.ts index a2fb990e5c..e774e86b20 100644 --- a/packages/insomnia/src/common/__tests__/constants.test.ts +++ b/packages/insomnia/src/common/__tests__/constants.test.ts @@ -2,10 +2,6 @@ import { describe, expect, it } from 'vitest'; import type { MockServer } from '../../models/mock-server'; import { - ACTIVITY_DEBUG, - ACTIVITY_HOME, - ACTIVITY_SPEC, - ACTIVITY_UNIT_TEST, FLEXIBLE_URL_REGEX, getContentTypeName, getMockServiceBinURL, @@ -41,22 +37,22 @@ describe('URL Regex', () => { describe('isWorkspaceActivity', () => { it('should return true', () => { - expect(isWorkspaceActivity(ACTIVITY_SPEC)).toBe(true); - expect(isWorkspaceActivity(ACTIVITY_DEBUG)).toBe(true); - expect(isWorkspaceActivity(ACTIVITY_UNIT_TEST)).toBe(true); + expect(isWorkspaceActivity('spec')).toBe(true); + expect(isWorkspaceActivity('debug')).toBe(true); + expect(isWorkspaceActivity('unittest')).toBe(true); }); it('should return false', () => { - expect(isWorkspaceActivity(ACTIVITY_HOME)).toBe(false); + expect(isWorkspaceActivity('home')).toBe(false); }); }); describe('isValidActivity', () => { it('should return true', () => { - expect(isValidActivity(ACTIVITY_SPEC)).toBe(true); - expect(isValidActivity(ACTIVITY_DEBUG)).toBe(true); - expect(isValidActivity(ACTIVITY_UNIT_TEST)).toBe(true); - expect(isValidActivity(ACTIVITY_HOME)).toBe(true); + expect(isValidActivity('spec')).toBe(true); + expect(isValidActivity('debug')).toBe(true); + expect(isValidActivity('unittest')).toBe(true); + expect(isValidActivity('home')).toBe(true); }); it('should return false', () => { diff --git a/packages/insomnia/src/common/constants.ts b/packages/insomnia/src/common/constants.ts index 4dec3ae0fd..ad12d63b03 100644 --- a/packages/insomnia/src/common/constants.ts +++ b/packages/insomnia/src/common/constants.ts @@ -176,23 +176,19 @@ export const DEFAULT_SIDEBAR_SIZE = 25; // Activities export type GlobalActivity = 'spec' | 'debug' | 'unittest' | 'home'; -export const ACTIVITY_SPEC: GlobalActivity = 'spec'; -export const ACTIVITY_DEBUG: GlobalActivity = 'debug'; -export const ACTIVITY_UNIT_TEST: GlobalActivity = 'unittest'; -export const ACTIVITY_HOME: GlobalActivity = 'home'; export const isWorkspaceActivity = (activity?: string): activity is GlobalActivity => isDesignActivity(activity) || isCollectionActivity(activity); export const isDesignActivity = (activity?: string): activity is GlobalActivity => { switch (activity) { - case ACTIVITY_SPEC: - case ACTIVITY_DEBUG: - case ACTIVITY_UNIT_TEST: { + case 'spec': + case 'debug': + case 'unittest': { return true; } - case ACTIVITY_HOME: + case 'home': default: { return false; } @@ -201,13 +197,13 @@ export const isDesignActivity = (activity?: string): activity is GlobalActivity export const isCollectionActivity = (activity?: string): activity is GlobalActivity => { switch (activity) { - case ACTIVITY_DEBUG: { + case 'debug': { return true; } - case ACTIVITY_SPEC: - case ACTIVITY_UNIT_TEST: - case ACTIVITY_HOME: + case 'spec': + case 'unittest': + case 'home': default: { return false; } @@ -216,10 +212,10 @@ export const isCollectionActivity = (activity?: string): activity is GlobalActiv export const isValidActivity = (activity: string): activity is GlobalActivity => { switch (activity) { - case ACTIVITY_SPEC: - case ACTIVITY_DEBUG: - case ACTIVITY_UNIT_TEST: - case ACTIVITY_HOME: { + case 'spec': + case 'debug': + case 'unittest': + case 'home': { return true; } diff --git a/packages/insomnia/src/common/import.ts b/packages/insomnia/src/common/import.ts index d84556647b..15d6636820 100644 --- a/packages/insomnia/src/common/import.ts +++ b/packages/insomnia/src/common/import.ts @@ -1,5 +1,7 @@ import { readFile } from 'node:fs/promises'; +import type { CurrentPlan } from '~/models/organization'; + import { type ApiSpec, isApiSpec } from '../models/api-spec'; import { type CookieJar, isCookieJar } from '../models/cookie-jar'; import { type BaseEnvironment, type Environment, isEnvironment } from '../models/environment'; @@ -15,7 +17,6 @@ import { isUnitTest, type UnitTest } from '../models/unit-test'; import { isUnitTestSuite, type UnitTestSuite } from '../models/unit-test-suite'; import { isWebSocketRequest, type WebSocketRequest } from '../models/websocket-request'; import { isWorkspace, type Workspace } from '../models/workspace'; -import type { CurrentPlan } from '../ui/organization-utils'; import { convert, type InsomniaImporter } from '../utils/importers/convert'; import type { ImportEntry } from '../utils/importers/entities'; import { id as postmanEnvImporterId } from '../utils/importers/importers/postman-env'; diff --git a/packages/insomnia/src/entry.client.tsx b/packages/insomnia/src/entry.client.tsx new file mode 100644 index 0000000000..b17312513f --- /dev/null +++ b/packages/insomnia/src/entry.client.tsx @@ -0,0 +1,104 @@ +import './ui/rendererListeners'; + +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import type { SessionData } from 'react-router'; +import { HydratedRouter } from 'react-router/dom'; + +import { migrateFromLocalStorage, setSessionData, setVaultSessionData } from './account/session'; +import { getInsomniaSession, getInsomniaVaultKey, getInsomniaVaultSalt, getSkipOnboarding } from './common/constants'; +import { database } from './common/database'; +import { initializeLogging } from './common/log'; +import { settings } from './models'; +import { initNewOAuthSession } from './network/o-auth-2/get-token'; +import { init as initPlugins } from './plugins'; +import { applyColorScheme } from './plugins/misc'; +import { HtmlElementWrapper } from './ui/components/html-element-wrapper'; +import { showModal } from './ui/components/modals'; +import { AlertModal } from './ui/components/modals/alert-modal'; +import { PromptModal } from './ui/components/modals/prompt-modal'; +import { WrapperModal } from './ui/components/modals/wrapper-modal'; +import { initializeSentry } from './ui/sentry'; +import { getInitialEntry } from './utils/router'; + +initializeSentry(); +initializeLogging(); + +try { + window.showAlert = options => showModal(AlertModal, options); + window.showPrompt = options => + showModal(PromptModal, { + ...options, + title: options?.title || '', + }); + window.showWrapper = options => + showModal(WrapperModal, { + ...options, + title: options?.title || '', + body: , + }); + + // In order to run playwight tests that simulate a logged in user + // we need to inject state into localStorage + const skipOnboarding = getSkipOnboarding(); + if (skipOnboarding) { + window.localStorage.setItem('hasSeenOnboardingV11', skipOnboarding.toString()); + window.localStorage.setItem('hasUserLoggedInBefore', skipOnboarding.toString()); + } +} catch (e) { + console.log('[onboarding] Failed to parse session data', e); +} + +await database.initClient(); +await initPlugins(); + +await migrateFromLocalStorage(); + +// Check if there is a Session provided by an env variable and use this +const insomniaSession = getInsomniaSession(); +const insomniaVaultKey = getInsomniaVaultKey() || ''; +const insomniaVaultSalt = getInsomniaVaultSalt() || ''; +if (insomniaSession) { + try { + const session = JSON.parse(insomniaSession) as SessionData; + await setSessionData( + session.id, + session.accountId, + session.firstName, + session.lastName, + session.email, + session.symmetricKey, + session.publicKey, + session.encPrivateKey, + ); + if (insomniaVaultSalt || insomniaVaultKey) { + await setVaultSessionData(insomniaVaultSalt, insomniaVaultKey); + } + } catch (e) { + console.log('[init] Failed to parse session data', e); + } +} + +const appSettings = await settings.getOrCreate(); + +if (appSettings.clearOAuth2SessionOnRestart) { + initNewOAuthSession(); +} + +await applyColorScheme(appSettings); + +const initialEntry = await getInitialEntry(); + +if (typeof initialEntry === 'string' && window.location.pathname !== initialEntry) { + console.log('[entry.client] Initial entry:', initialEntry); + window.location.pathname = initialEntry; +} + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/packages/insomnia/src/entry.server.tsx b/packages/insomnia/src/entry.server.tsx new file mode 100644 index 0000000000..7c182ea18c --- /dev/null +++ b/packages/insomnia/src/entry.server.tsx @@ -0,0 +1,65 @@ +import { PassThrough } from 'node:stream'; + +import { createReadableStreamFromReadable } from '@react-router/node'; +import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { renderToPipeableStream } from 'react-dom/server'; +import type { EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; + +export const streamTimeout = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + // loadContext: AppLoadContext + // If you have middleware enabled: + // loadContext: unstable_RouterContextProvider +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + const readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady'; + + const { pipe, abort } = renderToPipeableStream(, { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }); + + // Abort the rendering stream after the `streamTimeout` so it has time to + // flush down the rejected boundaries + setTimeout(abort, streamTimeout + 1000); + }); +} diff --git a/packages/insomnia/src/hidden-window.html b/packages/insomnia/src/hidden-window.html index f1c1172215..779010c8cc 100644 --- a/packages/insomnia/src/hidden-window.html +++ b/packages/insomnia/src/hidden-window.html @@ -11,6 +11,6 @@

Hidden Browser Window

- + diff --git a/packages/insomnia/src/index.html b/packages/insomnia/src/index.html deleted file mode 100644 index f1f45cd2a2..0000000000 --- a/packages/insomnia/src/index.html +++ /dev/null @@ -1,131 +0,0 @@ - - - - - - - - - -
-
-
- - - - - - - - - - - - - - - -
-
-
-
-
- - - diff --git a/packages/insomnia/src/main/api.protocol.ts b/packages/insomnia/src/main/api.protocol.ts index 82dcf5ba3c..e0ef548896 100644 --- a/packages/insomnia/src/main/api.protocol.ts +++ b/packages/insomnia/src/main/api.protocol.ts @@ -1,3 +1,4 @@ +import path from 'node:path'; import { Readable } from 'node:stream'; import { parse as urlParse } from 'node:url'; @@ -178,6 +179,15 @@ export async function registerInsomniaProtocols() { if (!protocol.isProtocolHandled(httpsScheme)) { protocol.handle(httpsScheme, async request => { + const url = new URL(request.url); + if (url.hostname === 'insomnia-app.local') { + const rootDir = path.resolve(__dirname, 'client'); + const filePath = path.join(rootDir, url.pathname.startsWith('/assets') ? url.pathname : 'index.html'); + console.log(`Loading index for: ${url.pathname} from: ${filePath}`); + + return await net.fetch(`file://${filePath}`, { bypassCustomProtocolHandlers: true }); + } + return net.fetch(request, { bypassCustomProtocolHandlers: true }); }); } diff --git a/packages/insomnia/src/main/git-service.ts b/packages/insomnia/src/main/git-service.ts index 7b7a52c428..aa2eeec24e 100644 --- a/packages/insomnia/src/main/git-service.ts +++ b/packages/insomnia/src/main/git-service.ts @@ -8,6 +8,8 @@ import { Errors, type HeadStatus, type PromiseFsClient, type StageStatus, type W import { v4 } from 'uuid'; import YAML, { parse } from 'yaml'; +import { type GitCredentials } from '~/models/git-repository'; + import { getApiBaseURL, getAppWebsiteBaseURL, @@ -31,7 +33,6 @@ import GitVCS, { GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME, GIT_INTERNAL_DIR, - type GitCredentials, MergeConflictError, } from '../sync/git/git-vcs'; import { MemClient } from '../sync/git/mem-client'; @@ -593,18 +594,14 @@ export const initGitRepoCloneAction = async ({ uri, authorName, authorEmail, - token, - username, - oauth2format, + credentials, ref, }: { organizationId: string; uri: string; authorName: string; authorEmail: string; - token: string; - username: string; - oauth2format?: string; + credentials: GitCredentials; ref?: string; }): Promise< | { @@ -630,24 +627,20 @@ export const initGitRepoCloneAction = async ({ }; // Git Credentials - if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); - - repoSettingsPatch.credentials = { - username, - token, - oauth2format, - }; + if ('oauth2format' in credentials) { + invariant( + credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github', + 'OAuth2 format is required', + ); + } else if ('password' in credentials) { + invariant(typeof credentials.username === 'string', 'Username is required'); + invariant(typeof credentials.password === 'string', 'Password is required'); } else { - invariant(typeof token === 'string', 'Token is required'); - invariant(typeof username === 'string', 'Username is required'); - - repoSettingsPatch.credentials = { - password: token, - username, - }; + throw new Error('Invalid credentials'); } + repoSettingsPatch.credentials = credentials; + repoSettingsPatch.needsFullClone = true; const inMemoryFsClient = MemClient.createClient(); @@ -704,53 +697,41 @@ export const cloneGitRepoAction = async ({ cloneIntoProjectId, name, uri, - authorName, - authorEmail, - token, - username, - oauth2format, + author, + credentials, ref, }: { organizationId: string; projectId?: string; cloneIntoProjectId?: string; + author: { + name: string; + email: string; + }; + credentials: GitCredentials; name?: string; uri: string; - authorName: string; - authorEmail: string; - token: string; - username: string; - oauth2format?: string; ref?: string; }) => { try { + const repoSettingsPatch: Partial = {}; + repoSettingsPatch.uri = parseGitToHttpsURL(uri); + repoSettingsPatch.author = author; + + // Git Credentials + if ('oauth2format' in credentials) { + invariant( + credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github', + 'OAuth2 format is required', + ); + } else if ('password' in credentials) { + invariant(typeof credentials.password === 'string', 'Password is required'); + invariant(typeof credentials.username === 'string', 'Username is required'); + } + + repoSettingsPatch.credentials = credentials; + if (!projectId) { - const repoSettingsPatch: Partial = {}; - repoSettingsPatch.uri = parseGitToHttpsURL(uri); - repoSettingsPatch.author = { - name: authorName, - email: authorEmail, - }; - - // Git Credentials - if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); - - repoSettingsPatch.credentials = { - username, - token, - oauth2format, - }; - } else { - invariant(typeof token === 'string', 'Token is required'); - invariant(typeof username === 'string', 'Username is required'); - - repoSettingsPatch.credentials = { - password: token, - username, - }; - } - trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'clone')); repoSettingsPatch.needsFullClone = true; @@ -881,32 +862,6 @@ export const cloneGitRepoAction = async ({ const project = await models.project.getById(projectId); invariant(project, 'Project not found'); - const repoSettingsPatch: Partial = {}; - repoSettingsPatch.uri = parseGitToHttpsURL(uri); - repoSettingsPatch.author = { - name: authorName, - email: authorEmail, - }; - - // Git Credentials - if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); - - repoSettingsPatch.credentials = { - username, - token, - oauth2format, - }; - } else { - invariant(typeof token === 'string', 'Token is required'); - invariant(typeof username === 'string', 'Username is required'); - - repoSettingsPatch.credentials = { - password: token, - username, - }; - } - trackSegmentEvent(SegmentEvent.vcsSyncStart, vcsSegmentEventProperties('git', 'clone')); repoSettingsPatch.needsFullClone = true; @@ -1099,22 +1054,19 @@ export const cloneGitRepoAction = async ({ export const updateGitRepoAction = async ({ projectId, workspaceId, - authorEmail, - authorName, + author, + credentials, uri, - oauth2format, - username, - token, ref, }: { projectId: string; workspaceId?: string; - authorName: string; - authorEmail: string; + author: { + name: string; + email: string; + }; + credentials: GitCredentials; uri: string; - oauth2format?: string; - username: string; - token: string; ref?: string; }) => { try { @@ -1138,27 +1090,22 @@ export const updateGitRepoAction = async ({ repoSettingsPatch.uri = parseGitToHttpsURL(uri); // Author - repoSettingsPatch.author = { - name: authorName, - email: authorEmail, - }; + repoSettingsPatch.author = author; // Git Credentials - if (oauth2format) { - invariant(oauth2format === 'gitlab' || oauth2format === 'github', 'OAuth2 format is required'); - - repoSettingsPatch.credentials = { - username, - token, - oauth2format, - }; - } else { - repoSettingsPatch.credentials = { - password: token, - username, - }; + // Git Credentials + if ('oauth2format' in credentials) { + invariant( + credentials.oauth2format === 'gitlab' || credentials.oauth2format === 'github', + 'OAuth2 format is required', + ); + } else if ('password' in credentials) { + invariant(typeof credentials.password === 'string', 'Password is required'); + invariant(typeof credentials.username === 'string', 'Username is required'); } + repoSettingsPatch.credentials = credentials; + async function setupGitRepository() { if (gitRepositoryId && gitRepositoryId !== 'empty') { const gitRepository = await models.gitRepository.getById(gitRepositoryId); @@ -1494,7 +1441,7 @@ export const checkoutGitBranchAction = async ({ }; } - const errorMessage = err instanceof Error ? err.message : err; + const errorMessage = err instanceof Error ? err.message : err.toString(); return { errors: [errorMessage], diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index b02a4c0959..d9a0322da2 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -104,6 +104,7 @@ export interface RendererToMainBridgeAPI { updateLatestStepName: (options: { requestId: string; stepName: string }) => void; extractJsonFileFromPostmanDataDumpArchive: (archivePath: string) => Promise; } + export function registerMainHandlers() { ipcMainOn('addExecutionStep', (_, options: { requestId: string; stepName: string }) => { addExecutionStep(options.requestId, options.stepName); diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 5718b0bba7..c30d163060 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -1,7 +1,6 @@ import fs from 'node:fs'; import * as os from 'node:os'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; import { app, @@ -82,6 +81,7 @@ export async function createHiddenBrowserWindow() { // if window crashed const windowWasClosedUnexpectedly = hiddenWindowIsBusy && !isRunning; if (windowWasClosedUnexpectedly) { + console.log('[main] hidden window was closed unexpectedly'); hiddenWindowIsBusy = false; } @@ -94,11 +94,12 @@ export async function createHiddenBrowserWindow() { // if window froze const isRunningButUnhealthy = isRunning && !isHealthy; if (isRunningButUnhealthy) { + console.log('[main] hidden window is busy, stopping it'); // stop and wait for window close event and sync the map and busy status await stopAndWaitForHiddenBrowserWindow(runningHiddenBrowserWindow); } - console.log('[main] hidden window is down, restarting'); + console.log('[main] hidden window is not running, starting it'); const hiddenBrowserWindow = new BrowserWindow({ show: false, title: 'HiddenBrowserWindow', @@ -126,9 +127,8 @@ export async function createHiddenBrowserWindow() { }); const hiddenBrowserWindowPath = path.resolve(__dirname, 'hidden-window.html'); - const hiddenBrowserWindowUrl = process.env.HIDDEN_BROWSER_WINDOW_URL || pathToFileURL(hiddenBrowserWindowPath).href; - hiddenBrowserWindow.loadURL(hiddenBrowserWindowUrl); - console.log(`[main] Loading ${hiddenBrowserWindowUrl}`); + hiddenBrowserWindow.loadFile(hiddenBrowserWindowPath); + console.log(`[main] Loading ${hiddenBrowserWindowPath}`); ipcMain.removeHandler('renderer-listener-ready'); const hiddenWinListenerReady = new Promise(resolve => { @@ -254,8 +254,7 @@ export function createWindow(): ElectronBrowserWindow { }); // Load the html of the app. - const appPath = path.resolve(__dirname, './index.html'); - const appUrl = process.env.APP_RENDER_URL || pathToFileURL(appPath).href; + const appUrl = process.env.APP_RENDER_URL || 'https://insomnia-app.local'; console.log(`[main] Loading ${appUrl}`); diff --git a/packages/insomnia/src/models/__tests__/request.test.ts b/packages/insomnia/src/models/__tests__/request.test.ts index 73e991dda2..bb8fa9f9eb 100644 --- a/packages/insomnia/src/models/__tests__/request.test.ts +++ b/packages/insomnia/src/models/__tests__/request.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { describe, expect, it, vi } from 'vitest'; import { CONTENT_TYPE_GRAPHQL } from '../../common/constants'; diff --git a/packages/insomnia/src/models/environment.ts b/packages/insomnia/src/models/environment.ts index fc36fcb15f..f548921df8 100644 --- a/packages/insomnia/src/models/environment.ts +++ b/packages/insomnia/src/models/environment.ts @@ -144,7 +144,7 @@ export const decryptSecretValue = (encryptedValue: string, symmetricKey: JsonWeb return encryptedValue; } try { - const jsonWebKey = base64decode(encryptedValue, true); + const jsonWebKey = base64decode(encryptedValue, true) as crypt.AESMessage; return crypt.decryptAES(symmetricKey, jsonWebKey); } catch (error) { // return origin value if failed to decrypt diff --git a/packages/insomnia/src/models/git-repository.ts b/packages/insomnia/src/models/git-repository.ts index c1d3a7e5ec..da7fb80958 100644 --- a/packages/insomnia/src/models/git-repository.ts +++ b/packages/insomnia/src/models/git-repository.ts @@ -1,5 +1,4 @@ import { database as db } from '../common/database'; -import type { GitCredentials } from '../sync/git/git-vcs'; import type { BaseModel } from './index'; export type OauthProviderName = 'gitlab' | 'github' | 'custom'; @@ -78,3 +77,32 @@ export function remove(repo: GitRepository) { export function all() { return db.all(type); } +export interface GitAuthor { + name: string; + email: string; +} + +export interface GitRemoteConfig { + remote: string; + url: string; +} +interface GitCredentialsBase { + username: string; + password: string; +} +interface GitCredentialsOAuth { + /** + * Supported OAuth formats. + * This is needed by isomorphic-git to be able to push/pull using an oauth2 token. + * https://isomorphic-git.org/docs/en/authentication.html + */ + oauth2format?: 'github' | 'gitlab'; + username: string; + token: string; +} + +export type GitCredentials = GitCredentialsBase | GitCredentialsOAuth; + +export const isGitCredentialsOAuth = (credentials: GitCredentials): credentials is GitCredentialsOAuth => { + return 'oauth2format' in credentials; +}; diff --git a/packages/insomnia/src/models/organization.ts b/packages/insomnia/src/models/organization.ts index e3cf456340..9ae90c6702 100644 --- a/packages/insomnia/src/models/organization.ts +++ b/packages/insomnia/src/models/organization.ts @@ -14,6 +14,14 @@ export interface Organization { branding?: Branding; metadata: Metadata; } + +export interface StorageRules { + enableCloudSync: boolean; + enableLocalVault: boolean; + enableGitSync: boolean; + isOverridden: boolean; +} + export const SCRATCHPAD_ORGANIZATION_ID = 'org_scratchpad'; export const isScratchpadOrganizationId = (organizationId: string) => organizationId === SCRATCHPAD_ORGANIZATION_ID; export const isPersonalOrganization = (organization: Organization) => @@ -30,3 +38,60 @@ export const findPersonalOrganization = (organizations: Organization[], accountI }), ); }; +export interface OrganizationsResponse { + start: number; + limit: number; + length: number; + total: number; + next: string; + organizations: Organization[]; +} + +export interface UserProfileResponse { + id: string; + email: string; + name: string; + picture: string; + bio: string; + github: string; + linkedin: string; + twitter: string; + identities: any; + given_name: string; + family_name: string; +} + +export type PersonalPlanType = 'free' | 'individual' | 'team' | 'enterprise' | 'enterprise-member'; +export const formatCurrentPlanType = (type: PersonalPlanType) => { + switch (type) { + case 'free': { + return 'Hobby'; + } + case 'individual': { + return 'Individual'; + } + case 'team': { + return 'Pro'; + } + case 'enterprise': { + return 'Enterprise'; + } + case 'enterprise-member': { + return 'Enterprise Member'; + } + default: { + return 'Free'; + } + } +}; +type PaymentSchedules = 'month' | 'year'; + +export interface CurrentPlan { + isActive: boolean; + period: PaymentSchedules; + planId: string; + price: number; + quantity: number; + type: PersonalPlanType; + planName: string; +} diff --git a/packages/insomnia/src/models/project.ts b/packages/insomnia/src/models/project.ts index a810d9f5ab..2feed07d55 100644 --- a/packages/insomnia/src/models/project.ts +++ b/packages/insomnia/src/models/project.ts @@ -1,6 +1,7 @@ +import type { StorageRules } from '~/models/organization'; + import { database as db } from '../common/database'; import { generateId } from '../common/misc'; -import type { StorageRules } from '../ui/organization-utils'; import { type BaseModel } from './index'; export const name = 'Project'; diff --git a/packages/insomnia/src/models/runner-test-result.ts b/packages/insomnia/src/models/runner-test-result.ts index 69df9c4417..d80c068962 100644 --- a/packages/insomnia/src/models/runner-test-result.ts +++ b/packages/insomnia/src/models/runner-test-result.ts @@ -1,6 +1,5 @@ import type { RequestTestResult } from '../../../insomnia-scripting-environment/src/objects'; import { database as db } from '../common/database'; -import type { RunnerSource } from '../ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; import type { BaseModel } from './index'; export const name = 'Runner Test Result'; @@ -29,7 +28,7 @@ export interface ResponseInfo { export type RunnerResultPerRequestPerIteration = RunnerResultPerRequest[][]; export interface BaseRunnerTestResult { - source: RunnerSource; + source: 'runner'; iterations: number; duration: number; // millisecond avgRespTime: number; // millisecond diff --git a/packages/insomnia/src/models/settings.ts b/packages/insomnia/src/models/settings.ts index 46d0235b40..aef1383cdc 100644 --- a/packages/insomnia/src/models/settings.ts +++ b/packages/insomnia/src/models/settings.ts @@ -108,9 +108,9 @@ export async function update(settings: Settings, patch: Partial) { return updatedSettings; } -export async function patch(patch: Partial) { +export async function patch(settingsPatch: Partial) { const settings = await getOrCreate(); - const updatedSettings = await db.docUpdate(settings, patch); + const updatedSettings = await db.docUpdate(settings, settingsPatch); return updatedSettings; } diff --git a/packages/insomnia/src/models/workspace.ts b/packages/insomnia/src/models/workspace.ts index 52dc576279..eb25b2c3bf 100644 --- a/packages/insomnia/src/models/workspace.ts +++ b/packages/insomnia/src/models/workspace.ts @@ -1,6 +1,5 @@ import type { Merge } from 'type-fest'; -import { ACTIVITY_DEBUG, ACTIVITY_SPEC } from '../common/constants'; import { database as db } from '../common/database'; import { strings } from '../common/strings'; import type { BaseModel } from './index'; @@ -169,10 +168,10 @@ export function isScratchpad(workspace?: Workspace) { export const scopeToActivity = (scope: WorkspaceScope) => { switch (scope) { case WorkspaceScopeKeys.collection: { - return ACTIVITY_DEBUG; + return 'debug'; } case WorkspaceScopeKeys.design: { - return ACTIVITY_SPEC; + return 'spec'; } case WorkspaceScopeKeys.mockServer: { return 'mock-server'; @@ -181,7 +180,7 @@ export const scopeToActivity = (scope: WorkspaceScope) => { return 'environment'; } default: { - return ACTIVITY_DEBUG; + return 'debug'; } } }; diff --git a/packages/insomnia/src/network/network.ts b/packages/insomnia/src/network/network.ts index 57f5801694..7a2452759e 100644 --- a/packages/insomnia/src/network/network.ts +++ b/packages/insomnia/src/network/network.ts @@ -53,7 +53,6 @@ import * as plugins from '../plugins/index'; import { RenderError } from '../templating/render-error'; import type { RenderedRequest, RenderPurpose } from '../templating/types'; import { maskOrDecryptVaultDataIfNecessary } from '../templating/utils'; -import { type SendActionRuntime } from '../ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; import { invariant } from '../utils/invariant'; import { serializeNDJSON } from '../utils/ndjson'; import { buildQueryStringFromParams, joinUrlAndQueryString, smartEncodeUrl } from '../utils/url/querystring'; @@ -63,6 +62,10 @@ import { filterClientCertificates } from './certificate'; import { runScriptConcurrently, type TransformedExecuteScriptContext } from './concurrency'; import { addSetCookiesToToughCookieJar } from './set-cookie-util'; +export interface SendActionRuntime { + appendTimeline: (timelinePath: string, logs: string[]) => Promise; +} + export const getOrInheritAuthentication = ({ request, requestGroups, diff --git a/packages/insomnia/src/plugins/context/__tests__/app.test.ts b/packages/insomnia/src/plugins/context/__tests__/app.test.ts index a5dd583152..cbfd648dd4 100644 --- a/packages/insomnia/src/plugins/context/__tests__/app.test.ts +++ b/packages/insomnia/src/plugins/context/__tests__/app.test.ts @@ -6,7 +6,7 @@ import * as plugin from '../app'; describe('init()', () => { it('initializes correctly', async () => { const result = plugin.init(); - expect(Object.keys(result)).toEqual(['app', '__private']); + expect(Object.keys(result)).toEqual(['app']); expect(Object.keys(result.app).sort()).toEqual( ['alert', 'clipboard', 'dialog', 'getPath', 'getInfo', 'prompt', 'showSaveDialog'].sort(), ); diff --git a/packages/insomnia/src/plugins/context/app.tsx b/packages/insomnia/src/plugins/context/app.ts similarity index 75% rename from packages/insomnia/src/plugins/context/app.tsx rename to packages/insomnia/src/plugins/context/app.ts index 4fdee074e5..8a788188da 100644 --- a/packages/insomnia/src/plugins/context/app.tsx +++ b/packages/insomnia/src/plugins/context/app.ts @@ -1,22 +1,11 @@ import { getAppPlatform, getAppVersion } from 'insomnia/src/common/constants'; import type { AppContext, RenderPurpose } from 'insomnia/src/templating/types'; import { invariant } from 'insomnia/src/utils/invariant'; -import type React from 'react'; -import type ReactDOM from 'react-dom'; -export interface PrivateProperties { - loadRendererModules: () => Promise< - | { - ReactDOM: typeof ReactDOM; - React: typeof React; - } - | {} - >; -} // TODO: consider how this would work in a webworker context const isRenderer = process.type === 'renderer'; -export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContext; __private: PrivateProperties } => ({ +export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContext } => ({ app: { alert: (title: string, message?: string) => { if (isRenderer) { @@ -32,9 +21,9 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex }); } }, - prompt: (title, options = {}) => { + prompt: (title, options) => { if (!isRenderer) { - return Promise.resolve(options.defaultValue || ''); + return Promise.resolve(options?.defaultValue || ''); } // This custom promise converts the prompt modal from being callback-based to reject when the modal is cancelled and resolve when the modal is submitted and hidden return new Promise((resolve, reject) => { @@ -78,20 +67,4 @@ export const init = (renderPurpose: RenderPurpose = 'general'): { app: AppContex clear: () => window.clipboard.clear(), }, }, - __private: { - // Provide modules that can be used in the renderer process - async loadRendererModules() { - if (globalThis.document === undefined) { - return {}; - } - - const ReactDOM = await import('react-dom'); - const React = await import('react'); - - return { - ReactDOM, - React, - }; - }, - }, }); diff --git a/packages/insomnia/src/root.tsx b/packages/insomnia/src/root.tsx new file mode 100644 index 0000000000..c4b01026f4 --- /dev/null +++ b/packages/insomnia/src/root.tsx @@ -0,0 +1,556 @@ +import '~/ui/css/styles.css'; + +import type { IpcRendererEvent } from 'electron'; +import type { FC } from 'react'; +import { useEffect, useState } from 'react'; +import { Button } from 'react-aria-components'; +import { + href, + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useNavigate, + useParams, + useRouteLoaderData, +} from 'react-router'; +import { isRouteErrorResponse, useNavigation } from 'react-router'; + +import { EXTERNAL_VAULT_PLUGIN_NAME, isDevelopment } from '~/common/constants'; +import * as models from '~/models'; +import type { Settings } from '~/models/settings'; +import type { UserSession } from '~/models/user-session'; +import { executePluginMainAction, reloadPlugins } from '~/plugins'; +import { createPlugin } from '~/plugins/create'; +import { setTheme } from '~/plugins/misc'; +import { useAuthorizeActionFetcher } from '~/routes/auth.authorize'; +import { useDefaultBrowserRedirectActionFetcher } from '~/routes/auth.default-browser-redirect'; +import { useLogoutFetcher } from '~/routes/auth.logout'; +import { useCreateCloudCredentialActionFetcher } from '~/routes/cloud-credentials.create'; +import { useGithubCompleteSignInFetcher } from '~/routes/git-credentials.github.complete-sign-in'; +import { useGitLabCompleteSignInFetcher } from '~/routes/git-credentials.gitlab.complete-sign-in'; +import { SegmentEvent } from '~/ui/analytics'; +import { getLoginUrl } from '~/ui/auth-session-provider.client'; +import { CopyButton } from '~/ui/components/base/copy-button'; +import { Link } from '~/ui/components/base/link'; +import { ErrorBoundary as ErrorView } from '~/ui/components/error-boundary'; +import { Icon } from '~/ui/components/icon'; +import { showError, showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { AskModal } from '~/ui/components/modals/ask-modal'; +import { ImportModal } from '~/ui/components/modals/import-modal/import-modal'; +import { + SettingsModal, + TAB_CLOUD_CREDENTIAL, + TAB_INDEX_PLUGINS, + TAB_INDEX_THEMES, +} from '~/ui/components/modals/settings-modal'; +import { Toaster } from '~/ui/components/toast-notification'; +import { AppHooks } from '~/ui/containers/app-hooks'; +import { NunjucksEnabledProvider } from '~/ui/context/nunjucks/nunjucks-enabled-context'; +import { useThemeChange } from '~/ui/hooks/use-theme-change'; +import Modals from '~/ui/modals'; + +import type { Route } from './+types/root'; + +export const links: Route.LinksFunction = () => { + return [ + { rel: 'icon', href: '/favicon.ico' }, + { rel: 'apple-touch-icon', href: '/apple-touch-icon.png' }, + { rel: 'mask-icon', href: '/safari-pinned-tab.svg', color: '#5bbad5' }, + ]; +}; + +export const ErrorBoundary: FC = ({ error }) => { + useThemeChange(); + const getErrorMessage = (err: any) => { + if (isRouteErrorResponse(err)) { + return err.data; + } + + if (err?.message) { + return err?.message; + } + + return 'Unknown error'; + }; + + const getErrorStack = (err: any) => { + if ('error' in err) { + return err.error?.stack; + } + + return err?.stack; + }; + + const navigate = useNavigate(); + const navigation = useNavigation(); + const errorMessage = getErrorMessage(error); + const logoutFetcher = useLogoutFetcher(); + + return ( +
+

+ Application Error +

+

+ Failed to render. Please report to{' '} + + our Github Issues + +

+
+ {errorMessage} +
+
+ + +
+
+ {getErrorStack(error)} +
+
+ ); +}; + +export interface RootLoaderData { + settings: Settings; + workspaceCount: number; + userSession: UserSession; +} + +export const useRootLoaderData = () => { + return useRouteLoaderData('root'); +}; + +export async function clientLoader(_args: Route.ClientLoaderArgs) { + const settings = await models.settings.get(); + const workspaceCount = await models.workspace.count(); + const userSession = await models.userSession.getOrCreate(); + const cloudCredentials = await models.cloudCredential.all(); + + return { + settings, + workspaceCount, + userSession, + cloudCredentials, + }; +} + +export const Layout = ({ children }: { children: React.ReactNode }) => { + return ( + + + + + + + + + + {children} + + +
+
+ + + ); +}; + +export const HydrateFallback = () => { + return ( +
+
+ + + + + + + + + + + + + + + +
+
+ ); +}; + +const Root = () => { + const { organizationId, projectId } = useParams() as { + organizationId: string; + projectId: string; + }; + + const [importUri, setImportUri] = useState(''); + const { submit: createCloudCredentials } = useCreateCloudCredentialActionFetcher(); + const { submit: authorizeSubmit } = useAuthorizeActionFetcher(); + const { submit: logoutSubmit } = useLogoutFetcher(); + const { submit: githubCompleteSignInSubmit } = useGithubCompleteSignInFetcher(); + const { submit: gitLabCompleteSignInSubmit } = useGitLabCompleteSignInFetcher(); + const { submit: redirectToDefaultBrowserSubmit } = useDefaultBrowserRedirectActionFetcher(); + const navigate = useNavigate(); + + useEffect(() => { + return window.main.on('shell:open', async (_: IpcRendererEvent, url: string) => { + // Get the url without params + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch { + console.log('[deep-link] Invalid args, expected insomnia://x/y/z', url); + return; + } + let urlWithoutParams = url.slice(0, Math.max(0, url.indexOf('?'))) || url; + const params = Object.fromEntries(parsedUrl.searchParams); + // Change protocol for dev redirects to match switch case + if (isDevelopment()) { + urlWithoutParams = urlWithoutParams.replace('insomniadev://', 'insomnia://'); + } + if (urlWithoutParams === 'insomnia://app/alert') { + return showModal(AlertModal, { + title: params.title, + message: params.message, + }); + } + if (urlWithoutParams === 'insomnia://app/auth/login') { + if (params.message) { + window.localStorage.setItem('logoutMessage', params.message); + } + + return logoutSubmit(); + } + if (urlWithoutParams === 'insomnia://app/import') { + window.main.trackSegmentEvent({ + event: SegmentEvent.importStarted, + properties: { + source: 'import-url', + }, + }); + + return setImportUri(params.uri); + } + if (urlWithoutParams === 'insomnia://plugins/install') { + if (!params.name || params.name.trim() === '') { + return showError({ + title: 'Plugin Install', + message: 'Plugin name is required', + }); + } + + return showModal(AskModal, { + title: 'Plugin Install', + message: ( +

+ Do you want to install {params.name}? +

+ ), + yesText: 'Install', + noText: 'Cancel', + onDone: async (isYes: boolean) => { + if (isYes) { + try { + // TODO (pavkout): Remove second parameter when we will decide about the @scoped packages name validation + await window.main.installPlugin(params.name.trim(), true); + showModal(SettingsModal, { tab: TAB_INDEX_PLUGINS }); + } catch (err) { + showError({ + title: 'Plugin Install', + message: 'Failed to install plugin', + error: err.message, + }); + } + } + }, + }); + } + if (urlWithoutParams === 'insomnia://plugins/theme') { + const parsedTheme = JSON.parse(decodeURIComponent(params.theme)); + showModal(AskModal, { + title: 'Install Theme', + message: ( + <> + Do you want to install {parsedTheme.displayName}? + + ), + yesText: 'Install', + noText: 'Cancel', + onDone: async (isYes: boolean) => { + if (isYes) { + const mainJsContent = `module.exports.themes = [${JSON.stringify(parsedTheme, null, 2)}];`; + await createPlugin(`theme-${parsedTheme.name}`, mainJsContent); + const settings = await models.settings.get(); + await models.settings.update(settings, { + theme: parsedTheme.name, + }); + await reloadPlugins(); + await setTheme(parsedTheme.name); + showModal(SettingsModal, { tab: TAB_INDEX_THEMES }); + } + }, + }); + } + if ( + urlWithoutParams === 'insomnia://oauth/github/authenticate' || + urlWithoutParams === 'insomnia://oauth/github-app/authenticate' + ) { + const { code, state } = params; + return githubCompleteSignInSubmit({ + code, + state, + }); + } + if (urlWithoutParams === 'insomnia://oauth/gitlab/authenticate') { + const { code, state } = params; + return gitLabCompleteSignInSubmit({ + code, + state, + }); + } + if (urlWithoutParams === 'insomnia://app/auth/finish') { + return authorizeSubmit({ + code: params.box, + }); + } + if (urlWithoutParams === 'insomnia://app/open/organization') { + // if user is logged out, navigate to authorize instead + // gracefully handle open org in app from browser + const userSession = await models.userSession.getOrCreate(); + if (!userSession.id || userSession.id === '') { + const url = new URL(getLoginUrl()); + window.main.openInBrowser(url.toString()); + window.localStorage.setItem('specificOrgRedirectAfterAuthorize', params.organizationId); + return navigate(href('/auth/authorize')); + } + return navigate(`/organization/${params.organizationId}`); + } + if (urlWithoutParams === 'insomnia://system-browser-oauth/redirect') { + const { url: redirectUrl } = params; + return redirectToDefaultBrowserSubmit({ + redirectUrl, + }); + } + if (urlWithoutParams === 'insomnia://oauth/azure/authenticate') { + const { code, ...restParams } = params; + if (code && typeof code === 'string') { + const authResult = await executePluginMainAction({ + pluginName: EXTERNAL_VAULT_PLUGIN_NAME, + actionName: 'exchangeCode', + params: { provider: 'azure', code }, + }); + const { success, result, error } = authResult; + if (success) { + const { account, uniqueId } = result!; + const name = account?.username || uniqueId; + createCloudCredentials({ + name, + credentials: result, + provider: 'azure', + isAuthenticated: true, + }); + const closeModalBtn = document.getElementById('close-add-cloud-credential-modal'); + if (closeModalBtn) { + // close the modal to hint user Azure oauth url if exists + closeModalBtn.click(); + } + showModal(SettingsModal, { tab: TAB_CLOUD_CREDENTIAL }); + } else { + showError({ + title: 'Azure Authorization Failed', + message: error?.errorMessage, + }); + } + } else { + const errorDetailKeys = Object.keys(restParams); + const { error, error_description, error_uri } = restParams; + if (error && error_description) { + showError({ + title: 'Azure Authorization Failed', + message: ( +
+ {error} + {error_description} + {error_uri && ( +
+ + View Document + +
+ )} + + + +
+ ), + }); + } else { + showError({ + title: 'Azure Authorization Failed', + message: ( +
+ {errorDetailKeys.length > 0 + ? errorDetailKeys.map(k => ( + + {k}: {restParams[k]} + + )) + : 'Unknown error'} +
+ ), + }); + } + } + } + console.log(`Unknown deep link: ${url}`); + }); + }, [ + authorizeSubmit, + createCloudCredentials, + gitLabCompleteSignInSubmit, + githubCompleteSignInSubmit, + logoutSubmit, + navigate, + redirectToDefaultBrowserSubmit, + ]); + + return ( + + +
+ + +
+ + + {/* triggered by insomnia://app/import */} + {importUri && ( + setImportUri('')} + projectName="Insomnia" + defaultProjectId={projectId} + organizationId={organizationId} + from={{ type: 'uri', defaultValue: importUri }} + /> + )} +
+
+ ); +}; + +export default Root; diff --git a/packages/insomnia/src/routes.ts b/packages/insomnia/src/routes.ts new file mode 100644 index 0000000000..4efdd68ff7 --- /dev/null +++ b/packages/insomnia/src/routes.ts @@ -0,0 +1,3 @@ +import { flatRoutes } from '@react-router/fs-routes'; + +export default flatRoutes(); diff --git a/packages/insomnia/src/routes/_index.tsx b/packages/insomnia/src/routes/_index.tsx new file mode 100644 index 0000000000..63043a33ee --- /dev/null +++ b/packages/insomnia/src/routes/_index.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'react-router'; + +export async function clientLoader() { + return redirect('/organization'); +} diff --git a/packages/insomnia/src/ui/routes/auth.authorize.tsx b/packages/insomnia/src/routes/auth.authorize.tsx similarity index 77% rename from packages/insomnia/src/ui/routes/auth.authorize.tsx rename to packages/insomnia/src/routes/auth.authorize.tsx index b1963007ec..18410af1e2 100644 --- a/packages/insomnia/src/ui/routes/auth.authorize.tsx +++ b/packages/insomnia/src/routes/auth.authorize.tsx @@ -1,26 +1,29 @@ -import React, { Fragment } from 'react'; +import { Fragment, useCallback } from 'react'; import { Button, Heading } from 'react-aria-components'; -import { type ActionFunction, redirect, useFetcher, useFetchers, useNavigate } from 'react-router'; +import { href, redirect, useFetcher, useFetchers, useNavigate } from 'react-router'; -import { userSession as sessionModel } from '../../models'; -import { invariant } from '../../utils/invariant'; -import { getVaultKeyFromStorage } from '../../utils/vault'; -import { SegmentEvent } from '../analytics'; -import { getLoginUrl, submitAuthCode } from '../auth-session-provider'; -import { Icon } from '../components/icon'; -import { insomniaFetch } from '../insomniaFetch'; -import { validateVaultKey } from '../vault-key'; +import { userSession as sessionModel } from '~/models'; +import { SegmentEvent } from '~/ui/analytics'; +import { getLoginUrl, submitAuthCode } from '~/ui/auth-session-provider.client'; +import { Icon } from '~/ui/components/icon'; +import { insomniaFetch } from '~/ui/insomniaFetch'; +import { validateVaultKey } from '~/ui/vault-key.client'; +import { invariant } from '~/utils/invariant'; +import { getVaultKeyFromStorage } from '~/utils/vault'; -export const action: ActionFunction = async ({ request }) => { +import type { Route } from './+types/auth.authorize'; + +export async function clientAction({ request }: Route.ClientActionArgs) { const data = await request.json(); invariant(typeof data?.code === 'string', 'Expected code to be a string'); const error = await submitAuthCode(data.code); if (error) { + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; const humanReadableError = - error?.message === 'Failed to fetch' + errorMessage === 'Failed to fetch' ? 'Network failed, please try again. If the problem persists, check your network and proxy settings.' - : error?.message; + : errorMessage; return { errors: { message: humanReadableError, @@ -64,19 +67,39 @@ export const action: ActionFunction = async ({ request }) => { } return redirect('/organization'); -}; +} -const Authorize = () => { +export function useAuthorizeActionFetcher(args: { key?: string } = {}) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + (data: { code: string }) => { + fetcherSubmit(data, { + action: href('/auth/authorize'), + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} + +const Component = () => { const url = getLoginUrl(); const copyUrl = () => { window.clipboard.writeText(url); }; - const authorizeFetcher = useFetcher(); + const authorizeFetcher = useAuthorizeActionFetcher(); const navigate = useNavigate(); const allFetchers = useFetchers(); - const authFetchers = allFetchers.filter(f => f.formAction === '/auth/authorize'); + const authFetchers = allFetchers.filter(f => f.formAction === href('/auth/authorize')); const isAuthenticating = authFetchers.some(f => f.state !== 'idle'); // 1 first time sign up @@ -124,15 +147,9 @@ const Authorize = () => { const code = data.get('code'); invariant(typeof code === 'string', 'Expected code to be a string'); - authorizeFetcher.submit( - { - code, - }, - { - method: 'POST', - encType: 'application/json', - }, - ); + authorizeFetcher.submit({ + code, + }); }} >
@@ -163,7 +180,7 @@ const Authorize = () => { {item => ( @@ -1481,7 +1327,7 @@ const ProjectRoute: FC = () => { isOpen project={activeProject} storageRules={storageRules} - currentPlan={currentPlan} + currentPlan={organizationData?.currentPlan} scope={newWorkspaceModalState.scope} onOpenChange={isOpen => { setNewWorkspaceModalState({ @@ -1505,6 +1351,4 @@ const ProjectRoute: FC = () => { ); }; -ProjectRoute.displayName = 'ProjectRoute'; - -export default ProjectRoute; +export default Component; diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx similarity index 63% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.delete.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx index 4efc1086b3..995e40a78b 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.delete.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.delete.tsx @@ -1,12 +1,15 @@ -import { type ActionFunctionArgs, redirect } from 'react-router'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; -import { database } from '../../common/database'; -import * as models from '../../models'; -import { invariant } from '../../utils/invariant'; -import { getInitialRouteForOrganization } from '../../utils/router'; -import { insomniaFetch } from '../insomniaFetch'; +import { database } from '~/common/database'; +import * as models from '~/models'; +import { insomniaFetch } from '~/ui/insomniaFetch'; +import { invariant } from '~/utils/invariant'; +import { getInitialRouteForOrganization } from '~/utils/router'; -export async function action({ params }: ActionFunctionArgs) { +import type { Route } from './+types/organization.$organizationId.project.$projectId.delete'; + +export async function clientAction({ params }: Route.ClientActionArgs) { const { organizationId, projectId } = params; invariant(organizationId, 'Organization ID is required'); invariant(projectId, 'Project ID is required'); @@ -62,3 +65,30 @@ export async function action({ params }: ActionFunctionArgs) { }; } } + +export function useProjectDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ organizationId, projectId }: { organizationId: string; projectId: string }) => { + const url = href('/organization/:organizationId/project/:projectId/delete', { + organizationId, + projectId, + }); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.list-workspaces.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.list-workspaces.tsx new file mode 100644 index 0000000000..f34c3b6f11 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.list-workspaces.tsx @@ -0,0 +1,157 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { parseApiSpec, type ParsedApiSpec } from '~/common/api-specs'; +import { database } from '~/common/database'; +import { isNotNullOrUndefined } from '~/common/misc'; +import { descendingNumberSort } from '~/common/sorting'; +import * as models from '~/models'; +import { type ApiSpec } from '~/models/api-spec'; +import type { GitRepository } from '~/models/git-repository'; +import { sortProjects } from '~/models/helpers/project'; +import type { MockServer } from '~/models/mock-server'; +import { type Project } from '~/models/project'; +import { isDesign } from '~/models/workspace'; +import type { WorkspaceMeta } from '~/models/workspace-meta'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.list-workspaces'; +import { type InsomniaFile, scopeToLabelMap } from './organization.$organizationId.project.$projectId._index'; + +async function getAllLocalFiles({ projectId }: { projectId: string }) { + const projectWorkspaces = await models.workspace.findByParentId(projectId); + const [workspaceMetas, apiSpecs, mockServers] = await Promise.all([ + database.find(models.workspaceMeta.type, { + parentId: { + $in: projectWorkspaces.map(w => w._id), + }, + }), + database.find(models.apiSpec.type, { + parentId: { + $in: projectWorkspaces.map(w => w._id), + }, + }), + database.find(models.mockServer.type, { + parentId: { + $in: projectWorkspaces.map(w => w._id), + }, + }), + ]); + + const gitRepositories = await database.find(models.gitRepository.type, { + parentId: { + $in: workspaceMetas.map(wm => wm.gitRepositoryId).filter(isNotNullOrUndefined), + }, + }); + + const files: InsomniaFile[] = projectWorkspaces.map(workspace => { + const apiSpec = apiSpecs.find(spec => spec.parentId === workspace._id); + const mockServer = mockServers.find(mock => mock.parentId === workspace._id); + let spec: ParsedApiSpec['contents'] = null; + let specFormat: ParsedApiSpec['format'] = null; + let specFormatVersion: ParsedApiSpec['formatVersion'] = null; + if (apiSpec) { + try { + const result = parseApiSpec(apiSpec.contents); + spec = result.contents; + specFormat = result.format; + specFormatVersion = result.formatVersion; + } catch { + // Assume there is no spec + // TODO: Check for parse errors if it's an invalid spec + } + } + const workspaceMeta = workspaceMetas.find(wm => wm.parentId === workspace._id); + const gitRepository = gitRepositories.find(gr => gr._id === workspaceMeta?.gitRepositoryId); + + const lastActiveBranch = gitRepository?.cachedGitRepositoryBranch; + + const lastCommitAuthor = gitRepository?.cachedGitLastAuthor; + + // WorkspaceMeta is a good proxy for last modified time + const workspaceModified = workspaceMeta?.modified || workspace.modified; + + const modifiedLocally = isDesign(workspace) ? apiSpec?.modified || 0 : workspaceModified; + + // Span spec, workspace and sync related timestamps for card last modified label and sort order + const lastModifiedFrom = [ + workspace?.modified, + workspaceMeta?.modified, + modifiedLocally, + gitRepository?.cachedGitLastCommitTime, + ]; + + const lastModifiedTimestamp = lastModifiedFrom.filter(isNotNullOrUndefined).sort(descendingNumberSort)[0]; + + const hasUnsavedChanges = Boolean( + isDesign(workspace) && + gitRepository?.cachedGitLastCommitTime && + modifiedLocally > gitRepository?.cachedGitLastCommitTime, + ); + + const specVersion = spec?.info?.version ? String(spec?.info?.version) : ''; + + return { + id: workspace._id, + name: workspace.name, + scope: workspace.scope, + label: scopeToLabelMap[workspace.scope], + created: workspace.created, + lastModifiedTimestamp: + (hasUnsavedChanges && modifiedLocally) || gitRepository?.cachedGitLastCommitTime || lastModifiedTimestamp, + branch: lastActiveBranch || '', + lastCommit: + hasUnsavedChanges && gitRepository?.cachedGitLastCommitTime && lastCommitAuthor ? `by ${lastCommitAuthor}` : '', + version: specVersion ? `${specVersion?.startsWith('v') ? '' : 'v'}${specVersion}` : '', + oasFormat: specFormat ? `${specFormat === 'openapi' ? 'OpenAPI' : 'Swagger'} ${specFormatVersion || ''}` : '', + mockServer, + apiSpec, + workspace, + hasUncommittedChanges: workspaceMeta?.hasUncommittedChanges, + hasUnpushedChanges: workspaceMeta?.hasUnpushedChanges, + gitFilePath: workspaceMeta?.gitFilePath, + }; + }); + return files; +} + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId } = params; + + const project = await models.project.getById(projectId); + invariant(project, `Project was not found ${projectId}`); + const organizationProjects = + (await database.find(models.project.type, { + parentId: organizationId, + })) || []; + + const projects = sortProjects(organizationProjects); + const files = await getAllLocalFiles({ projectId }); + + return { + files, + activeProject: project, + projects, + }; +} + +export function useProjectListWorkspacesLoaderFetcher(args?: Parameters[0]) { + const { load: fetcherLoad, ...fetcherRest } = useFetcher(args); + + const load = useCallback( + ({ organizationId, projectId }: { organizationId: string; projectId: string }) => { + return fetcherLoad( + href('/organization/:organizationId/project/:projectId/list-workspaces', { + organizationId, + projectId, + }), + ); + }, + [fetcherLoad], + ); + + return { + ...fetcherRest, + load, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move-workspace.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move-workspace.tsx new file mode 100644 index 0000000000..e1b10c9427 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move-workspace.tsx @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.move-workspace'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const formData = await request.formData(); + const projectId = formData.get('projectId'); + const workspaceId = formData.get('workspaceId'); + invariant(typeof projectId === 'string', 'Project ID is required'); + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + invariant(typeof workspaceId === 'string', 'Workspace ID is required'); + const workspace = await models.workspace.getById(workspaceId); + invariant(workspace, 'Workspace not found'); + + await models.workspace.update(workspace, { + parentId: projectId, + }); + + return null; +} + +export function useProjectMoveWorkspaceActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + (organizationId: string, projectId: string, workspaceId: string) => { + const formData = new FormData(); + formData.set('projectId', projectId); + formData.set('workspaceId', workspaceId); + + return fetcherSubmit(formData, { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/move-workspace`, { + organizationId, + projectId, + }), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move.tsx new file mode 100644 index 0000000000..da8d023104 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.move.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.move'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { projectId } = params as { projectId: string }; + const formData = await request.formData(); + + const organizationId = formData.get('organizationId'); + + invariant(typeof organizationId === 'string', 'Organization ID is required'); + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + await models.project.update(project, { + parentId: organizationId, + // We move a project to another organization as local no matter what it was before + remoteId: null, + }); + + return null; +} + +export function useProjectMoveActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + currentOrganizationId, + projectId, + newOrganizationId, + }: { + currentOrganizationId: string; + projectId: string; + newOrganizationId: string; + }) => { + return fetcherSubmit( + { + organizationId: newOrganizationId, + }, + { + method: 'POST', + action: href('/organization/:organizationId/project/:projectId/move', { + organizationId: currentOrganizationId, + projectId, + }), + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx new file mode 100644 index 0000000000..3e4eb41d93 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.tsx @@ -0,0 +1,22 @@ +import { useRouteLoaderData } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId'; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { projectId } = params; + invariant(projectId, 'Project ID is required'); + + const project = await models.project.getById(projectId); + invariant(project, `Project was not found ${projectId}`); + + return { + activeProject: project, + }; +} + +export function useProjectLoaderData() { + return useRouteLoaderData('routes/organization.$organizationId.project.$projectId'); +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx similarity index 74% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.update.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx index 7c707db81b..746dee5ea5 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.update.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.update.tsx @@ -1,28 +1,40 @@ -import { type ActionFunctionArgs } from 'react-router'; +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; -import { database } from '../../common/database'; -import * as models from '../../models'; -import type { WorkspaceMeta } from '../../models/workspace-meta'; -import { invariant } from '../../utils/invariant'; -import { SegmentEvent } from '../analytics'; -import { insomniaFetch } from '../insomniaFetch'; +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { OauthProviderName } from '~/models/git-credentials'; +import type { GitCredentials } from '~/models/git-repository'; +import type { WorkspaceMeta } from '~/models/workspace-meta'; +import { SegmentEvent } from '~/ui/analytics'; +import { insomniaFetch } from '~/ui/insomniaFetch'; +import { invariant } from '~/utils/invariant'; -export interface UpdateProjectActionResult { - error?: string; - success?: boolean; +import type { Route } from './+types/organization.$organizationId.project.$projectId.update'; + +interface UpdateProjectInputData { + name: string; + storageType: 'local' | 'remote' | 'git'; + authorName?: string; + authorEmail?: string; + uri?: string; + ref?: string; + username?: string; + password?: string; + token?: string; + oauth2format?: OauthProviderName; + connectRepositoryLater?: boolean; } -export async function action({ request, params }: ActionFunctionArgs) { - const { name, storageType, ...projectData } = await request.json(); +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { name, storageType, ...projectData } = (await request.json()) as UpdateProjectInputData; invariant(typeof name === 'string', 'Name is required'); invariant(storageType === 'local' || storageType === 'remote' || storageType === 'git', 'Project type is required'); const { organizationId, projectId } = params; - invariant(projectId, 'Project ID is required'); const project = await models.project.getById(projectId); - invariant(project, 'Project not found'); const user = await models.userSession.getOrCreate(); @@ -210,10 +222,34 @@ export async function action({ request, params }: ActionFunctionArgs) { if (projectData.connectRepositoryLater) { await models.project.update(project, { name, gitRepositoryId: 'empty' }); } else { + let credentials: GitCredentials | undefined = undefined; + if (projectData.oauth2format) { + credentials = { + oauth2format: projectData.oauth2format, + token: projectData.token ?? '', + username: projectData.username ?? '', + }; + } else if (projectData.username && projectData.password) { + credentials = { + username: projectData.username, + password: projectData.password, + }; + } + const { errors } = await window.main.git.cloneGitRepo({ organizationId, cloneIntoProjectId: project._id, - ...projectData, + author: { + name: projectData.authorName ?? '', + email: projectData.authorEmail ?? '', + }, + uri: projectData.uri ?? '', + credentials: credentials || { + username: '', + password: '', + }, + ref: projectData.ref, + name, }); const projectWorkspaces = await models.workspace.findByParentId(project._id); @@ -279,3 +315,34 @@ export async function action({ request, params }: ActionFunctionArgs) { }; } } + +export function useProjectUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + projectData, + }: { + organizationId: string; + projectId: string; + projectData: UpdateProjectInputData; + }) => { + return fetcherSubmit(JSON.stringify(projectData), { + method: 'POST', + action: href('/organization/:organizationId/project/:projectId/update', { + organizationId, + projectId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete.tsx new file mode 100644 index 0000000000..bba4772950 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete.tsx @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.delete'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const caCertificate = await models.caCertificate.findByParentId(workspaceId); + invariant(caCertificate, 'CA Certificate not found'); + await models.caCertificate.removeWhere(workspaceId); + return null; +} + +export function useCaCertDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/cacert/delete', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.new.tsx new file mode 100644 index 0000000000..83bcd2cbc8 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.new.tsx @@ -0,0 +1,48 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.new'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = await request.json(); + await models.caCertificate.create(patch); + return null; +} + +export function useCACertNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: Record; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/cacert/new', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.update.tsx new file mode 100644 index 0000000000..89cb7482bb --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.update.tsx @@ -0,0 +1,56 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { CaCertificate } from '~/models/ca-certificate'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.cacert.update'; + +type CertificatePatch = { _id: string } & Partial; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = (await request.json()) as CertificatePatch; + const caCertificate = await models.caCertificate.getById(patch._id); + invariant(caCertificate, 'CA Certificate not found'); + + await models.caCertificate.update(caCertificate, patch); + + return null; +} + +export function useCACertUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: CertificatePatch; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/cacert/update', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete.tsx new file mode 100644 index 0000000000..d357db98b8 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete.tsx @@ -0,0 +1,52 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.delete'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const { _id } = await request.json(); + const clientCertificate = await models.clientCertificate.getById(_id); + invariant(clientCertificate, 'CA Certificate not found'); + + await models.clientCertificate.remove(clientCertificate); + return null; +} + +export function useClientCertDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + _id, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + _id: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/clientcert/delete', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify({ _id }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new.tsx new file mode 100644 index 0000000000..3a3886a32d --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new.tsx @@ -0,0 +1,50 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { ClientCertificate } from '~/models/client-certificate'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.new'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = await request.json(); + const certificate = await models.clientCertificate.create(patch); + + return { + certificate, + }; +} + +export function useClientCertNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: Partial; + }) => { + return fetcherSubmit(JSON.stringify(patch), { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/clientcert/new`, { + organizationId, + projectId, + workspaceId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update.tsx new file mode 100644 index 0000000000..83a8c8fcd4 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update.tsx @@ -0,0 +1,56 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { ClientCertificate } from '~/models/client-certificate'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.clientcert.update'; + +type CertificatePatch = { _id: string } & Partial; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = (await request.json()) as CertificatePatch; + const clientCertificate = await models.clientCertificate.getById(patch._id); + invariant(clientCertificate, 'Client Certificate not found'); + + await models.clientCertificate.update(clientCertificate, patch); + + return null; +} + +export function useClientCertUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: CertificatePatch; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/clientcert/update', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder.tsx new file mode 100644 index 0000000000..29590dffa5 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder.tsx @@ -0,0 +1,86 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { getById, update } from '~/models/helpers/request-operations'; +import { isRequestGroup, isRequestGroupId } from '~/models/request-group'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder'; + +const getCollectionItem = async (id: string) => { + let item; + if (isRequestGroupId(id)) { + item = await models.requestGroup.getById(id); + } else { + item = await getById(id); + } + + invariant(item, 'Item not found'); + + return item; +}; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const { id, targetId, dropPosition, metaSortKey } = await request.json(); + invariant(typeof id === 'string', 'ID is required'); + invariant(typeof targetId === 'string', 'Target ID is required'); + invariant(typeof dropPosition === 'string', 'Drop position is required'); + invariant(typeof metaSortKey === 'number', 'MetaSortKey position is required'); + + if (id === targetId) { + return null; + } + + const item = await getCollectionItem(id); + const targetItem = await getCollectionItem(targetId); + + const parentId = dropPosition === 'after' && isRequestGroup(targetItem) ? targetItem._id : targetItem.parentId; + + if (isRequestGroup(item)) { + await models.requestGroup.update(item, { parentId, metaSortKey }); + } else { + await update(item, { parentId, metaSortKey }); + } + + return null; +} + +export function useDebugReorderActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + params, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + params: { + id: string; + targetId: string; + dropPosition: string; + metaSortKey: number; + }; + }) => { + return fetcherSubmit(JSON.stringify(params), { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/reorder`, { + organizationId, + projectId, + workspaceId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.tsx new file mode 100644 index 0000000000..e2013950da --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.tsx @@ -0,0 +1,35 @@ +import { href, redirect, useRouteLoaderData } from 'react-router'; + +import * as models from '~/models'; +import type { RequestGroup } from '~/models/request-group'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; + +export interface RequestGroupLoaderData { + activeRequestGroup: RequestGroup; +} + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId, requestGroupId, workspaceId } = params; + + const activeRequestGroup = await models.requestGroup.getById(requestGroupId); + if (!activeRequestGroup) { + throw redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', { + organizationId, + projectId, + workspaceId, + }), + ); + } + + return { + activeRequestGroup, + }; +} + +export function useRequestGroupLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId', + ); +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update-meta.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update-meta.tsx new file mode 100644 index 0000000000..b8715127a0 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update-meta.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { RequestGroupMeta } from '~/models/request-group-meta'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { requestGroupId } = params; + invariant(typeof requestGroupId === 'string', 'Request Group ID is required'); + const patch = (await request.json()) as Partial; + const requestGroupMeta = await models.requestGroupMeta.getByParentId(requestGroupId); + if (requestGroupMeta) { + await models.requestGroupMeta.update(requestGroupMeta, patch); + return null; + } + await models.requestGroupMeta.create({ parentId: requestGroupId, collapsed: Boolean(patch?.collapsed) }); + return null; +} + +export function useRequestGroupUpdateMetaActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestGroupId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestGroupId: string; + patch: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId/update-meta', + { + organizationId, + projectId, + workspaceId, + requestGroupId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update.tsx new file mode 100644 index 0000000000..4999f5aed2 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { RequestGroup } from '~/models/request-group'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId.update-meta'; + +export async function clientAction({ request, params }: Route.ActionArgs) { + const { requestGroupId } = params; + + const reqGroup = await models.requestGroup.getById(requestGroupId); + invariant(reqGroup, 'Request Group not found'); + + const patch = (await request.json()) as Partial; + + await models.requestGroup.update(reqGroup, patch); + + return null; +} + +export function useRequestGroupUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestGroupId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestGroupId: string; + patch: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/:requestGroupId/update', + { + organizationId, + projectId, + workspaceId, + requestGroupId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete.tsx new file mode 100644 index 0000000000..ef7e47f07b --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete.tsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.delete'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const formData = await request.formData(); + const id = formData.get('id') as string; + + const requestGroup = await models.requestGroup.getById(id); + invariant(requestGroup, 'Request Group not found'); + + models.stats.incrementDeletedRequestsForDescendents(requestGroup); + + await models.requestGroup.remove(requestGroup); + + return null; +} + +export function useRequestGroupDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + id, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + id: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/delete', + { + organizationId, + projectId, + workspaceId, + }, + ); + + const formData = new FormData(); + formData.set('id', id); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.duplicate.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.duplicate.tsx new file mode 100644 index 0000000000..7e9f4f7d6d --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.duplicate.tsx @@ -0,0 +1,76 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { RequestGroup } from '~/models/request-group'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.duplicate'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = (await request.json()) as Partial; + invariant(patch._id, 'Request group id not found'); + + const requestGroup = await models.requestGroup.getById(patch._id); + invariant(requestGroup, 'Request group not found'); + + if (patch.parentId) { + const workspace = await models.workspace.getById(patch.parentId); + invariant(workspace, 'Workspace is required'); + // TODO: if gRPC, we should also copy the protofile to the destination workspace - INS-267 + // Move to top of sort order + const newRequestGroup = await models.requestGroup.duplicate(requestGroup, { + name: patch.name, + parentId: patch.parentId, + metaSortKey: -1e9, + }); + + models.stats.incrementCreatedRequestsForDescendents(newRequestGroup); + + return null; + } + + const newRequestGroup = await models.requestGroup.duplicate(requestGroup, { name: patch.name }); + + models.stats.incrementCreatedRequestsForDescendents(newRequestGroup); + + return null; +} + +export function useRequestGroupDuplicateActionFetcher(args?: Parameters[0]) { + const { + submit: fetcherSubmit, + ...fetcherRest + } = useFetcher(args); + + const submit = useCallback(( + { + organizationId, + projectId, + workspaceId, + requestGroupData, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestGroupData: Partial; + } + ) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/duplicate', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify(requestGroupData), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, [fetcherSubmit]); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new.tsx new file mode 100644 index 0000000000..56e410ef6f --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new.tsx @@ -0,0 +1,68 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { EnvironmentType } from '~/models/environment'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + const formData = await request.formData(); + const name = formData.get('name') as string; + const parentId = formData.get('parentId') as string; + // New folder environment to be key-value pair by default; + const environmentType = (formData.get('environmentType') as EnvironmentType) || EnvironmentType.KVPAIR; + const requestGroup = await models.requestGroup.create({ parentId: parentId || workspaceId, name, environmentType }); + + await models.requestGroupMeta.create({ parentId: requestGroup._id, collapsed: false }); + + return null; +} + +export function useRequestGroupNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + name, + parentId, + environmentType, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + name: string; + parentId?: string; + environmentType?: EnvironmentType; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request-group/new', + { + organizationId, + projectId, + workspaceId, + }, + ); + + const formData = new FormData(); + formData.set('name', name); + if (parentId) formData.set('parentId', parentId); + if (environmentType) formData.set('environmentType', environmentType); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect.tsx new file mode 100644 index 0000000000..c2a10e9dea --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect.tsx @@ -0,0 +1,147 @@ +import { GRAPHQL_TRANSPORT_WS_PROTOCOL, MessageType } from 'graphql-ws'; +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import type { ChangeBufferEvent } from '~/common/database'; +import { database } from '~/common/database'; +import type { CookieJar } from '~/models/cookie-jar'; +import * as requestOperations from '~/models/helpers/request-operations'; +import type { RequestAuthentication, RequestHeader } from '~/models/request'; +import { isEventStreamRequest, isGraphqlSubscriptionRequest } from '~/models/request'; +import { isRequestMeta } from '~/models/request-meta'; +import { isSocketIORequest } from '~/models/socket-io-request'; +import { isWebSocketRequestId } from '~/models/websocket-request'; +import { getAuthHeader } from '~/network/authentication'; +import type { RenderedRequest } from '~/templating/types'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.connect'; + +export interface ConnectActionParams { + url: string; + headers: RequestHeader[]; + authentication: RequestAuthentication; + cookieJar: CookieJar; + suppressUserAgent: boolean; + query?: Record; +} + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { requestId, workspaceId } = params; + + const req = await requestOperations.getById(requestId); + invariant(req, 'Request not found'); + invariant(workspaceId, 'Workspace ID is required'); + const rendered = (await request.json()) as ConnectActionParams; + + if (isWebSocketRequestId(requestId)) { + window.main.webSocket.open({ + requestId, + workspaceId, + url: rendered.url, + headers: rendered.headers, + authentication: rendered.authentication, + cookieJar: rendered.cookieJar, + }); + } + if (isGraphqlSubscriptionRequest(req)) { + window.main.webSocket.open({ + requestId, + workspaceId, + // replace url with ws/wss for graphql subscriptions + url: rendered.url.replace('http', 'ws').replace('https', 'wss'), + headers: [ + ...rendered.headers, + // add graphql-transport-ws protocol for graphql subscription + { + name: 'sec-websocket-protocol', + value: GRAPHQL_TRANSPORT_WS_PROTOCOL, + }, + ], + isGraphqlSubscriptionRequest: true, + // graphql-ws protocol needs to send ConnectionInit message first. Refer: https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md + initialPayload: JSON.stringify({ + type: MessageType.ConnectionInit, + }), + authentication: rendered.authentication, + cookieJar: rendered.cookieJar, + }); + } + if (isEventStreamRequest(req)) { + const renderedRequest = { ...req, ...rendered } as RenderedRequest; + const authHeader = await getAuthHeader(renderedRequest, rendered.url); + window.main.curl.open({ + requestId, + workspaceId, + url: rendered.url, + headers: rendered.headers, + authHeader, + authentication: rendered.authentication, + cookieJar: rendered.cookieJar, + suppressUserAgent: rendered.suppressUserAgent, + }); + } + if (isSocketIORequest(req)) { + window.main.socketIO.open({ + requestId, + workspaceId, + url: rendered.url, + headers: rendered.headers, + cookieJar: rendered.cookieJar, + query: rendered.query || {}, + }); + } + // HACK: even more elaborate hack to get the request to update + return new Promise(resolve => { + database.onChange(async (changes: ChangeBufferEvent[]) => { + for (const change of changes) { + const [event, doc] = change; + if (isRequestMeta(doc) && doc.parentId === requestId && event === 'update') { + resolve(null); + } + } + }); + }); +} + +export function useRequestConnectActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + connectParams, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + connectParams: ConnectActionParams; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/connect', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit(JSON.stringify(connectParams), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate.tsx new file mode 100644 index 0000000000..dc76d3d784 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate.tsx @@ -0,0 +1,87 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import * as requestOperations from '~/models/helpers/request-operations'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId, requestId } = params; + const { name, parentId } = await request.json(); + + const req = await requestOperations.getById(requestId); + invariant(req, 'Request not found'); + + if (parentId) { + const workspace = await models.workspace.getById(parentId); + invariant(workspace, 'Workspace is required'); + // TODO: if gRPC, we should also copy the protofile to the destination workspace - INS-267 + // Move to top of sort order + const newRequest = await requestOperations.duplicate(req, { name, parentId, metaSortKey: -1e9 }); + invariant(newRequest, 'Failed to duplicate request'); + + models.stats.incrementCreatedRequests(); + + return null; + } + + const newRequest = await requestOperations.duplicate(req, { name }); + invariant(newRequest, 'Failed to duplicate request'); + + models.stats.incrementCreatedRequests(); + + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId', { + organizationId, + projectId, + workspaceId, + requestId: newRequest._id, + }), + ); +} + +export function useRequestDuplicateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + name, + parentId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + name: string; + parentId?: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/duplicate', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit(JSON.stringify({ name, parentId }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx new file mode 100644 index 0000000000..58dac3b625 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete-all.tsx @@ -0,0 +1,72 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import * as requestOperations from '~/models/helpers/request-operations'; +import { isSocketIORequestId } from '~/models/socket-io-request'; +import { isWebSocketRequestId } from '~/models/websocket-request'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { workspaceId, requestId } = params; + + const req = await requestOperations.getById(requestId); + invariant(req, 'Request not found'); + + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + invariant(workspaceMeta, 'Active workspace meta not found'); + + if (isWebSocketRequestId(requestId)) { + await models.webSocketResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); + } else if (isSocketIORequestId(requestId)) { + await models.socketIOResponse.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); + } else { + await models.response.removeForRequest(requestId, workspaceMeta.activeEnvironmentId); + } + + return null; +} + +export function useRequestResponseDeleteAllActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/response/delete-all', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx new file mode 100644 index 0000000000..30a95dd5be --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete.tsx @@ -0,0 +1,95 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import * as requestOperations from '~/models/helpers/request-operations'; +import { isSocketIORequestId } from '~/models/socket-io-request'; +import { isWebSocketRequestId } from '~/models/websocket-request'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.response.delete'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId, requestId } = params; + + const req = await requestOperations.getById(requestId); + invariant(req, 'Request not found'); + + const { responseId } = await request.json(); + invariant(typeof responseId === 'string', 'Response ID is required'); + + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + invariant(workspaceMeta, 'Active workspace meta not found'); + + if (isWebSocketRequestId(requestId)) { + const res = await models.webSocketResponse.getById(responseId); + invariant(res, 'Response not found'); + await models.webSocketResponse.remove(res); + const response = await models.webSocketResponse.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId); + if (response?.requestVersionId) { + await models.requestVersion.restore(response.requestVersionId); + } + await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); + } else if (isSocketIORequestId(requestId)) { + const res = await models.socketIOResponse.getById(responseId); + invariant(res, 'Response not found'); + await models.socketIOResponse.remove(res); + const response = await models.socketIOResponse.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId); + if (response?.requestVersionId) { + await models.requestVersion.restore(response.requestVersionId); + } + await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); + } else { + const res = await models.response.getById(responseId); + invariant(res, 'Response not found'); + await models.response.remove(res); + const response = await models.response.getLatestForRequest(requestId, workspaceMeta.activeEnvironmentId); + if (response?.requestVersionId) { + await models.requestVersion.restore(response.requestVersionId); + } + await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: response?._id || null }); + } + + return null; +} + +export function useRequestResponseDeleteActionFetcher(args?: Parameters[0]) { + const { + submit: fetcherSubmit, + ...fetcherRest + } = useFetcher(args); + + const submit = useCallback(( + { + organizationId, + projectId, + workspaceId, + requestId, + responseId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + responseId: string; + } + ) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/response/delete', { + organizationId, + projectId, + workspaceId, + requestId, + }); + + return fetcherSubmit(JSON.stringify({ responseId }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, [fetcherSubmit]); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx new file mode 100644 index 0000000000..d625f47077 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send.tsx @@ -0,0 +1,408 @@ +import { createWriteStream } from 'node:fs'; +import path from 'node:path'; + +import contentDisposition from 'content-disposition'; +import { extension as mimeExtension } from 'mime-types'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; +import { v4 as uuidv4 } from 'uuid'; + +import { getContentDispositionHeader } from '~/common/misc'; +import type { ResponsePatch } from '~/main/network/libcurl-promise'; +import type { TimingStep } from '~/main/network/request-timing'; +import * as models from '~/models'; +import type { Environment, UserUploadEnvironment } from '~/models/environment'; +import type { RequestMeta } from '~/models/request-meta'; +import type { ResponseInfo, RunnerResultPerRequestPerIteration } from '~/models/runner-test-result'; +import { + defaultSendActionRuntime, + fetchRequestData, + responseTransform, + type SendActionRuntime, + sendCurlAndWriteTimeline, + tryToExecuteAfterResponseScript, + tryToExecutePreRequestScript, + tryToInterpolateRequest, + tryToTransformRequestWithPlugins, +} from '~/network/network'; +import { parseGraphQLReqeustBody } from '~/utils/graph-ql'; +import { invariant } from '~/utils/invariant'; + +import type { RequestTestResult } from '../../../insomnia-scripting-environment/src/objects'; +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send'; + +export interface SendActionParams { + requestId: string; + shouldPromptForPathAfterResponse?: boolean; + ignoreUndefinedEnvVariable?: boolean; +} + +export interface CollectionRunnerContext { + source: 'runner'; + environmentId: string; + iterationCount: number; + iterationData: object; + duration: number; // millisecond + testCount: number; + avgRespTime: number; // millisecond + iterationResults: RunnerResultPerRequestPerIteration; + done: boolean; + responsesInfo: ResponseInfo[]; + transientVariables: Environment; +} + +export interface RunnerContextForRequest { + requestId: string; + requestName: string; + requestUrl: string; + statusCode: number; + duration: number; // millisecond + size: number; + results: RequestTestResult[]; + responseId: string; +} + +const writeToDownloadPath = ( + downloadPathAndName: string, + responsePatch: ResponsePatch, + requestMeta: RequestMeta, + maxHistoryResponses: number, +) => { + invariant(downloadPathAndName, 'filename should be set by now'); + + const to = createWriteStream(downloadPathAndName); + const readStream = models.response.getBodyStream(responsePatch); + if (!readStream || typeof readStream === 'string') { + return null; + } + readStream.pipe(to); + + return new Promise(resolve => { + readStream.on('end', async () => { + responsePatch.error = `Saved to ${downloadPathAndName}`; + const response = await models.response.create(responsePatch, maxHistoryResponses); + await models.requestMeta.update(requestMeta, { activeResponseId: response._id }); + resolve(null); + }); + readStream.on('error', async err => { + console.warn('Failed to download request after sending', responsePatch.bodyPath, err); + const response = await models.response.create(responsePatch, maxHistoryResponses); + await models.requestMeta.update(requestMeta, { activeResponseId: response._id }); + resolve(null); + }); + }); +}; + +export const sendActionImplementation = async (options: { + requestId: string; + shouldPromptForPathAfterResponse: boolean | undefined; + ignoreUndefinedEnvVariable: boolean | undefined; + testResultCollector?: RunnerContextForRequest; + iteration?: number; + iterationCount?: number; + userUploadEnvironment?: UserUploadEnvironment; + transientVariables?: Environment; + runtime?: SendActionRuntime; +}) => { + const { + requestId, + userUploadEnvironment, + shouldPromptForPathAfterResponse, + ignoreUndefinedEnvVariable, + testResultCollector, + iteration, + iterationCount, + transientVariables: nullableTransientVariables, + runtime = defaultSendActionRuntime, + } = options; + + window.main.startExecution({ requestId }); + const requestData = await fetchRequestData(requestId); + const requestMeta = await models.requestMeta.getByParentId(requestId); + const transientVariables = nullableTransientVariables || { + ...models.environment.init(), + _id: uuidv4(), + type: models.environment.type, + parentId: requestData.environment.parentId, + modified: 0, + created: Date.now(), + name: 'Transient Environment', + data: {}, + }; + + window.main.addExecutionStep({ requestId, stepName: 'Executing pre-request script' }); + const mutatedContext = await tryToExecutePreRequestScript( + requestData, + transientVariables, + userUploadEnvironment, + iteration, + iterationCount, + runtime, + ); + if ('error' in mutatedContext) { + window.main.completeExecutionStep({ requestId }); + throw { + // create response with error info, so that we can store response in db and show it in response viewer + response: { + _id: requestData.responseId, + parentId: requestId, + environemntId: requestData.environment, + statusMessage: 'Error', + error: mutatedContext.error, + }, + maxHistoryResponses: requestData.settings.maxHistoryResponses, + requestMeta, + error: mutatedContext.error, + }; + } + if (mutatedContext.execution?.skipRequest) { + // cancel request running if skipRequest in pre-request script + const responseId = requestData.responseId; + const responsePatch = { + _id: responseId, + parentId: requestId, + environemntId: requestData.environment, + statusMessage: 'Cancelled', + error: 'Request was cancelled by pre-request script', + }; + // create and update response to activeResponse + await models.response.create(responsePatch, requestData.settings.maxHistoryResponses); + await models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: responseId }); + window.main.completeExecutionStep({ requestId }); + return mutatedContext; + } + + window.main.completeExecutionStep({ requestId }); + + // disable after-response script here to avoiding rendering it + // @TODO This should be handled in a better way. Maybe remove the key from the request object we pass in tryToInterpolateRequest + const afterResponseScript = mutatedContext.request.afterResponseScript + ? `${mutatedContext.request.afterResponseScript}` + : undefined; + mutatedContext.request.afterResponseScript = ''; + + window.main.addExecutionStep({ requestId, stepName: 'Rendering request' }); + const renderedResult = await tryToInterpolateRequest({ + request: mutatedContext.request, + environment: mutatedContext.environment, + purpose: 'send', + extraInfo: undefined, + baseEnvironment: mutatedContext.baseEnvironment, + userUploadEnvironment: mutatedContext.userUploadEnvironment, + transientVariables: mutatedContext.transientVariables, + ignoreUndefinedEnvVariable, + }); + const renderedRequest = await tryToTransformRequestWithPlugins(renderedResult); + window.main.completeExecutionStep({ requestId }); + + // TODO: remove this temporary hack to support GraphQL variables in the request body properly + parseGraphQLReqeustBody(renderedRequest); + + invariant(requestMeta, 'RequestMeta not found'); + + window.main.addExecutionStep({ requestId, stepName: 'Sending request' }); + const response = await sendCurlAndWriteTimeline( + renderedRequest, + mutatedContext.clientCertificates, + requestData.caCert, + mutatedContext.settings, + requestData.timelinePath, + requestData.responseId, + runtime, + ); + window.main.completeExecutionStep({ requestId }); + if ('error' in response) { + throw { + response: await responseTransform( + response, + requestData.activeEnvironmentId, + renderedRequest, + renderedResult.context, + ), + maxHistoryResponses: requestData.settings.maxHistoryResponses, + requestMeta, + error: response.error, + }; + } + + const baseResponsePatch = await responseTransform( + response, + requestData.activeEnvironmentId, + renderedRequest, + renderedResult.context, + ); + const is2XXWithBodyPath = + baseResponsePatch.statusCode && + baseResponsePatch.statusCode >= 200 && + baseResponsePatch.statusCode < 300 && + baseResponsePatch.bodyPath; + const shouldWriteToFile = shouldPromptForPathAfterResponse && is2XXWithBodyPath; + + mutatedContext.request.afterResponseScript = afterResponseScript; + window.main.addExecutionStep({ requestId, stepName: 'Executing after-response script' }); + const postMutatedContext = await tryToExecuteAfterResponseScript({ + ...requestData, + ...mutatedContext, + transientVariables: mutatedContext.transientVariables || transientVariables, + response, + iteration, + iterationCount, + runtime, + }); + if ('error' in postMutatedContext) { + throw { + response: await responseTransform( + response, + requestData.activeEnvironmentId, + renderedRequest, + renderedResult.context, + ), + maxHistoryResponses: requestData.settings.maxHistoryResponses, + requestMeta, + error: postMutatedContext.error, + }; + } + + window.main.completeExecutionStep({ requestId }); + + const preTestResults = (mutatedContext.requestTestResults || []).map( + (result: RequestTestResult): RequestTestResult => ({ ...result, category: 'pre-request' }), + ); + const postTestResults = + (postMutatedContext?.requestTestResults || []).map( + (result: RequestTestResult): RequestTestResult => ({ ...result, category: 'after-response' }), + ) || []; + if (testResultCollector) { + testResultCollector.results = [...testResultCollector.results, ...preTestResults, ...postTestResults]; + const timingSteps = await window.main.getExecution({ requestId }); + testResultCollector.duration = timingSteps.reduce((acc: number, cur: TimingStep) => { + return acc + (cur.duration || 0); + }, 0); + testResultCollector.responseId = response._id; + } + const responsePatch = postMutatedContext + ? { + ...baseResponsePatch, + // both pre-request and after-response test results are collected + requestTestResults: [...preTestResults, ...postTestResults], + } + : baseResponsePatch; + + if (!shouldWriteToFile) { + const response = await models.response.create(responsePatch, requestData.settings.maxHistoryResponses); + await models.requestMeta.update(requestMeta, { activeResponseId: response._id }); + return postMutatedContext; + } + + if (requestMeta.downloadPath) { + const header = getContentDispositionHeader(responsePatch.headers || []); + const name = header + ? contentDisposition.parse(header.value).parameters.filename + : `${requestData.request.name.replace(/\s/g, '-').toLowerCase()}.${(responsePatch.contentType && mimeExtension(responsePatch.contentType)) || 'unknown'}`; + return writeToDownloadPath( + path.join(requestMeta.downloadPath, name), + responsePatch, + requestMeta, + requestData.settings.maxHistoryResponses, + ); + } + const defaultPath = window.localStorage.getItem('insomnia.sendAndDownloadLocation'); + const { filePath } = await window.dialog.showSaveDialog({ + title: 'Select Download Location', + buttonLabel: 'Save', + // NOTE: An error will be thrown if defaultPath is supplied but not a String + ...(defaultPath ? { defaultPath } : {}), + }); + if (!filePath) { + return null; + } + window.localStorage.setItem('insomnia.sendAndDownloadLocation', filePath); + return writeToDownloadPath(filePath, responsePatch, requestMeta, requestData.settings.maxHistoryResponses); +}; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { requestId } = params; + const { shouldPromptForPathAfterResponse, ignoreUndefinedEnvVariable } = (await request.json()) as SendActionParams; + + try { + return await sendActionImplementation({ + requestId, + shouldPromptForPathAfterResponse, + ignoreUndefinedEnvVariable, + }); + } catch (error) { + const err = error as unknown as { + error: any; + response?: ResponsePatch & { _id: string }; + requestMeta?: RequestMeta; + maxHistoryResponses?: number; + }; + + console.log('[request] Failed to send request', err); + const e = err.error || err; + const url = new URL(request.url); + + // when after-script error, there is no error in response, we need to set error info into response, so that we can show it in response viewer + if (err.response && err.requestMeta && err.response._id) { + if (!err.response.error) { + err.response.error = e; + err.response.statusMessage = 'Error'; + err.response.statusCode = 0; + } + // this part is for persisting useful info (e.g. timeline) for debugging, even there is an error + const existingResponse = await models.response.getById(err.response._id); + const response = existingResponse || (await models.response.create(err.response, err.maxHistoryResponses)); + await models.requestMeta.update(err.requestMeta, { activeResponseId: response._id }); + } else { + // if the error is not from response, we need to set it to url param and show it in modal + url.searchParams.set('error', e); + if (e?.extraInfo && e?.extraInfo?.subType === 'environmentVariable') { + url.searchParams.set('envVariableMissing', '1'); + url.searchParams.set('undefinedEnvironmentVariables', e?.extraInfo?.undefinedEnvironmentVariables); + } + } + + window.main.completeExecutionStep({ requestId }); + return redirect(`${url.pathname}?${url.searchParams}`); + } +} + +export function useDebugRequestSendActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + params, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + params: { shouldPromptForPathAfterResponse?: boolean; ignoreUndefinedEnvVariable?: boolean }; + }) => { + return fetcherSubmit(JSON.stringify(params), { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/send`, + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.tsx new file mode 100644 index 0000000000..d576880a1e --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.tsx @@ -0,0 +1,168 @@ +import { redirect, useRouteLoaderData } from 'react-router'; + +import { database } from '~/common/database'; +import type { BaseModel } from '~/models'; +import * as models from '~/models'; +import { type GrpcRequest, isGrpcRequestId } from '~/models/grpc-request'; +import type { GrpcRequestMeta } from '~/models/grpc-request-meta'; +import * as requestOperations from '~/models/helpers/request-operations'; +import type { MockRoute } from '~/models/mock-route'; +import type { MockServer } from '~/models/mock-server'; +import { isGraphqlSubscriptionRequest } from '~/models/request'; +import { type Request } from '~/models/request'; +import { type RequestMeta } from '~/models/request-meta'; +import type { RequestVersion } from '~/models/request-version'; +import type { Response } from '~/models/response'; +import type { SocketIOPayload } from '~/models/socket-io-payload'; +import { isSocketIORequest, type SocketIORequest } from '~/models/socket-io-request'; +import type { SocketIOResponse } from '~/models/socket-io-response'; +import { isWebSocketRequest, type WebSocketRequest } from '~/models/websocket-request'; +import { isWebSocketResponse, type WebSocketResponse } from '~/models/websocket-response'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; + +export interface WebSocketRequestLoaderData { + activeRequest: WebSocketRequest; + activeRequestMeta: RequestMeta; + activeResponse: WebSocketResponse | null; + responses: WebSocketResponse[]; + requestVersions: RequestVersion[]; +} + +export interface SocketIORequestLoaderData { + activeRequest: SocketIORequest; + activeRequestMeta: RequestMeta; + activeResponse: null; + responses: SocketIOResponse[]; + requestVersions: RequestVersion[]; + requestPayload: SocketIOPayload; +} +export interface GrpcRequestLoaderData { + activeRequest: GrpcRequest; + activeRequestMeta: GrpcRequestMeta; + activeResponse: null; + responses: []; + requestVersions: RequestVersion[]; +} +export interface RequestLoaderData { + activeRequest: Request; + activeRequestMeta: RequestMeta; + activeResponse: Response | null; + responses: Response[]; + requestVersions: RequestVersion[]; + mockServerAndRoutes: (MockServer & { routes: MockRoute[] })[]; +} + +const getResponseModelName = (request: Request | WebSocketRequest | SocketIORequest | GrpcRequest) => { + const isGraphqlWsRequest = isGraphqlSubscriptionRequest(request); + if (isWebSocketRequest(request) || isGraphqlWsRequest) { + return 'webSocketResponse'; + } + if (isSocketIORequest(request)) { + return 'socketIOResponse'; + } + return 'response'; +}; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId, requestId, workspaceId } = params; + + const activeRequest = await requestOperations.getById(requestId); + if (!activeRequest) { + throw redirect(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug`); + } + const activeWorkspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + invariant(activeWorkspaceMeta, 'Active workspace meta not found'); + // NOTE: loaders shouldnt mutate data, this should be moved somewhere else + await models.workspaceMeta.updateByParentId(workspaceId, { activeRequestId: requestId }); + if (isGrpcRequestId(requestId)) { + return { + activeRequest, + activeRequestMeta: await models.grpcRequestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }), + activeResponse: null, + responses: [], + requestVersions: [], + } as GrpcRequestLoaderData; + } + const activeRequestMeta = await models.requestMeta.updateOrCreateByParentId(requestId, { lastActive: Date.now() }); + invariant(activeRequestMeta, 'Request meta not found'); + const { filterResponsesByEnv } = await models.settings.get(); + const isGraphqlWsRequest = isGraphqlSubscriptionRequest(activeRequest); + + const responseModelName = getResponseModelName(activeRequest); + + const activeResponse = activeRequestMeta.activeResponseId + ? await models[responseModelName].getById(activeRequestMeta.activeResponseId) + : await models[responseModelName].getLatestForRequest(requestId, activeWorkspaceMeta.activeEnvironmentId); + const allResponses = (await models[responseModelName].findByParentId(requestId)) as ( + | Response + | WebSocketResponse + | SocketIOResponse + )[]; + const filteredResponses = allResponses.filter( + (r: Response | WebSocketResponse | SocketIOResponse) => r.environmentId === activeWorkspaceMeta.activeEnvironmentId, + ); + const responses = (filterResponsesByEnv ? filteredResponses : allResponses).sort((a: BaseModel, b: BaseModel) => + a.created > b.created ? -1 : 1, + ); + + if (activeResponse && 'bodyPath' in activeResponse) { + // read the body if its smaller than the limit add it to the activeResponse + const length = Math.max(activeResponse.bytesContent, activeResponse.bytesRead); + const isOversizedResponse = length > 5 * 1024 * 1024; // 5MB + // Oversized repsonses are handled in the response-viewer.tsx for now + if (!isOversizedResponse) { + const buffer = await models.response.getBodyBuffer(activeResponse); + activeResponse.bodyBuffer = typeof buffer === 'string' ? Buffer.from(buffer) : buffer; + } + } + + // Q(gatzjames): load mock servers here or somewhere else? + const mockServers = await models.mockServer.findByProjectId(projectId); + const mockRoutes = await database.find(models.mockRoute.type, { + parentId: { $in: mockServers.map(s => s._id) }, + }); + const mockServerAndRoutes = mockServers.map(mockServer => ({ + ...mockServer, + routes: mockRoutes.filter(route => route.parentId === mockServer._id), + })); + // set empty activeResponse if graphql websocket request and activeResponse is not websocket response + if (isGraphqlWsRequest && activeResponse && !isWebSocketResponse(activeResponse)) { + return { + activeRequest, + activeRequestMeta, + activeResponse: null, + responses: [], + requestVersions: [], + mockServerAndRoutes, + } as RequestLoaderData | WebSocketRequestLoaderData; + } + + if (isSocketIORequest(activeRequest)) { + const socketIOPayload = await models.socketIOPayload.getOrCreateByParentId(requestId); + return { + activeRequest, + activeRequestMeta, + activeResponse, + responses, + requestVersions: await models.requestVersion.findByParentId(requestId), + mockServerAndRoutes, + requestPayload: socketIOPayload, + } as SocketIORequestLoaderData; + } + return { + activeRequest, + activeRequestMeta, + activeResponse, + responses, + requestVersions: await models.requestVersion.findByParentId(requestId), + mockServerAndRoutes, + } as RequestLoaderData | WebSocketRequestLoaderData; +} + +export function useRequestLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId', + ); +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-meta.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-meta.tsx new file mode 100644 index 0000000000..485f13d7bf --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-meta.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { isGrpcRequestId } from '~/models/grpc-request'; +import type { GrpcRequestMeta } from '~/models/grpc-request-meta'; +import type { RequestMeta } from '~/models/request-meta'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-meta'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { requestId } = params; + invariant(typeof requestId === 'string', 'Request ID is required'); + const patch = (await request.json()) as Partial; + if (isGrpcRequestId(requestId)) { + await models.grpcRequestMeta.updateOrCreateByParentId(requestId, patch); + return null; + } + await models.requestMeta.updateOrCreateByParentId(requestId, patch); + return null; +} + +export function useRequestUpdateMetaActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + patch: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/update-meta', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-payload.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-payload.tsx new file mode 100644 index 0000000000..8b99e0215a --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-payload.tsx @@ -0,0 +1,59 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { SocketIOPayload } from '~/models/socket-io-payload'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update-payload'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { requestId } = params; + + const patch = (await request.json()) as Partial; + + await models.socketIOPayload.updateOrCreateByParentId(requestId, patch); + + return null; +} + +export function useRequestUpdatePayloadActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + payload, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + payload: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/update-payload', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit(JSON.stringify(payload), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update.tsx new file mode 100644 index 0000000000..b8255ed0bd --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update.tsx @@ -0,0 +1,88 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as requestOperations from '~/models/helpers/request-operations'; +import { getPathParametersFromUrl, isRequest } from '~/models/request'; +import type { WebSocketRequest } from '~/models/websocket-request'; +import { isWebSocketRequest } from '~/models/websocket-request'; +import { updateMimeType } from '~/ui/components/dropdowns/content-type-dropdown'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.update'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { requestId } = params; + + const req = await requestOperations.getById(requestId); + invariant(req, 'Request not found'); + const patch = await request.json(); + + const isRequestURLChanged = (isRequest(req) || isWebSocketRequest(req)) && patch.url && patch.url !== req.url; + + if (isRequestURLChanged) { + const { url } = patch as Request | WebSocketRequest; + + // Check the URL for path parameters and store them in the request + const urlPathParameters = getPathParametersFromUrl(url); + + const pathParameters = urlPathParameters.map(name => ({ + name, + value: req.pathParameters?.find(p => p.name === name)?.value || '', + })); + + patch.pathParameters = pathParameters; + } + + // TODO: if gRPC, we should also copy the protofile to the destination workspace - INS-267 + const isMimeTypeChanged = isRequest(req) && patch.body && patch.body.mimeType !== req.body.mimeType; + if (isMimeTypeChanged) { + await requestOperations.update(req, { ...patch, ...updateMimeType(req, patch.body?.mimeType) }); + return null; + } + + await requestOperations.update(req, patch); + + return null; +} + +export function useRequestUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestId: string; + patch: any; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId/update', + { + organizationId, + projectId, + workspaceId, + requestId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete.tsx new file mode 100644 index 0000000000..45d9bb2522 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete.tsx @@ -0,0 +1,72 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import * as requestOperations from '~/models/helpers/request-operations'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const formData = await request.formData(); + const id = formData.get('id') as string; + const req = await requestOperations.getById(id); + invariant(req, 'Request not found'); + models.stats.incrementDeletedRequests(); + await requestOperations.remove(req); + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + invariant(workspaceMeta, 'Workspace meta not found'); + if (workspaceMeta.activeRequestId === id) { + await models.workspaceMeta.updateByParentId(workspaceId, { activeRequestId: null }); + if (request.url.includes(id)) { + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', { + organizationId, + projectId, + workspaceId, + }), + ); + } + } + return null; +} + +export function useRequestDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + id, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + id: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/delete', { + organizationId, + projectId, + workspaceId, + }); + + const formData = new FormData(); + formData.append('id', id); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send.tsx new file mode 100644 index 0000000000..665289f7c0 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send.tsx @@ -0,0 +1,99 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { Request } from '~/models/request'; +import { + fetchRequestData, + responseTransform, + sendCurlAndWriteTimeline, + tryToInterpolateRequest, + tryToTransformRequestWithPlugins, +} from '~/network/network'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const patch = (await request.json()) as Partial; + invariant(typeof patch.url === 'string', 'URL is required'); + invariant(typeof patch.method === 'string', 'method is required'); + invariant(typeof patch.parentId === 'string', 'mock route ID is required'); + const mockRoute = await models.mockRoute.getById(patch.parentId); + invariant(mockRoute, 'mock route not found'); + // Get or create a testing request for this mock route + const childRequests = await models.request.findByParentId(mockRoute._id); + const testRequest = childRequests[0] || (await models.request.create({ parentId: mockRoute._id, isPrivate: true })); + invariant(testRequest, 'mock route is missing a testing request'); + const req = await models.request.update(testRequest, patch); + + const { environment, settings, clientCertificates, caCert, activeEnvironmentId, timelinePath, responseId } = + await fetchRequestData(req._id); + window.main.startExecution({ requestId: req._id }); + window.main.addExecutionStep({ + requestId: req._id, + stepName: 'Rendering request', + }); + + const renderResult = await tryToInterpolateRequest({ request: req, environment: environment._id, purpose: 'send' }); + const renderedRequest = await tryToTransformRequestWithPlugins(renderResult); + + window.main.completeExecutionStep({ requestId: req._id }); + window.main.addExecutionStep({ + requestId: req._id, + stepName: 'Sending request', + }); + + const res = await sendCurlAndWriteTimeline( + renderedRequest, + clientCertificates, + caCert, + settings, + timelinePath, + responseId, + ); + + const response = await responseTransform(res, activeEnvironmentId, renderedRequest, renderResult.context); + await models.response.create(response); + window.main.completeExecutionStep({ requestId: req._id }); + return null; +} + +export function useRequestNewMockSendActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/new-mock-send', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx new file mode 100644 index 0000000000..378ce217b3 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new.tsx @@ -0,0 +1,170 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import { + CONTENT_TYPE_EVENT_STREAM, + CONTENT_TYPE_GRAPHQL, + CONTENT_TYPE_JSON, + getAppVersion, + METHOD_GET, + METHOD_POST, +} from '~/common/constants'; +import * as models from '~/models'; +import type { Request, RequestBody, RequestParameter } from '~/models/request'; +import { SegmentEvent } from '~/ui/analytics'; +import type { CreateRequestType } from '~/ui/hooks/use-request'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new'; + +export async function clientAction({ params, request }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const { requestType, parentId, req } = (await request.json()) as { + requestType: CreateRequestType; + parentId?: string; + req?: Request; + }; + + const settings = await models.settings.getOrCreate(); + const defaultHeaders = settings.disableAppVersionUserAgent + ? [] + : [{ name: 'User-Agent', value: `insomnia/${getAppVersion()}` }]; + + let activeRequestId; + if (requestType === 'HTTP') { + activeRequestId = ( + await models.request.create({ + parentId: parentId || workspaceId, + method: METHOD_GET, + name: 'New Request', + headers: defaultHeaders, + }) + )._id; + } + if (requestType === 'gRPC') { + activeRequestId = ( + await models.grpcRequest.create({ + parentId: parentId || workspaceId, + name: 'New Request', + }) + )._id; + } + if (requestType === 'GraphQL') { + activeRequestId = ( + await models.request.create({ + parentId: parentId || workspaceId, + method: METHOD_POST, + headers: [...defaultHeaders, { name: 'Content-Type', value: CONTENT_TYPE_JSON }], + body: { + mimeType: CONTENT_TYPE_GRAPHQL, + text: '', + }, + name: 'New Request', + }) + )._id; + } + if (requestType === 'Event Stream') { + activeRequestId = ( + await models.request.create({ + parentId: parentId || workspaceId, + method: METHOD_GET, + url: '', + headers: [...defaultHeaders, { name: 'Accept', value: CONTENT_TYPE_EVENT_STREAM }], + name: 'New Event Stream', + }) + )._id; + } + if (requestType === 'WebSocket') { + activeRequestId = ( + await models.webSocketRequest.create({ + parentId: parentId || workspaceId, + name: 'New WebSocket Request', + headers: defaultHeaders, + }) + )._id; + } + if (requestType === 'SocketIO') { + activeRequestId = ( + await models.socketIORequest.create({ + parentId: parentId || workspaceId, + name: 'New Socket.IO Request', + headers: defaultHeaders, + }) + )._id; + } + if (requestType === 'From Curl') { + if (!req) { + return null; + } + try { + activeRequestId = ( + await models.request.create({ + parentId: parentId || workspaceId, + url: req.url, + method: req.method, + headers: req.headers, + body: req.body as RequestBody, + authentication: req.authentication, + parameters: req.parameters as RequestParameter[], + }) + )._id; + } catch (error) { + console.error(error); + return null; + } + } + invariant(typeof activeRequestId === 'string', 'Request ID is required'); + models.stats.incrementCreatedRequests(); + window.main.trackSegmentEvent({ event: SegmentEvent.requestCreate, properties: { requestType } }); + + // add a created query param to the URL to indicate that the request was just created, this is for distinguishing if we will create a temporary or permanent tab + return redirect( + `${href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/:requestId`, { + organizationId, + projectId, + workspaceId, + requestId: activeRequestId, + })}?created=true`, + ); +} + +export function useRequestNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + requestType, + parentId, + req, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + requestType: CreateRequestType; + parentId?: string; + req?: Partial; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug/request/new', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify({ requestType, parentId, req }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx similarity index 92% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx index 377863cc0e..1ab7777154 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner.tsx @@ -17,54 +17,45 @@ import { useDragAndDrop, } from 'react-aria-components'; import { Panel, PanelResizeHandle } from 'react-resizable-panels'; -import { - type ActionFunction, - type LoaderFunction, - useNavigate, - useParams, - useRouteLoaderData, - useSearchParams, - useSubmit, -} from 'react-router'; -import { useInterval } from 'react-use'; +import { useNavigate, useParams, useSearchParams, useSubmit } from 'react-router'; +import * as reactUse from 'react-use'; import { v4 as uuidv4 } from 'uuid'; -import { type RequestContext } from '../../../../insomnia-scripting-environment/src/objects'; -import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '../../common/constants'; -import type { ResponseTimelineEntry } from '../../main/network/libcurl-promise'; -import type { TimingStep } from '../../main/network/request-timing'; -import * as models from '../../models'; -import type { UserUploadEnvironment } from '../../models/environment'; -import type { RunnerResultPerRequest, RunnerTestResult } from '../../models/runner-test-result'; -import { cancelRequestById } from '../../network/cancellation'; -import { defaultSendActionRuntime } from '../../network/network'; -import { moveAfter, moveBefore } from '../../utils'; -import { invariant } from '../../utils/invariant'; -import { SegmentEvent } from '../analytics'; -import { Dropdown, DropdownItem, ItemContent } from '../components/base/dropdown'; -import { ErrorBoundary } from '../components/error-boundary'; -import { HelpTooltip } from '../components/help-tooltip'; -import { Icon } from '../components/icon'; -import { showModal } from '../components/modals'; -import { AlertModal } from '../components/modals/alert-modal'; -import { CLIPreviewModal } from '../components/modals/cli-preview-modal'; -import { UploadDataModal, type UploadDataType } from '../components/modals/upload-runner-data-modal'; -import { Pane, PaneBody, PaneHeader } from '../components/panes/pane'; -import { RunnerResultHistoryPane } from '../components/panes/runner-result-history-pane'; -import { RunnerTestResultPane } from '../components/panes/runner-test-result-pane'; -import { ResponseTimer } from '../components/response-timer'; -import { getTimeAndUnit } from '../components/tags/time-tag'; -import { Tooltip } from '../components/tooltip'; -import { ResponseTimelineViewer } from '../components/viewers/response-timeline-viewer'; -import { useRunnerContext } from '../context/app/runner-context'; -import { useRunnerRequestList } from '../hooks/use-runner-request-list'; -import { - type CollectionRunnerContext, - type RunnerSource, - sendActionImplementation, -} from './$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { OrganizationLoaderData } from './organization'; -import { useRootLoaderData } from './root'; +import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '~/common/constants'; +import type { ResponseTimelineEntry } from '~/main/network/libcurl-promise'; +import type { TimingStep } from '~/main/network/request-timing'; +import * as models from '~/models'; +import type { UserUploadEnvironment } from '~/models/environment'; +import type { RunnerResultPerRequest, RunnerTestResult } from '~/models/runner-test-result'; +import { cancelRequestById } from '~/network/cancellation'; +import { defaultSendActionRuntime } from '~/network/network'; +import { useRootLoaderData } from '~/root'; +import { useOrganizationLoaderData } from '~/routes/organization'; +import type { CollectionRunnerContext } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send'; +import { sendActionImplementation } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.send'; +import { SegmentEvent } from '~/ui/analytics'; +import { Dropdown, DropdownItem, ItemContent } from '~/ui/components/base/dropdown'; +import { ErrorBoundary } from '~/ui/components/error-boundary'; +import { HelpTooltip } from '~/ui/components/help-tooltip'; +import { Icon } from '~/ui/components/icon'; +import { showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { CLIPreviewModal } from '~/ui/components/modals/cli-preview-modal'; +import { UploadDataModal, type UploadDataType } from '~/ui/components/modals/upload-runner-data-modal'; +import { Pane, PaneBody, PaneHeader } from '~/ui/components/panes/pane'; +import { RunnerResultHistoryPane } from '~/ui/components/panes/runner-result-history-pane'; +import { RunnerTestResultPane } from '~/ui/components/panes/runner-test-result-pane'; +import { ResponseTimer } from '~/ui/components/response-timer'; +import { getTimeAndUnit } from '~/ui/components/tags/time-tag'; +import { Tooltip } from '~/ui/components/tooltip'; +import { ResponseTimelineViewer } from '~/ui/components/viewers/response-timeline-viewer'; +import { useRunnerContext } from '~/ui/context/app/runner-context'; +import { useRunnerRequestList } from '~/ui/hooks/use-runner-request-list'; +import { moveAfter, moveBefore } from '~/utils'; +import { invariant } from '~/utils/invariant'; + +import { type RequestContext } from '../../../insomnia-scripting-environment/src/objects'; +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner'; const inputStyle = 'placeholder:italic py-0.5 mr-1.5 px-1 w-24 rounded-sm border-2 border-solid border-[--hl-sm] bg-[--color-bg] text-[--color-font] focus:outline-none focus:ring-1 focus:ring-[--hl-md] transition-colors'; @@ -148,7 +139,7 @@ export const Runner: FC<{}> = () => { const [searchParams] = useSearchParams(); const [errorMsg, setErrorMsg] = useState(null); - const { currentPlan } = useRouteLoaderData('/organization') as OrganizationLoaderData; + const organizationData = useOrganizationLoaderData(); const targetFolderId = searchParams.get('folder') || ''; const { organizationId, projectId, workspaceId } = useParams() as { @@ -162,7 +153,7 @@ export const Runner: FC<{}> = () => { // For backward compatibility,the runnerId we use for testResult in database is no prefix with 'runner_' const runnerId = targetFolderId ? targetFolderId : workspaceId; - const { settings } = useRootLoaderData(); + const { settings } = useRootLoaderData()!; const [showUploadModal, setShowUploadModal] = useState(false); const [showCLIModal, setShowCLIModal] = useState(false); const [direction, setDirection] = useState<'horizontal' | 'vertical'>( @@ -270,7 +261,7 @@ export const Runner: FC<{}> = () => { window.main.trackSegmentEvent({ event: SegmentEvent.collectionRunExecute, - properties: { plan: currentPlan?.type || 'scratchpad', iterations: iterationCount }, + properties: { plan: organizationData?.currentPlan?.type || 'scratchpad', iterations: iterationCount }, }); const requests = selectedKeys === 'all' ? reqList : reqList.filter(item => (selectedKeys as Set).has(item.id)); @@ -300,7 +291,7 @@ export const Runner: FC<{}> = () => { submit(JSON.stringify(actionInput), { method: 'post', encType: 'application/json', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner/run`, + action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/runner`, navigate: false, }); }; @@ -413,7 +404,7 @@ export const Runner: FC<{}> = () => { } }, [runnerId]); - useInterval( + reactUse.useInterval( () => { refreshPanes(); }, @@ -495,7 +486,7 @@ export const Runner: FC<{}> = () => { iterationCount: parseInt(e.target.value, 10), }); } - } catch (ex) {} + } catch {} }} type="number" className={iterationInputStyle} @@ -513,7 +504,7 @@ export const Runner: FC<{}> = () => { if (delay >= 0) { updateRunnerState(organizationId, runnerId, { delay }); // also update the temp settings } - } catch (ex) {} + } catch {} }} type="number" className={inputStyle} @@ -899,7 +890,7 @@ export interface runCollectionActionParams { } // don't forget also apply modification on this function to the cli.ts at the moment -export const runCollectionAction: ActionFunction = async ({ request, params }) => { +export async function clientAction({ request, params }: Route.ClientActionArgs) { const { organizationId, projectId, workspaceId } = params; invariant(organizationId, 'Organization id is required'); invariant(projectId, 'Project id is required'); @@ -907,11 +898,11 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = const { requests, iterationCount, delay, userUploadEnvs, bail, targetFolderId, keepLog } = (await request.json()) as runCollectionActionParams; - const source: RunnerSource = 'runner'; + const runnerId = targetFolderId ? targetFolderId : workspaceId; let testCtx: CollectionRunnerContext = { - source, + source: 'runner', environmentId: '', iterationCount, iterationData: userUploadEnvs, @@ -1110,10 +1101,4 @@ export const runCollectionAction: ActionFunction = async ({ request, params }) = }); } return null; -}; - -export const collectionRunnerStatusLoader: LoaderFunction = async ({ params }) => { - const { workspaceId } = params; - invariant(workspaceId, 'Workspace id is required'); - return null; -}; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx similarity index 85% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx index 5b5d9a0a27..5924d49858 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.tsx @@ -1,7 +1,7 @@ import type { IconName } from '@fortawesome/fontawesome-svg-core'; import type { ServiceError, StatusObject } from '@grpc/grpc-js'; import { useVirtualizer } from '@tanstack/react-virtual'; -import React, { type FC, Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'; import { Breadcrumb, Breadcrumbs, @@ -28,108 +28,105 @@ import { } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { - type LoaderFunction, type NavigateFunction, NavLink, redirect, - Route, + Route as RouteComponent, Routes, - useFetcher, useFetchers, useNavigate, useParams, - useRouteLoaderData, useSearchParams, } from 'react-router'; -import { - DEFAULT_SIDEBAR_SIZE, - getProductName, - SORT_ORDERS, - type SortOrder, - sortOrderName, -} from '../../common/constants'; -import { type ChangeBufferEvent, database as db } from '../../common/database'; -import { generateId, isNotNullOrUndefined } from '../../common/misc'; -import type { PlatformKeyCombinations } from '../../common/settings'; -import type { GrpcMethodInfo } from '../../main/ipc/grpc'; -import * as models from '../../models'; -import type { Environment } from '../../models/environment'; -import { type GrpcRequest, isGrpcRequest, isGrpcRequestId } from '../../models/grpc-request'; -import { getByParentId as getGrpcRequestMetaByParentId } from '../../models/grpc-request-meta'; -import type { Project } from '../../models/project'; +import { DEFAULT_SIDEBAR_SIZE, getProductName, SORT_ORDERS, type SortOrder, sortOrderName } from '~/common/constants'; +import { type ChangeBufferEvent, database as db } from '~/common/database'; +import { generateId, isNotNullOrUndefined } from '~/common/misc'; +import type { PlatformKeyCombinations } from '~/common/settings'; +import type { GrpcMethodInfo } from '~/main/ipc/grpc'; +import * as models from '~/models'; +import type { Environment } from '~/models/environment'; +import { type GrpcRequest, isGrpcRequest, isGrpcRequestId } from '~/models/grpc-request'; +import { getByParentId as getGrpcRequestMetaByParentId } from '~/models/grpc-request-meta'; +import type { Project } from '~/models/project'; import { isEventStreamRequest, isGraphqlSubscriptionRequest, isRequest, isRequestId, type Request, -} from '../../models/request'; -import { isRequestGroup, isRequestGroupId, type RequestGroup } from '../../models/request-group'; -import type { RequestGroupMeta } from '../../models/request-group-meta'; -import { getByParentId as getRequestMetaByParentId } from '../../models/request-meta'; -import { isSocketIORequest, isSocketIORequestId, type SocketIORequest } from '../../models/socket-io-request'; -import { isWebSocketRequest, isWebSocketRequestId, type WebSocketRequest } from '../../models/websocket-request'; -import { isDesign } from '../../models/workspace'; -import { scrollElementIntoView } from '../../utils'; -import { getGrpcConnectionErrorDetails, isGrpcConnectionError } from '../../utils/grpc'; -import { invariant } from '../../utils/invariant'; -import { DropdownHint } from '../components/base/dropdown/dropdown-hint'; -import { DocumentTab } from '../components/document-tab'; -import { RequestActionsDropdown } from '../components/dropdowns/request-actions-dropdown'; -import { RequestGroupActionsDropdown } from '../components/dropdowns/request-group-actions-dropdown'; -import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; -import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; -import { EditableInput } from '../components/editable-input'; -import { EnvironmentPicker } from '../components/environment-picker'; -import { ErrorBoundary } from '../components/error-boundary'; -import { Icon } from '../components/icon'; -import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; -import { showModal } from '../components/modals'; -import { AskModal } from '../components/modals/ask-modal'; -import { CookiesModal } from '../components/modals/cookies-modal'; -import { ErrorModal } from '../components/modals/error-modal'; -import { GenerateCodeModal } from '../components/modals/generate-code-modal'; -import { ImportModal } from '../components/modals/import-modal/import-modal'; -import { OAuthAuthorizationStatusModal } from '../components/modals/oauth-authorization-status-modal'; -import { PasteCurlModal } from '../components/modals/paste-curl-modal'; -import { PromptModal } from '../components/modals/prompt-modal'; -import { RequestSettingsModal } from '../components/modals/request-settings-modal'; -import { CertificatesModal } from '../components/modals/workspace-certificates-modal'; -import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal'; -import { GrpcRequestPane } from '../components/panes/grpc-request-pane'; -import { GrpcResponsePane } from '../components/panes/grpc-response-pane'; -import { PlaceholderRequestPane } from '../components/panes/placeholder-request-pane'; -import { RequestGroupPane } from '../components/panes/request-group-pane'; -import { RequestPane } from '../components/panes/request-pane'; -import { ResponsePane } from '../components/panes/response-pane'; -import { SocketIORequestPane } from '../components/socket-io/request-pane'; -import { OrganizationTabList } from '../components/tabs/tab-list'; -import { getMethodShortHand } from '../components/tags/method-tag'; -import { RealtimeResponsePane } from '../components/websockets/realtime-response-pane'; -import { WebSocketRequestPane } from '../components/websockets/websocket-request-pane'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useCloseConnection } from '../hooks/use-close-connection'; -import { useExecutionState } from '../hooks/use-execution-state'; -import { useInsomniaTab } from '../hooks/use-insomnia-tab'; -import { useReadyState } from '../hooks/use-ready-state'; +} from '~/models/request'; +import { isRequestGroup, isRequestGroupId, type RequestGroup } from '~/models/request-group'; +import type { RequestGroupMeta } from '~/models/request-group-meta'; +import { getByParentId as getRequestMetaByParentId } from '~/models/request-meta'; +import { isSocketIORequest, isSocketIORequestId, type SocketIORequest } from '~/models/socket-io-request'; +import { isWebSocketRequest, isWebSocketRequestId, type WebSocketRequest } from '~/models/websocket-request'; +import { isDesign } from '~/models/workspace'; +import { useRootLoaderData } from '~/root'; +import { + type Child, + useWorkspaceLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { useDebugReorderActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.reorder'; +import { useRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { useRequestDuplicateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId.duplicate'; +import { useRequestDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.delete'; +import { useRequestNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new'; +import { useRequestGroupLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { useRequestGroupNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.new'; +import Runner from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.runner'; +import { useToggleExpandAllActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.toggle-expand-all'; +import { DropdownHint } from '~/ui/components/base/dropdown/dropdown-hint'; +import { DocumentTab } from '~/ui/components/document-tab'; +import { RequestActionsDropdown } from '~/ui/components/dropdowns/request-actions-dropdown'; +import { RequestGroupActionsDropdown } from '~/ui/components/dropdowns/request-group-actions-dropdown'; +import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown'; +import { WorkspaceSyncDropdown } from '~/ui/components/dropdowns/workspace-sync-dropdown'; +import { EditableInput } from '~/ui/components/editable-input'; +import { EnvironmentPicker } from '~/ui/components/environment-picker'; +import { ErrorBoundary } from '~/ui/components/error-boundary'; +import { Icon } from '~/ui/components/icon'; +import { useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder'; +import { showModal } from '~/ui/components/modals'; +import { AskModal } from '~/ui/components/modals/ask-modal'; +import { CookiesModal } from '~/ui/components/modals/cookies-modal'; +import { ErrorModal } from '~/ui/components/modals/error-modal'; +import { GenerateCodeModal } from '~/ui/components/modals/generate-code-modal'; +import { ImportModal } from '~/ui/components/modals/import-modal/import-modal'; +import { OAuthAuthorizationStatusModal } from '~/ui/components/modals/oauth-authorization-status-modal'; +import { PasteCurlModal } from '~/ui/components/modals/paste-curl-modal'; +import { PromptModal } from '~/ui/components/modals/prompt-modal'; +import { RequestSettingsModal } from '~/ui/components/modals/request-settings-modal'; +import { CertificatesModal } from '~/ui/components/modals/workspace-certificates-modal'; +import { WorkspaceEnvironmentsEditModal } from '~/ui/components/modals/workspace-environments-edit-modal'; +import { GrpcRequestPane } from '~/ui/components/panes/grpc-request-pane'; +import { GrpcResponsePane } from '~/ui/components/panes/grpc-response-pane'; +import { PlaceholderRequestPane } from '~/ui/components/panes/placeholder-request-pane'; +import { RequestGroupPane } from '~/ui/components/panes/request-group-pane'; +import { RequestPane } from '~/ui/components/panes/request-pane'; +import { ResponsePane } from '~/ui/components/panes/response-pane'; +import { SocketIORequestPane } from '~/ui/components/socket-io/request-pane'; +import { OrganizationTabList } from '~/ui/components/tabs/tab-list'; +import { getMethodShortHand } from '~/ui/components/tags/method-tag'; +import { RealtimeResponsePane } from '~/ui/components/websockets/realtime-response-pane'; +import { WebSocketRequestPane } from '~/ui/components/websockets/websocket-request-pane'; +import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant'; +import { useCloseConnection } from '~/ui/hooks/use-close-connection'; +import { useExecutionState } from '~/ui/hooks/use-execution-state'; +import { useInsomniaTab } from '~/ui/hooks/use-insomnia-tab'; +import { useReadyState } from '~/ui/hooks/use-ready-state'; import { type CreateRequestType, useRequestGroupMetaPatcher, useRequestGroupPatcher, useRequestMetaPatcher, useRequestPatcher, -} from '../hooks/use-request'; -import type { Child, WorkspaceLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId'; -import type { - GrpcRequestLoaderData, - RequestLoaderData, - SocketIORequestLoaderData, - WebSocketRequestLoaderData, -} from './$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; -import Runner from './$organizationId.project.$projectId.workspace.$workspaceId.debug.runner'; -import { useRootLoaderData } from './root'; +} from '~/ui/hooks/use-request'; +import { scrollElementIntoView } from '~/utils'; +import { getGrpcConnectionErrorDetails, isGrpcConnectionError } from '~/utils/grpc'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug'; export interface GrpcMessage { id: string; @@ -155,7 +152,8 @@ const INITIAL_GRPC_REQUEST_STATE = { error: undefined, methods: [], }; -export const loader: LoaderFunction = async ({ params, request }) => { + +export async function clientLoader({ params, request }: Route.ClientLoaderArgs) { if (!params.requestId && !params.requestGroupId) { const { projectId, workspaceId, organizationId } = params; invariant(workspaceId, 'Workspace ID is required'); @@ -178,7 +176,7 @@ export const loader: LoaderFunction = async ({ params, request }) => { } } return null; -}; +} const WebSocketSpinner = ({ requestId }: { requestId: string }) => { const readyState = useReadyState({ requestId, protocol: 'webSocket' }); @@ -226,7 +224,7 @@ const RequestTiming = ({ requestId }: { requestId: string }) => { ) : null; }; -export const Debug: FC = () => { +const Debug = () => { const { activeWorkspace, activeProject, @@ -236,15 +234,15 @@ export const Debug: FC = () => { clientCertificates, grpcRequests, collection, - } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const requestData = useRouteLoaderData('request/:requestId') as - | RequestLoaderData - | GrpcRequestLoaderData - | WebSocketRequestLoaderData - | SocketIORequestLoaderData - | undefined; + } = useWorkspaceLoaderData()!; + + const requestData = useRequestLoaderData(); const { activeRequest } = requestData || {}; - const requestFetcher = useFetcher(); + + const deleteRequestFetcher = useRequestDeleteActionFetcher(); + const duplicateRequestFetcher = useRequestDuplicateActionFetcher(); + const createRequestFetcher = useRequestNewActionFetcher(); + const createRequestGroupFetcher = useRequestGroupNewActionFetcher(); const [isPasteCurlModalOpen, setPasteCurlModalOpen] = useState(false); const [pastedCurl, setPastedCurl] = useState(''); @@ -257,7 +255,7 @@ export const Debug: FC = () => { requestGroupId?: string; }; - const { activeRequestGroup } = (useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData) || {}; + const { activeRequestGroup } = useRequestGroupLoaderData() || {}; const [grpcStates, setGrpcStates] = useState( grpcRequests.map(r => ({ @@ -286,7 +284,7 @@ export const Debug: FC = () => { }); }, []); - const { settings } = useRootLoaderData(); + const { settings } = useRootLoaderData()!; const grpcState = grpcStates.find(s => s.requestId === requestId); const setGrpcState = (newState: GrpcRequestState) => @@ -396,20 +394,19 @@ export const Debug: FC = () => { color: 'danger', onDone: async (confirmed: boolean) => { if (confirmed) { - requestFetcher.submit( - { id: requestId }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/delete`, - method: 'post', - }, - ); + deleteRequestFetcher.submit({ + organizationId, + projectId, + workspaceId, + id: requestId, + }); } }, }); } }, request_showDuplicate: () => { - if (activeRequest) { + if (activeRequest && requestId) { showModal(PromptModal, { title: 'Duplicate Request', defaultValue: activeRequest.name, @@ -417,28 +414,26 @@ export const Debug: FC = () => { label: 'New Name', selectText: true, onComplete: async (name: string) => { - requestFetcher.submit( - { name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/${requestId}/duplicate`, - method: 'post', - encType: 'application/json', - }, - ); + duplicateRequestFetcher.submit({ + organizationId, + projectId, + requestId, + workspaceId, + name, + }); }, }); } }, request_createHTTP: async () => { const parentId = activeRequest ? activeRequest.parentId : activeWorkspace._id; - requestFetcher.submit( - { requestType: 'HTTP', parentId }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/new`, - method: 'post', - encType: 'application/json', - }, - ); + createRequestFetcher.submit({ + organizationId, + projectId, + workspaceId, + requestType: 'HTTP', + parentId, + }); }, request_showCreateFolder: () => { const parentId = activeRequest ? activeRequest.parentId : workspaceId; @@ -449,13 +444,13 @@ export const Debug: FC = () => { label: 'Name', selectText: true, onComplete: name => - requestFetcher.submit( - { parentId, name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, - method: 'post', - }, - ), + createRequestGroupFetcher.submit({ + organizationId, + projectId, + workspaceId, + parentId, + name, + }), }); }, environment_showEditor: () => setEnvironmentModalOpen(true), @@ -493,14 +488,17 @@ export const Debug: FC = () => { parentId: string; req?: Partial; }) => - requestFetcher.submit(JSON.stringify({ requestType, parentId, req }), { - encType: 'application/json', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/new`, - method: 'post', + createRequestFetcher.submit({ + organizationId, + projectId, + workspaceId, + requestType, + parentId, + req, }); const groupMetaPatcher = useRequestGroupMetaPatcher(); - const reorderFetcher = useFetcher(); + const reorderFetcher = useDebugReorderActionFetcher(); const navigate = useNavigate(); @@ -573,19 +571,17 @@ export const Debug: FC = () => { } if (metaSortKey) { - reorderFetcher.submit( - { + reorderFetcher.submit({ + organizationId, + projectId, + workspaceId, + params: { targetId, id, dropPosition: event.target.dropPosition, metaSortKey, }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/reorder`, - method: 'POST', - encType: 'application/json', - }, - ); + }); } }, renderDropIndicator(target) { @@ -640,13 +636,13 @@ export const Debug: FC = () => { label: 'Name', selectText: true, onComplete: name => - requestFetcher.submit( - { parentId: workspaceId, name }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request-group/new`, - method: 'post', - }, - ), + createRequestGroupFetcher.submit({ + organizationId, + projectId, + workspaceId, + parentId: workspaceId, + name, + }), }), }, { @@ -736,7 +732,7 @@ export const Debug: FC = () => { // const allCollapsed = collection.every(item => item.hidden); const [allExpanded, setAllExpanded] = useState(false); - const toggleExpandAllFetcher = useFetcher(); + const toggleExpandAllFetcher = useToggleExpandAllActionFetcher(); const visibleCollection = collection.filter(item => !item.hidden); @@ -749,17 +745,6 @@ export const Debug: FC = () => { getItemKey: index => visibleCollection[index].doc._id, }); - const expandAllForRequestFetcher = useFetcher(); - - useLayoutEffect(() => { - if (expandAllForRequestFetcher.state !== 'idle' && expandAllForRequestFetcher.data && requestId) { - setTimeout(() => { - const activeIndex = collection.findIndex(item => item.doc._id === requestId); - activeIndex && virtualizer.scrollToIndex(activeIndex); - }, 100); - } - }, [collection, expandAllForRequestFetcher.data, expandAllForRequestFetcher.state, requestId, virtualizer]); - const [direction, setDirection] = useState<'horizontal' | 'vertical'>( settings.forceVerticalLayout ? 'vertical' : 'horizontal', ); @@ -945,16 +930,12 @@ export const Debug: FC = () => { defaultSelected={allExpanded} onChange={() => { setAllExpanded(!allExpanded); - toggleExpandAllFetcher.submit( - { - toggle: allExpanded ? 'collapse-all' : 'expand-all', - }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/toggle-expand-all`, - method: 'POST', - encType: 'application/json', - }, - ); + toggleExpandAllFetcher.submit({ + organizationId, + projectId, + workspaceId, + toggle: allExpanded ? 'collapse-all' : 'expand-all', + }); }} className="flex aspect-square h-full items-center justify-center rounded-sm text-sm text-[--color-font] ring-1 ring-transparent transition-all hover:bg-[--hl-xs] focus:ring-inset focus:ring-[--hl-md]" > @@ -1198,7 +1179,7 @@ export const Debug: FC = () => { - @@ -1260,7 +1241,7 @@ export const Debug: FC = () => { } /> - } /> + } /> diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create.tsx new file mode 100644 index 0000000000..5cc6277c89 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create.tsx @@ -0,0 +1,60 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { EnvironmentType } from '~/models/environment'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const { isPrivate, environmentType = EnvironmentType.KVPAIR } = await request.json(); + + const baseEnvironment = await models.environment.getByParentId(workspaceId); + + invariant(baseEnvironment, 'Base environment not found'); + + const environment = await models.environment.create({ + parentId: baseEnvironment._id, + environmentType, + isPrivate, + }); + + return environment; +} + +export function useEnvironmentCreateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + params, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + params: { isPrivate: boolean; environmentType?: string }; + }) => { + return fetcherSubmit(JSON.stringify(params), { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/create`, { + organizationId, + projectId, + workspaceId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete.tsx new file mode 100644 index 0000000000..600832ace7 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + + const environmentId = formData.get('environmentId'); + invariant(typeof environmentId === 'string', 'Environment ID is required'); + + const environment = await models.environment.getById(environmentId); + const baseEnvironment = await models.environment.getByParentId(workspaceId); + invariant(environment?._id !== baseEnvironment?._id, 'Cannot delete base environment'); + invariant(environment, 'Environment not found'); + + await models.environment.remove(environment); + + return null; +} + +export function useEnvironmentDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + environmentId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + environmentId: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/delete', { + organizationId, + projectId, + workspaceId, + }); + + const formData = new FormData(); + formData.set('environmentId', environmentId); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate.tsx new file mode 100644 index 0000000000..8560c7984d --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate'; + +export async function clientAction({ request }: Route.ClientActionArgs) { + const formData = await request.formData(); + + const environmentId = formData.get('environmentId'); + + invariant(typeof environmentId === 'string', 'Environment ID is required'); + + const environment = await models.environment.getById(environmentId); + invariant(environment, 'Environment not found'); + + const newEnvironment = await models.environment.duplicate(environment); + + return newEnvironment; +} + +export function useEnvironmentDuplicateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + environmentId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + environmentId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/duplicate', + { + organizationId, + projectId, + workspaceId, + }, + ); + + const formData = new FormData(); + formData.set('environmentId', environmentId); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global.tsx new file mode 100644 index 0000000000..a6fe7fb199 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + + const environmentId = formData.get('environmentId'); + invariant(typeof environmentId === 'string', 'Environment ID is required'); + + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); + invariant(workspaceMeta, 'Workspace meta not found'); + + await models.workspaceMeta.update(workspaceMeta, { activeGlobalEnvironmentId: environmentId || null }); + + return null; +} + +export function useEnvironmentSetActiveGlobalActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + environmentId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + environmentId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/set-active-global', + { + organizationId, + projectId, + workspaceId, + }, + ); + + const formData = new FormData(); + formData.set('environmentId', environmentId); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active.tsx new file mode 100644 index 0000000000..344f5e50bb --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active.tsx @@ -0,0 +1,64 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + + const environmentId = formData.get('environmentId'); + invariant(typeof environmentId === 'string', 'Environment ID is required'); + + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); + invariant(workspaceMeta, 'Workspace meta not found'); + + await models.workspaceMeta.update(workspaceMeta, { activeEnvironmentId: environmentId || null }); + + return null; +} + +export function useSetActiveEnvironmentFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + environmentId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + environmentId: string; + }) => { + return fetcherSubmit( + { + environmentId, + }, + { + method: 'POST', + action: href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/set-active', + { + organizationId, + projectId, + workspaceId, + }, + ), + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx similarity index 72% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx index 3e7bc03809..722ed33af8 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.tsx @@ -18,63 +18,73 @@ import { useDragAndDrop, } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { NavLink, useFetcher, useParams, useRouteLoaderData } from 'react-router'; +import { NavLink } from 'react-router'; -import { DEFAULT_SIDEBAR_SIZE } from '../../common/constants'; -import { debounce } from '../../common/misc'; +import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants'; +import { debounce } from '~/common/misc'; +import { userSession } from '~/models'; import { type Environment, type EnvironmentKvPairData, EnvironmentKvPairDataType, EnvironmentType, getDataFromKVPair, -} from '../../models/environment'; -import { isRemoteProject } from '../../models/project'; -import { decryptVaultKeyFromSession } from '../../utils/vault'; -import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; -import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; -import { EditableInput } from '../components/editable-input'; +} from '~/models/environment'; +import { isRemoteProject } from '~/models/project'; +import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { useEnvironmentCreateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.create'; +import { useEnvironmentDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.delete'; +import { useEnvironmentDuplicateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.duplicate'; +import { useEnvironmentUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update'; +import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown'; +import { WorkspaceSyncDropdown } from '~/ui/components/dropdowns/workspace-sync-dropdown'; +import { EditableInput } from '~/ui/components/editable-input'; import { EnvironmentEditor, type EnvironmentEditorHandle, type EnvironmentInfo, -} from '../components/editors/environment-editor'; -import { EnvironmentKVEditor } from '../components/editors/environment-key-value-editor/key-value-editor'; -import { handleToggleEnvironmentType } from '../components/editors/environment-utils'; -import { Icon } from '../components/icon'; -import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; -import { showModal } from '../components/modals'; -import { AlertModal } from '../components/modals/alert-modal'; -import { InputVaultKeyModal } from '../components/modals/input-vault-key-modal'; -import { OrganizationTabList } from '../components/tabs/tab-list'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/use-insomnia-tab'; -import { useOrganizationPermissions } from '../hooks/use-organization-features'; -import type { WorkspaceLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId'; -import { useRootLoaderData } from './root'; +} from '~/ui/components/editors/environment-editor'; +import { EnvironmentKVEditor } from '~/ui/components/editors/environment-key-value-editor/key-value-editor'; +import { handleToggleEnvironmentType } from '~/ui/components/editors/environment-utils'; +import { Icon } from '~/ui/components/icon'; +import { useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder'; +import { showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { InputVaultKeyModal } from '~/ui/components/modals/input-vault-key-modal'; +import { OrganizationTabList } from '~/ui/components/tabs/tab-list'; +import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant'; +import { useInsomniaTab } from '~/ui/hooks/use-insomnia-tab'; +import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features'; +import { decryptVaultKeyFromSession } from '~/utils/vault'; -const Environments = () => { - const { - organizationId = '', - projectId = '', - workspaceId = '', - } = useParams<{ organizationId: string; projectId: string; workspaceId: string }>(); - const routeData = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment'; + +export async function clientLoader(_args: Route.ClientLoaderArgs) { + const user = await userSession.get(); + + const vaultKey = user.vaultKey ? await decryptVaultKeyFromSession(user.vaultKey, false) : ''; + + return { + vaultKey, + }; +} + +const Component = ({ loaderData, params }: Route.ComponentProps) => { + const { organizationId, projectId, workspaceId } = params; + const { vaultKey } = loaderData; + const routeData = useWorkspaceLoaderData()!; const environmentEditorRef = useRef(null); const { features } = useOrganizationPermissions(); - const { userSession } = useRootLoaderData(); - const { vaultKey: vaultKeyInSession, vaultSalt } = userSession; - const createEnvironmentFetcher = useFetcher(); - const deleteEnvironmentFetcher = useFetcher(); - const updateEnvironmentFetcher = useFetcher(); - const duplicateEnvironmentFetcher = useFetcher(); + const createEnvironmentFetcher = useEnvironmentCreateActionFetcher(); + const deleteEnvironmentFetcher = useEnvironmentDeleteActionFetcher(); + const updateEnvironmentFetcher = useEnvironmentUpdateActionFetcher(); + const duplicateEnvironmentFetcher = useEnvironmentDuplicateActionFetcher(); const { activeProject, baseEnvironment, activeEnvironment, subEnvironments, activeWorkspaceMeta, activeWorkspace } = routeData; const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(activeEnvironment._id); - const [vaultKey, setVaultKey] = useState(''); const isUsingInsomniaCloudSync = Boolean(isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId); const isUsingGitSync = Boolean(features.gitSync.enabled && activeWorkspaceMeta?.gitRepositoryId); @@ -88,7 +98,7 @@ const Environments = () => { const containsSecret = allEnvironment.some( env => env.isPrivate && env.kvPairData?.some(pairData => pairData.type === EnvironmentKvPairDataType.SECRET), ); - const shouldShowVaultKeyModal = containsSecret && !vaultKeyInSession; + const shouldShowVaultKeyModal = containsSecret && !loaderData.vaultKey; const [showInputVaultKeyModal, setShowModal] = useState(shouldShowVaultKeyModal); const environmentActionsList: { @@ -102,15 +112,12 @@ const Environments = () => { name: 'Duplicate', icon: 'copy', action: async (environment: Environment) => { - duplicateEnvironmentFetcher.submit( - { - environmentId: environment._id, - }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/duplicate`, - }, - ); + duplicateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + environmentId: environment._id, + }); }, }, { @@ -124,15 +131,12 @@ const Environments = () => { addCancel: true, okLabel: 'Delete', onConfirm: async () => { - deleteEnvironmentFetcher.submit( - { - environmentId: environment._id, - }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/delete`, - }, - ); + deleteEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + environmentId: environment._id, + }); setSelectedEnvironmentId(baseEnvironment._id); }, @@ -154,16 +158,14 @@ const Environments = () => { description: `${isUsingGitSync ? 'Synced with Git Sync and exportable' : isUsingInsomniaCloudSync ? 'Synced with Insomnia Sync and exportable' : 'Exportable'}`, icon: isUsingGitSync ? ['fab', 'git-alt'] : isUsingInsomniaCloudSync ? 'globe-americas' : 'file-arrow-down', action: async () => { - createEnvironmentFetcher.submit( - { + createEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + params: { isPrivate: false, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`, - encType: 'application/json', - }, - ); + }); }, }, { @@ -172,16 +174,14 @@ const Environments = () => { description: 'Local and not exportable', icon: 'lock', action: async () => { - createEnvironmentFetcher.submit( - { + createEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + params: { isPrivate: true, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/create`, - encType: 'application/json', - }, - ); + }); }, }, ]; @@ -190,41 +190,33 @@ const Environments = () => { if (environmentEditorRef.current?.isValid() && selectedEnvironment) { const { object, propertyOrder } = value; - updateEnvironmentFetcher.submit( - { - patch: { - data: object, - dataPropertyOrder: propertyOrder, - }, - environmentId: selectedEnvironment._id, + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + data: object, + dataPropertyOrder: propertyOrder, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', - }, - ); + environmentId: selectedEnvironment._id, + }); } }, 500); const handleKVPairChange = (kvPairData: EnvironmentKvPairData[]) => { if (selectedEnvironment) { const environmentData = getDataFromKVPair(kvPairData); - updateEnvironmentFetcher.submit( - JSON.stringify({ - patch: { - data: environmentData.data, - dataPropertyOrder: environmentData.dataPropertyOrder, - kvPairData, - }, - environmentId: selectedEnvironment._id, - }), - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + data: environmentData.data, + dataPropertyOrder: environmentData.dataPropertyOrder, + kvPairData, }, - ); + environmentId: selectedEnvironment._id, + }); } }; @@ -257,17 +249,13 @@ const Environments = () => { } } - updateEnvironmentFetcher.submit( - { - patch: { metaSortKey: sourceEnv.metaSortKey }, - environmentId: sourceEnv._id, - }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', - }, - ); + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { metaSortKey: sourceEnv.metaSortKey }, + environmentId: sourceEnv._id, + }); }, renderDropIndicator(target) { if (target.type === 'item') { @@ -299,10 +287,7 @@ const Environments = () => { sidebarPanelRef.current?.setLayout(layout); } - const handleInputVaultKeyModalClose = (newVaultKey?: string) => { - if (newVaultKey) { - setVaultKey(newVaultKey); - } + const handleInputVaultKeyModalClose = () => { setShowModal(false); }; @@ -312,16 +297,6 @@ const Environments = () => { return unsubscribe; }, []); - useEffect(() => { - if (vaultKeyInSession && vaultSalt) { - async function updateVaultKey(key: string) { - const decryptedVaultKey = await decryptVaultKeyFromSession(key, false); - setVaultKey(decryptedVaultKey); - } - updateVaultKey(vaultKeyInSession); - } - }, [vaultKeyInSession, vaultSalt]); - useDocBodyKeyboardShortcuts({ sidebar_toggle: toggleSidebar, }); @@ -416,19 +391,15 @@ const Environments = () => { className="flex-1 px-1 hover:!bg-transparent" onSubmit={name => { name && - updateEnvironmentFetcher.submit( - { - patch: { - name, - }, - environmentId: item._id, + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + name, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', - }, - ); + environmentId: item._id, + }); }} /> {item.parentId !== workspaceId && ( @@ -535,19 +506,15 @@ const Environments = () => { className="flex-1 px-1" onSubmit={name => { name && - updateEnvironmentFetcher.submit( - { - patch: { - name, - }, - environmentId: selectedEnvironmentId, + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + name, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', - }, - ); + environmentId: selectedEnvironmentId, + }); }} /> @@ -557,19 +524,15 @@ const Environments = () => { { const color = e.target.value; - updateEnvironmentFetcher.submit( - { - patch: { - color, - }, - environmentId: selectedEnvironment._id, + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + color, }, - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', - }, - ); + environmentId: selectedEnvironment._id, + }); }} type="color" value={selectedEnvironment?.color || ''} @@ -583,20 +546,16 @@ const Environments = () => { newEnvironmentType: EnvironmentType, kvPairData: EnvironmentKvPairData[], ) => { - updateEnvironmentFetcher.submit( - JSON.stringify({ - patch: { - environmentType: newEnvironmentType, - kvPairData: kvPairData, - }, - environmentId: selectedEnvironment._id, - }), - { - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/update`, - encType: 'application/json', + updateEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { + environmentType: newEnvironmentType, + kvPairData: kvPairData, }, - ); + environmentId: selectedEnvironment._id, + }); }; const isValidJSON = !!environmentEditorRef.current?.isValid(); handleToggleEnvironmentType( @@ -651,4 +610,4 @@ const Environments = () => { ); }; -export default Environments; +export default Component; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update.tsx new file mode 100644 index 0000000000..09a49032c6 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update.tsx @@ -0,0 +1,65 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { Environment } from '~/models/environment'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.update'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const { environmentId, patch } = (await request.json()) as { environmentId: string; patch: Partial }; + invariant(typeof environmentId === 'string', 'Environment ID is required'); + + const environment = await models.environment.getById(environmentId); + + invariant(environment, 'Environment not found'); + + const baseEnvironment = await models.environment.getByParentId(workspaceId); + + invariant(baseEnvironment, 'Base environment not found'); + + const updatedEnvironment = await models.environment.update(environment, patch); + + return updatedEnvironment; +} + +export function useEnvironmentUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + environmentId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + environmentId: string; + patch: Partial; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/environment/update', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit(JSON.stringify({ environmentId, patch }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.checkout.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.checkout.tsx new file mode 100644 index 0000000000..1045f94642 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.checkout.tsx @@ -0,0 +1,81 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import type { Operation } from '~/common/database'; +import { database } from '~/common/database'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems, remoteCompareCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.checkout'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const formData = await request.formData(); + + const branch = formData.get('branch'); + invariant(typeof branch === 'string', 'Branch is required'); + + const vcs = VCSInstance(); + const { syncItems } = await getSyncItems({ workspaceId }); + + try { + const delta = await vcs.checkout(syncItems, branch); + await database.batchModifyDocs(delta as Operation); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while checking out branch.'; + + return { + error: errorMessage, + }; + } + + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useInsomniaSyncBranchCheckoutActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + branch, + organizationId, + projectId, + workspaceId, + }: { + branch: string; + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const formData = new FormData(); + formData.set('branch', branch); + + return fetcherSubmit(formData, { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/branch/checkout`, + { + organizationId, + projectId, + workspaceId, + }, + ), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.create.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.create.tsx new file mode 100644 index 0000000000..cab9a2bd10 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.create.tsx @@ -0,0 +1,67 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import type { Operation } from '~/common/database'; +import { database } from '~/common/database'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems, remoteCompareCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + + const branchName = formData.get('branchName'); + invariant(typeof branchName === 'string', 'Branch is required'); + + const { syncItems } = await getSyncItems({ workspaceId }); + + try { + const vcs = VCSInstance(); + await vcs.fork(branchName); + // Checkout new branch + const delta = await vcs.checkout(syncItems, branchName); + await database.batchModifyDocs(delta as Operation); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while merging branch.'; + + return { + error: errorMessage, + }; + } + + return null; +} + +export function useInsomniaSyncBranchCreateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + (branchName: string, organizationId: string, projectId: string, workspaceId: string) => { + const formData = new FormData(); + formData.set('branchName', branchName); + + return fetcherSubmit(formData, { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/branch/create`, + { + organizationId, + projectId, + workspaceId, + }, + ), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete.tsx new file mode 100644 index 0000000000..34f0abb196 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete.tsx @@ -0,0 +1,80 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { remoteBranchesCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.delete'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const formData = await request.formData(); + const branch = formData.get('branch'); + invariant(typeof branch === 'string', 'Branch is required'); + + try { + const vcs = VCSInstance(); + await vcs.removeRemoteBranch(branch); + try { + await vcs.removeBranch(branch); + } catch (err) { + // Branch doesn't exist locally, ignore + } + + delete remoteBranchesCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while merging branch.'; + return { + error: errorMessage, + }; + } + + return redirect( + href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug`, { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useInsomniaSyncBranchDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + branch, + organizationId, + projectId, + workspaceId, + }: { + branch: string; + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const formData = new FormData(); + formData.set('branch', branch); + + return fetcherSubmit(formData, { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/branch/delete`, + { + organizationId, + projectId, + workspaceId, + }, + ), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx new file mode 100644 index 0000000000..bf860ca6c8 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge.tsx @@ -0,0 +1,79 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import type { Operation } from '~/common/database'; +import { database } from '~/common/database'; +import { UserAbortResolveMergeConflictError, VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems, remoteCompareCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.branch.merge'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + const branch = formData.get('branch'); + invariant(typeof branch === 'string', 'Branch is required'); + const vcs = VCSInstance(); + const { syncItems } = await getSyncItems({ workspaceId }); + let delta; + try { + delta = await vcs.merge(syncItems, branch); + } catch (err) { + if (err instanceof UserAbortResolveMergeConflictError) { + return null; + } + throw err; + } + try { + await database.batchModifyDocs(delta as Operation); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while merging branch.'; + return { + error: errorMessage, + }; + } + + return null; +} + +export function useInsomniaSyncBranchMergeActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + branch, + organizationId, + projectId, + workspaceId, + }: { + branch: string; + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const formData = new FormData(); + formData.set('branch', branch); + + return fetcherSubmit(formData, { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/branch/merge`, + { + organizationId, + projectId, + workspaceId, + }, + ), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.create-snapshot.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.create-snapshot.tsx new file mode 100644 index 0000000000..58f26bfa65 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.create-snapshot.tsx @@ -0,0 +1,87 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { remoteCompareCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.create-snapshot'; + +interface CreateSnapshotData { + message: string; + push?: boolean; +} + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { projectId, workspaceId } = params; + + const data = (await request.json()) as CreateSnapshotData; + + invariant(typeof data.message === 'string', 'Message is required'); + + const vcs = VCSInstance(); + + try { + await vcs.takeSnapshot(data.message); + if (data.push) { + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + invariant(project.remoteId, 'Project is not remote'); + + await vcs.push({ + teamId: project.parentId, + teamProjectId: project.remoteId, + }); + } + + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while creating snapshot.'; + + return { + error: errorMessage, + }; + } + + return null; +} + +export function useInsomniaSyncCreateSnapshotActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + message, + push, + organizationId, + projectId, + workspaceId, + }: { + message: string; + organizationId: string; + projectId: string; + workspaceId: string; + push?: boolean; + }) => { + return fetcherSubmit(JSON.stringify({ message, push }), { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/create-snapshot`, + { + organizationId, + projectId, + workspaceId, + }, + ), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.fetch.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.fetch.tsx new file mode 100644 index 0000000000..f71fb7662b --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.fetch.tsx @@ -0,0 +1,81 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.fetch'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { projectId } = params; + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + const formData = await request.formData(); + const branch = formData.get('branch'); + invariant(typeof branch === 'string', 'Branch is required'); + const vcs = VCSInstance(); + const currentBranch = await vcs.getCurrentBranchName(); + + try { + invariant(project.remoteId, 'Project is not remote'); + await vcs.checkout([], branch); + const delta = await vcs.pull({ + candidates: [], + teamId: project.parentId, + teamProjectId: project.remoteId, + projectId, + }); + + await database.batchModifyDocs(delta); + } catch (err) { + await vcs.checkout([], currentBranch); + const errorMessage = err instanceof Error ? err.message : 'Unknown error while fetching remote branch.'; + return { + error: errorMessage, + }; + } + + return null; +} + +export function useInsomniaSyncFetchActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + branch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + branch: string; + }) => { + return fetcherSubmit( + { + branch, + }, + { + method: 'POST', + action: href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/fetch', { + organizationId, + projectId, + workspaceId, + }), + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.pull.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.pull.tsx new file mode 100644 index 0000000000..49743c69a9 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.pull.tsx @@ -0,0 +1,87 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { SegmentEvent } from '~/ui/analytics'; +import { getSyncItems, remoteCompareCache, vcsSegmentEventProperties } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.pull'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { projectId, workspaceId } = params; + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + const { syncItems } = await getSyncItems({ workspaceId }); + try { + invariant(project.remoteId, 'Project is not remote'); + const vcs = VCSInstance(); + const delta = await vcs.pull({ + candidates: syncItems, + teamId: project.parentId, + teamProjectId: project.remoteId, + projectId: project._id, + }); + + window.main.trackSegmentEvent({ + event: SegmentEvent.vcsAction, + properties: vcsSegmentEventProperties('remote', 'pull'), + }); + + await database.batchModifyDocs(delta); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while pulling from remote.'; + + window.main.trackSegmentEvent({ + event: SegmentEvent.vcsAction, + properties: vcsSegmentEventProperties('remote', 'pull', errorMessage), + }); + + return { + error: errorMessage, + }; + } + + return { + error: null, + success: true, + }; +} + +export function useInsomniaSyncPullActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + return fetcherSubmit( + {}, + { + action: href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/pull', { + organizationId, + projectId, + workspaceId, + }), + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.push.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.push.tsx new file mode 100644 index 0000000000..7ea33d0b87 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.push.tsx @@ -0,0 +1,82 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { SegmentEvent } from '~/ui/analytics'; +import { remoteCompareCache, vcsSegmentEventProperties } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.push'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { projectId, workspaceId } = params; + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + invariant(project.remoteId, 'Project is not remote'); + + try { + const vcs = VCSInstance(); + await vcs.push({ + teamId: project.parentId, + teamProjectId: project.remoteId, + }); + + window.main.trackSegmentEvent({ + event: SegmentEvent.vcsAction, + properties: vcsSegmentEventProperties('remote', 'push'), + }); + + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while pushing to remote.'; + + window.main.trackSegmentEvent({ + event: SegmentEvent.vcsAction, + properties: vcsSegmentEventProperties('remote', 'push', errorMessage), + }); + + return { + error: errorMessage, + }; + } + + return null; +} + +export function useInsomniaSyncPushActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/push', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore.tsx new file mode 100644 index 0000000000..dc0ac4dad7 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore.tsx @@ -0,0 +1,74 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import type { Operation } from '~/common/database'; +import { database } from '~/common/database'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems, remoteCompareCache } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.restore'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const formData = await request.formData(); + const id = formData.get('id'); + invariant(typeof id === 'string', 'Id is required'); + try { + const vcs = VCSInstance(); + const { syncItems } = await getSyncItems({ workspaceId }); + const delta = await vcs.rollback(id, syncItems); + await database.batchModifyDocs(delta as unknown as Operation); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while restoring changes.'; + return { + error: errorMessage, + }; + } + + return redirect( + href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug`, { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useInsomniaSyncRestoreActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + id, + organizationId, + projectId, + workspaceId, + }: { + id: string; + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const formData = new FormData(); + formData.set('id', id); + + return fetcherSubmit(formData, { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/restore`, { + organizationId, + projectId, + workspaceId, + }), + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.rollback.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.rollback.tsx new file mode 100644 index 0000000000..56d7bc4bac --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.rollback.tsx @@ -0,0 +1,73 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import type { Operation } from '~/common/database'; +import { database } from '~/common/database'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems, remoteCompareCache } from '~/ui/sync-utils'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.rollback'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + try { + const vcs = VCSInstance(); + const { syncItems } = await getSyncItems({ workspaceId }); + const delta = await vcs.rollbackToLatest(syncItems); + await database.batchModifyDocs(delta as unknown as Operation); + delete remoteCompareCache[workspaceId]; + } catch (err) { + const errorMessage = err instanceof Error ? err.message : 'Unknown error while rolling back changes.'; + return { + error: errorMessage, + }; + } + + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useInsomniaSyncRollbackActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/rollback', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage.tsx new file mode 100644 index 0000000000..243e236bd3 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage.tsx @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { isNotNullOrUndefined } from '~/common/misc'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.stage'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const data = await request.json(); + const keys = data.keys; + invariant(Array.isArray(keys), 'Keys are required'); + const { syncItems } = await getSyncItems({ workspaceId }); + const vcs = VCSInstance(); + const status = await vcs.status(syncItems); + // Staging needs to happen since it creates blobs for the files + const itemsToStage = keys + .map(key => { + if (typeof key === 'string') { + const item = status.unstaged[key]; + return item; + } + + return null; + }) + .filter(isNotNullOrUndefined); + + await vcs.stage(itemsToStage); + + return null; +} + +export function useInsomniaSyncStageActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + keys, + organizationId, + projectId, + workspaceId, + }: { + keys: string[]; + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + return fetcherSubmit(JSON.stringify({ keys }), { + method: 'POST', + action: href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/stage`, { + organizationId, + projectId, + workspaceId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data.tsx new file mode 100644 index 0000000000..d8fb61098f --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data.tsx @@ -0,0 +1,182 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { BackendProject, Compare } from '~/sync/types'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.sync-data'; + +const remoteBranchesCache: Record = {}; +const remoteCompareCache: Record = {}; +const remoteBackendProjectsCache: Record = {}; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { projectId, workspaceId } = params; + try { + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + invariant(project.remoteId, 'Project is not remote'); + const vcs = VCSInstance(); + const { syncItems } = await getSyncItems({ workspaceId }); + const localBranches = (await vcs.getBranchNames()).sort(); + const currentBranch = await vcs.getCurrentBranchName(); + const history = (await vcs.getHistory()).sort((a, b) => (b.created > a.created ? 1 : -1)); + const historyCount = await vcs.getHistoryCount(); + const status = await vcs.status(syncItems); + + let remoteBranches: string[] = []; + let compare = { ahead: 0, behind: 0 }; + try { + remoteBranches = (remoteBranchesCache[workspaceId] || (await vcs.getRemoteBranchNames())).sort(); + compare = remoteCompareCache[workspaceId] || (await vcs.compareRemoteBranch()); + const remoteBackendProjects = + remoteBackendProjectsCache[workspaceId] || + (await vcs.remoteBackendProjects({ + teamId: project.parentId, + teamProjectId: project.remoteId, + })); + remoteBranchesCache[workspaceId] = remoteBranches; + remoteCompareCache[workspaceId] = compare; + remoteBackendProjectsCache[workspaceId] = remoteBackendProjects; + + let hasUncommittedChanges = false; + if (status?.unstaged && Object.keys(status.unstaged).length > 0) { + hasUncommittedChanges = true; + } + if (status?.stage && Object.keys(status.stage).length > 0) { + hasUncommittedChanges = true; + } + // update workspace meta with sync data, use for show unpushed changes on collection card + models.workspaceMeta.updateByParentId(workspaceId, { + hasUncommittedChanges, + hasUnpushedChanges: compare?.ahead > 0, + }); + } catch {} + return { + syncItems, + localBranches, + remoteBranches, + currentBranch, + history, + historyCount, + status, + compare, + }; + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error while loading sync data.'; + return { + error: errorMessage, + }; + } +} + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { projectId, workspaceId } = params; + + try { + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + invariant(project.remoteId, 'Project is not remote'); + const vcs = VCSInstance(); + const remoteBranches = (await vcs.getRemoteBranchNames()).sort(); + const compare = await vcs.compareRemoteBranch(); + const remoteBackendProjects = await vcs.remoteBackendProjects({ + teamId: project.parentId, + teamProjectId: project.remoteId, + }); + + // Cache remote branches + remoteBranchesCache[workspaceId] = remoteBranches; + remoteCompareCache[workspaceId] = compare; + remoteBackendProjectsCache[workspaceId] = remoteBackendProjects; + + return { + remoteBranches, + compare, + remoteBackendProjects, + }; + } catch (e) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error while syncing data.'; + delete remoteBranchesCache[workspaceId]; + delete remoteCompareCache[workspaceId]; + delete remoteBackendProjectsCache[workspaceId]; + return { + error: errorMessage, + }; + } +} + +export function useInsomniaSyncDataLoaderFetcher(args?: Parameters[0]) { + const { load: fetcherLoad, ...fetcherRest } = useFetcher(args); + + const load = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/sync-data', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherLoad(url); + }, + [fetcherLoad], + ); + + return { + ...fetcherRest, + load, + }; +} + +export function useInsomniaSyncDataActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/sync-data', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.tsx new file mode 100644 index 0000000000..cbdb451163 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.tsx @@ -0,0 +1,91 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { Workspace } from '~/models/workspace'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync'; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId } = params; + invariant(typeof organizationId === 'string', 'Organization Id is required'); + invariant(typeof projectId === 'string', 'Project Id is required'); + + try { + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + const remoteId = project.remoteId; + if (!remoteId) { + return { + backendProjectsToPull: [], + }; + } + const vcs = VCSInstance(); + + const allPulledBackendProjectsForRemoteId = (await vcs.localBackendProjects()).filter(p => p.id === remoteId); + // Remote backend projects are fetched from the backend since they are not stored locally + const allFetchedRemoteBackendProjectsForRemoteId = await vcs.remoteBackendProjects({ + teamId: organizationId, + teamProjectId: remoteId, + }); + + // Get all workspaces that are connected to backend projects and under the current project + const workspacesWithBackendProjects = await database.find(models.workspace.type, { + _id: { + $in: [...allPulledBackendProjectsForRemoteId, ...allFetchedRemoteBackendProjectsForRemoteId].map( + p => p.rootDocumentId, + ), + }, + parentId: project._id, + }); + + // Get the list of remote backend projects that we need to pull + const backendProjectsToPull = allFetchedRemoteBackendProjectsForRemoteId.filter( + p => !workspacesWithBackendProjects.find(w => w._id === p.rootDocumentId), + ); + + return { + backendProjectsToPull, + }; + } catch (e) { + console.warn('Failed to load backend projects', e); + } + + return { + backendProjectsToPull: [], + }; +} + +export function useInsomniaSyncLoaderFetcher(args?: Parameters[0]) { + const { load: fetcherLoad, ...fetcherRest } = useFetcher(args); + + const load = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync', { + organizationId, + projectId, + workspaceId, + }); + + return fetcherLoad(url); + }, + [fetcherLoad], + ); + + return { + ...fetcherRest, + load, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage.tsx new file mode 100644 index 0000000000..8baa82a148 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage.tsx @@ -0,0 +1,74 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { isNotNullOrUndefined } from '~/common/misc'; +import { VCSInstance } from '~/sync/vcs/insomnia-sync'; +import { getSyncItems } from '~/ui/sync-utils'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.insomnia-sync.unstage'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const data = await request.json(); + const keys = data.keys; + invariant(Array.isArray(keys), 'Keys are required'); + const { syncItems } = await getSyncItems({ workspaceId }); + const vcs = VCSInstance(); + const status = await vcs.status(syncItems); + // Staging needs to happen since it creates blobs for the files + const itemsToUnstage = keys + .map(key => { + if (typeof key === 'string') { + const item = status.stage[key]; + return item; + } + + return null; + }) + .filter(isNotNullOrUndefined); + + await vcs.unstage(itemsToUnstage); + + return null; +} + +export function useInsomniaSyncUnstageActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + keys, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + keys: string[]; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/insomnia-sync/unstage', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherSubmit(JSON.stringify({ keys }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete.tsx new file mode 100644 index 0000000000..bac8887990 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete.tsx @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId, mockRouteId } = params; + invariant(typeof mockRouteId === 'string', 'Mock route id is required'); + const mockRoute = await models.mockRoute.getById(mockRouteId); + invariant(mockRoute, 'mockRoute not found'); + const { isSelected } = await request.json(); + + await models.mockRoute.remove(mockRoute); + if (isSelected) { + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server', { + organizationId, + projectId, + workspaceId, + }), + ); + } + return null; +} + +export function useMockRouteDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + mockRouteId, + isSelected, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + mockRouteId: string; + isSelected: boolean; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId/delete', + { + organizationId, + projectId, + workspaceId, + mockRouteId, + }, + ); + + return fetcherSubmit(JSON.stringify({ isSelected }), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx similarity index 80% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx index 4f8d1e6f5f..609a04ad5a 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.tsx @@ -1,7 +1,7 @@ import type * as Har from 'har-format'; -import React from 'react'; +import { useCallback } from 'react'; import { Button, Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-aria-components'; -import { type LoaderFunction, useFetcher, useParams, useRouteLoaderData } from 'react-router'; +import { useParams, useRouteLoaderData } from 'react-router'; import { CONTENT_TYPE_JSON, @@ -13,28 +13,32 @@ import { getMockServiceBinURL, getMockServiceURL, RESPONSE_CODE_REASONS, -} from '../../common/constants'; -import { database as db } from '../../common/database'; -import { getResponseCookiesFromHeaders } from '../../common/har'; -import * as models from '../../models'; -import type { MockRoute } from '../../models/mock-route'; -import type { MockServer } from '../../models/mock-server'; -import type { Request, RequestHeader } from '../../models/request'; -import type { Response } from '../../models/response'; -import { invariant } from '../../utils/invariant'; -import { Dropdown, DropdownItem, ItemContent } from '../components/base/dropdown'; -import { CodeEditor } from '../components/codemirror/code-editor'; -import { MockResponseHeadersEditor } from '../components/editors/mock-response-headers-editor'; -import { MockResponsePane } from '../components/mocks/mock-response-pane'; -import { MockUrlBar } from '../components/mocks/mock-url-bar'; -import { showModal } from '../components/modals'; -import { AlertModal } from '../components/modals/alert-modal'; -import { EmptyStatePane } from '../components/panes/empty-state-pane'; -import { Pane, PaneBody, PaneHeader } from '../components/panes/pane'; -import { SvgIcon } from '../components/svg-icon'; -import { insomniaFetch } from '../insomniaFetch'; -import type { MockServerLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId.mock-server'; -import { useRootLoaderData } from './root'; +} from '~/common/constants'; +import { database as db } from '~/common/database'; +import { getResponseCookiesFromHeaders } from '~/common/har'; +import * as models from '~/models'; +import type { MockRoute } from '~/models/mock-route'; +import type { MockServer } from '~/models/mock-server'; +import type { Request, RequestHeader } from '~/models/request'; +import type { Response } from '~/models/response'; +import { useRootLoaderData } from '~/root'; +import { useRequestNewMockSendActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send'; +import { useMockRouteUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; +import { Dropdown, DropdownItem, ItemContent } from '~/ui/components/base/dropdown'; +import { MockResponseHeadersEditor } from '~/ui/components/editors/mock-response-headers-editor'; +import { MockResponsePane } from '~/ui/components/mocks/mock-response-pane'; +import { MockUrlBar } from '~/ui/components/mocks/mock-url-bar'; +import { showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { EmptyStatePane } from '~/ui/components/panes/empty-state-pane'; +import { Pane, PaneBody, PaneHeader } from '~/ui/components/panes/pane'; +import { SvgIcon } from '~/ui/components/svg-icon'; +import { insomniaFetch } from '~/ui/insomniaFetch'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; +import { useMockServerLoaderData } from './organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server'; export interface MockRouteLoaderData { mockServer: MockServer; @@ -42,12 +46,9 @@ export interface MockRouteLoaderData { activeResponse?: Response; } -export const loader: LoaderFunction = async ({ params }): Promise => { - const { organizationId, projectId, workspaceId, mockRouteId } = params; - invariant(organizationId, 'Organization ID is required'); - invariant(projectId, 'Project ID is required'); - invariant(workspaceId, 'Workspace ID is required'); - invariant(mockRouteId, 'Mock route ID is required'); +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { workspaceId, mockRouteId } = params; + const mockServer = await models.mockServer.getByParentId(workspaceId); invariant(mockServer, 'Mock server is required'); const mockRoute = await models.mockRoute.getById(mockRouteId); @@ -73,7 +74,7 @@ export const loader: LoaderFunction = async ({ params }): Promise { - const { organizationId, projectId, workspaceId } = useParams<{ + const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; - }>(); - const fetcher = useFetcher(); - return (id: string, patch: Partial) => { - fetcher.submit(JSON.stringify(patch), { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${id}/update`, - method: 'post', - encType: 'application/json', - }); }; + const { submit } = useMockRouteUpdateActionFetcher(); + return useCallback( + (id: string, patch: Partial) => { + submit({ + mockRouteId: id, + organizationId, + projectId, + workspaceId, + patch, + }); + }, + [organizationId, projectId, submit, workspaceId], + ); }; -export const MockRouteRoute = () => { - const { mockServer, mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData; - const { mockRoutes } = useRouteLoaderData('mock-server') as MockServerLoaderData; +export function useMockRouteLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId', + ); +} - const { userSession } = useRootLoaderData(); +export const MockRouteRoute = () => { + const { mockServer, mockRoute } = useMockRouteLoaderData()!; + const { mockRoutes } = useMockServerLoaderData()!; + + const { userSession } = useRootLoaderData()!; const patchMockRoute = useMockRoutePatcher(); const mockbinUrl = mockServer.useInsomniaCloud ? getMockServiceURL() : mockServer.url; - const requestFetcher = useFetcher({ key: 'mock-request-fetcher' }); + const requestFetcher = useRequestNewMockSendActionFetcher({ key: 'mock-request-fetcher' }); const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; @@ -184,16 +197,17 @@ export const MockRouteRoute = () => { console.log('[mock] Error: invalid response from remote', { res, mockbinUrl }); return 'Unexpected response, see console for details'; } catch (e) { - console.log(e); - return `Unhandled contacting Mock API at ${mockbinUrl}\n${e.message}`; + const errorMessage = e instanceof Error ? e.message : String(e); + return `Unhandled contacting Mock API at ${mockbinUrl}\n${errorMessage}`; } }; const createAndSendPrivateRequest = (patch: Partial) => - requestFetcher.submit(JSON.stringify(patch), { - encType: 'application/json', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/debug/request/new-mock-send`, - method: 'post', + requestFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch, }); const upsertMockbinHar = async (pathInput?: string) => { const hasRouteInServer = mockRoutes diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update.tsx new file mode 100644 index 0000000000..0b8630054c --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update.tsx @@ -0,0 +1,63 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { MockRoute } from '~/models/mock-route'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { mockRouteId } = params; + + const patch = (await request.json()) as Partial; + + const mockRoute = await models.mockRoute.getById(mockRouteId); + invariant(mockRoute, 'Mock route is required'); + + await models.mockRoute.update(mockRoute, patch); + + return null; +} + +export function useMockRouteUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + mockRouteId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + mockRouteId: string; + patch: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId/update', + { + organizationId, + projectId, + workspaceId, + mockRouteId, + }, + ); + + return fetcherSubmit(JSON.stringify(patch), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new.tsx new file mode 100644 index 0000000000..4978b340a3 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new.tsx @@ -0,0 +1,98 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import type { MockRoute } from '~/models/mock-route'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new'; + +type NewMockRoutePatch = { mockServerName?: string } & Partial; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const patch = (await request.json()) as NewMockRoutePatch; + invariant(typeof patch.name === 'string', 'Name is required'); + // TODO: remove this hack which enables a mock server to be created alongside a route + // TODO: use an alternate method to create new workspace and server together + // create a mock server under the workspace with the same name + if (patch.mockServerName) { + const collectionWorkspace = await models.workspace.getById(workspaceId); + invariant(collectionWorkspace, 'Collection workspace not found'); + const mockWorkspace = await models.workspace.create({ + name: collectionWorkspace.name, + scope: 'mock-server', + parentId: projectId, + }); + invariant(mockWorkspace, 'Workspace not found'); + const newMockServer = await models.mockServer.getOrCreateForParentId(mockWorkspace._id, { + name: collectionWorkspace.name, + }); + delete patch.mockServerName; + const mockRoute = await models.mockRoute.create({ ...patch, parentId: newMockServer._id }); + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId', + { + organizationId, + projectId, + workspaceId: newMockServer.parentId, + mockRouteId: mockRoute._id, + }, + ), + ); + } + invariant(patch.parentId, 'parentId is required'); + const mockServer = await models.mockServer.getById(patch.parentId); + invariant(mockServer, 'Mock server not found'); + const mockRoute = await models.mockRoute.create(patch); + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/:mockRouteId', + { + organizationId, + projectId, + workspaceId: mockServer.parentId, + mockRouteId: mockRoute._id, + }, + ), + ); +} + +export function useMockRouteNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + patch, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + patch: NewMockRoutePatch; + }) => { + return fetcherSubmit(JSON.stringify(patch), { + method: 'POST', + action: href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/mock-server/mock-route/new`, + { + organizationId, + projectId, + workspaceId, + }, + ), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx similarity index 81% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx index 808ae6ba9b..4509b863c4 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.tsx @@ -13,52 +13,54 @@ import { } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; import { - type LoaderFunction, NavLink, - Route, + Route as RouteComponent, Routes, - useFetcher, useLoaderData, useNavigate, useParams, useRouteLoaderData, } from 'react-router'; -import { DEFAULT_SIDEBAR_SIZE } from '../../common/constants'; -import * as models from '../../models'; -import type { MockRoute } from '../../models/mock-route'; -import { invariant } from '../../utils/invariant'; -import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; -import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; -import { EditableInput } from '../components/editable-input'; -import { Icon } from '../components/icon'; -import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; -import { showModal } from '../components/modals'; -import { AlertModal } from '../components/modals/alert-modal'; -import { AskModal } from '../components/modals/ask-modal'; -import { PromptModal } from '../components/modals/prompt-modal'; -import { EmptyStatePane } from '../components/panes/empty-state-pane'; -import { SvgIcon } from '../components/svg-icon'; -import { OrganizationTabList } from '../components/tabs/tab-list'; -import { formatMethodName } from '../components/tags/method-tag'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/use-insomnia-tab'; -import type { WorkspaceLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId'; +import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants'; +import * as models from '~/models'; +import type { MockRoute } from '~/models/mock-route'; +import { useRootLoaderData } from '~/root'; +import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { useMockRouteDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.delete'; +import { useMockRouteUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId.update'; +import { useMockRouteNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new'; +import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown'; +import { WorkspaceSyncDropdown } from '~/ui/components/dropdowns/workspace-sync-dropdown'; +import { EditableInput } from '~/ui/components/editable-input'; +import { Icon } from '~/ui/components/icon'; +import { useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder'; +import { showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { AskModal } from '~/ui/components/modals/ask-modal'; +import { PromptModal } from '~/ui/components/modals/prompt-modal'; +import { EmptyStatePane } from '~/ui/components/panes/empty-state-pane'; +import { SvgIcon } from '~/ui/components/svg-icon'; +import { OrganizationTabList } from '~/ui/components/tabs/tab-list'; +import { formatMethodName } from '~/ui/components/tags/method-tag'; +import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant'; +import { useInsomniaTab } from '~/ui/hooks/use-insomnia-tab'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server'; import { MockRouteResponse, MockRouteRoute, useMockRoutePatcher, -} from './$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; -import { useRootLoaderData } from './root'; +} from './organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; + export interface MockServerLoaderData { mockServerId: string; mockRoutes: MockRoute[]; } -export const loader: LoaderFunction = async ({ params }): Promise => { - const { organizationId, projectId, workspaceId } = params; - invariant(organizationId, 'Organization ID is required'); - invariant(projectId, 'Project ID is required'); - invariant(workspaceId, 'Workspace ID is required'); + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { workspaceId } = params; const activeWorkspace = await models.workspace.getById(workspaceId); invariant(activeWorkspace, 'Workspace not found'); @@ -70,21 +72,29 @@ export const loader: LoaderFunction = async ({ params }): Promise { +export function useMockServerLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server', + ); +} + +const Component = () => { const { organizationId, projectId, workspaceId, mockRouteId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; mockRouteId: string; }; - const { settings } = useRootLoaderData(); + const { settings } = useRootLoaderData()!; const { mockServerId, mockRoutes } = useLoaderData() as MockServerLoaderData; - const { activeProject, activeWorkspace } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const { activeProject, activeWorkspace } = useWorkspaceLoaderData()!; - const fetcher = useFetcher(); + const deleteMockRouteFetcher = useMockRouteDeleteActionFetcher(); + const createMockRouteFetcher = useMockRouteNewActionFetcher(); + const updateMockRouteFetcher = useMockRouteUpdateActionFetcher(); const navigate = useNavigate(); const patchMockRoute = useMockRoutePatcher(); const mockRouteActionList: { @@ -141,16 +151,13 @@ const MockServerRoute = () => { noText: 'Cancel', onDone: async (isYes: boolean) => { if (isYes) { - fetcher.submit( - { - isSelected: mockRouteId === id, - }, - { - encType: 'application/json', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${id}/delete`, - method: 'POST', - }, - ); + deleteMockRouteFetcher.submit({ + organizationId, + projectId, + workspaceId, + mockRouteId: id, + isSelected: mockRouteId === id, + }); } }, }); @@ -277,17 +284,15 @@ const MockServerRoute = () => { }); return; } - fetcher.submit( - { + createMockRouteFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { name, parentId: mockServerId, }, - { - encType: 'application/json', - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`, - }, - ); + }); }, }); }} @@ -365,14 +370,15 @@ const MockServerRoute = () => { return; } name && - fetcher.submit( - { name }, - { - encType: 'application/json', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/${item._id}/update`, - method: 'POST', + updateMockRouteFetcher.submit({ + organizationId, + projectId, + workspaceId, + mockRouteId: item._id, + patch: { + name, }, - ); + }); }} /> @@ -422,7 +428,7 @@ const MockServerRoute = () => { - @@ -430,7 +436,7 @@ const MockServerRoute = () => { } /> - { /> - @@ -455,7 +461,7 @@ const MockServerRoute = () => { } /> - { ); }; -export default MockServerRoute; +export default Component; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx new file mode 100644 index 0000000000..13c726996b --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection.tsx @@ -0,0 +1,102 @@ +import path from 'node:path'; + +import type { IRuleResult } from '@stoplight/spectral-core'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import { importResourcesToWorkspace, scanResources } from '~/common/import'; +import * as models from '~/models'; +import { isGitProject } from '~/models/project'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId } = params; + + const project = await models.project.getById(projectId); + invariant(project, 'Project not found'); + + const apiSpec = await models.apiSpec.getByParentId(workspaceId); + invariant(apiSpec, 'No API Specification was found'); + + const workspace = await models.workspace.getById(workspaceId); + + invariant(workspace, 'Workspace not found'); + + const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); + + const isLintError = (result: IRuleResult) => result.severity === 0; + + const gitRepositoryId = isGitProject(project) ? project.gitRepositoryId : workspaceMeta?.gitRepositoryId; + + const rulesetPath = gitRepositoryId + ? path.join(window.app.getPath('userData'), `version-control/git/${gitRepositoryId}/other/.spectral.yaml`) + : ''; + + const { diagnostics, error } = await window.main.lintSpec({ documentContent: apiSpec.contents, rulesetPath }); + if (error) { + throw error; + } + const results = diagnostics?.filter(isLintError); + if (apiSpec.contents && results && results.length) { + throw new Error('Error Generating Configuration'); + } + + await scanResources([ + { + contentStr: apiSpec.contents, + }, + ]); + + await importResourcesToWorkspace({ + workspaceId, + }); + + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/debug', { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useSpecGenerateRequestCollectionActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/spec/generate-request-collection', + { + organizationId, + projectId, + workspaceId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx similarity index 91% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx index c1bde4e912..62b33f5c84 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.tsx @@ -3,17 +3,7 @@ import path from 'node:path'; import { type IRuleResult } from '@stoplight/spectral-core'; import CodeMirror from 'codemirror'; import type { OpenAPIV3 } from 'openapi-types'; -import React, { - type FC, - Fragment, - type ReactNode, - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; +import { Fragment, type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { Breadcrumb, Breadcrumbs, @@ -32,48 +22,43 @@ import { TooltipTrigger, } from 'react-aria-components'; import { type ImperativePanelGroupHandle, Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { type LoaderFunction, NavLink, useFetcher, useLoaderData, useParams, useRouteLoaderData } from 'react-router'; -import { useUnmount } from 'react-use'; +import { NavLink, useLoaderData } from 'react-router'; +import * as reactUse from 'react-use'; import { SwaggerUIBundle } from 'swagger-ui-dist'; import YAML from 'yaml'; -import { parseApiSpec } from '../../common/api-specs'; -import { ACTIVITY_SPEC, DEFAULT_SIDEBAR_SIZE } from '../../common/constants'; -import { debounce, isNotNullOrUndefined } from '../../common/misc'; -import type { ApiSpec } from '../../models/api-spec'; -import * as models from '../../models/index'; -import { isGitProject } from '../../models/project'; -import { invariant } from '../../utils/invariant'; -import { CodeEditor, type CodeEditorHandle } from '../components/codemirror/code-editor'; -import { DesignEmptyState } from '../components/design-empty-state'; -import { DocumentTab } from '../components/document-tab'; -import { WorkspaceDropdown } from '../components/dropdowns/workspace-dropdown'; -import { WorkspaceSyncDropdown } from '../components/dropdowns/workspace-sync-dropdown'; -import { EnvironmentPicker } from '../components/environment-picker'; -import { Icon } from '../components/icon'; -import { useDocBodyKeyboardShortcuts } from '../components/keydown-binder'; -import { showError } from '../components/modals'; -import { CookiesModal } from '../components/modals/cookies-modal'; -import { CertificatesModal } from '../components/modals/workspace-certificates-modal'; -import { WorkspaceEnvironmentsEditModal } from '../components/modals/workspace-environments-edit-modal'; -import { OrganizationTabList } from '../components/tabs/tab-list'; -import { formatMethodName } from '../components/tags/method-tag'; -import { INSOMNIA_TAB_HEIGHT } from '../constant'; -import { useInsomniaTab } from '../hooks/use-insomnia-tab'; -import { useActiveApiSpecSyncVCSVersion, useGitVCSVersion } from '../hooks/use-vcs-version'; -import type { WorkspaceLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId'; -import { useRootLoaderData } from './root'; +import { parseApiSpec } from '~/common/api-specs'; +import { DEFAULT_SIDEBAR_SIZE } from '~/common/constants'; +import { debounce, isNotNullOrUndefined } from '~/common/misc'; +import * as models from '~/models/index'; +import { isGitProject } from '~/models/project'; +import { useRootLoaderData } from '~/root'; +import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { useSpecGenerateRequestCollectionActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.generate-request-collection'; +import { useSpecUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; +import { DesignEmptyState } from '~/ui/components/design-empty-state'; +import { DocumentTab } from '~/ui/components/document-tab'; +import { WorkspaceDropdown } from '~/ui/components/dropdowns/workspace-dropdown'; +import { WorkspaceSyncDropdown } from '~/ui/components/dropdowns/workspace-sync-dropdown'; +import { EnvironmentPicker } from '~/ui/components/environment-picker'; +import { Icon } from '~/ui/components/icon'; +import { useDocBodyKeyboardShortcuts } from '~/ui/components/keydown-binder'; +import { showError } from '~/ui/components/modals'; +import { CookiesModal } from '~/ui/components/modals/cookies-modal'; +import { CertificatesModal } from '~/ui/components/modals/workspace-certificates-modal'; +import { WorkspaceEnvironmentsEditModal } from '~/ui/components/modals/workspace-environments-edit-modal'; +import { OrganizationTabList } from '~/ui/components/tabs/tab-list'; +import { formatMethodName } from '~/ui/components/tags/method-tag'; +import { INSOMNIA_TAB_HEIGHT } from '~/ui/constant'; +import { useInsomniaTab } from '~/ui/hooks/use-insomnia-tab'; +import { useActiveApiSpecSyncVCSVersion, useGitVCSVersion } from '~/ui/hooks/use-vcs-version'; +import { invariant } from '~/utils/invariant'; -interface LoaderData { - apiSpec: ApiSpec; - rulesetPath: string; - parsedSpec?: OpenAPIV3.Document; -} +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec'; -export const loader: LoaderFunction = async ({ params }): Promise => { +export async function clientLoader({ params }: Route.ClientLoaderArgs) { const { projectId, workspaceId } = params; - invariant(projectId, 'Project ID is required'); - invariant(workspaceId, 'Workspace ID is required'); const project = await models.project.getById(projectId); invariant(project, 'Project not found'); @@ -104,14 +89,14 @@ export const loader: LoaderFunction = async ({ params }): Promise => rulesetPath, parsedSpec, }; -}; +} const SwaggerUIDiv = ({ text }: { text: string }) => { useEffect(() => { let spec = {}; try { spec = parseApiSpec(text).contents || {}; - } catch (err) {} + } catch {} SwaggerUIBundle({ spec, dom_id: '#swagger-ui' }); }, [text]); return ( @@ -125,6 +110,7 @@ const SwaggerUIDiv = ({ text }: { text: string }) => { /> ); }; + interface LintMessage { type: 'error' | 'warning' | 'info'; message: string; @@ -154,29 +140,24 @@ const lintOptions = { delay: 1000, }; -const Design: FC = () => { - const { organizationId, projectId, workspaceId } = useParams() as { - organizationId: string; - projectId: string; - workspaceId: string; - }; - const { activeProject, activeCookieJar, caCertificate, clientCertificates, activeWorkspace } = useRouteLoaderData( - ':workspaceId', - ) as WorkspaceLoaderData; - const { settings } = useRootLoaderData(); +const Component = ({ params }: Route.ComponentProps) => { + const { organizationId, projectId, workspaceId } = params; + const { activeProject, activeCookieJar, caCertificate, clientCertificates, activeWorkspace } = + useWorkspaceLoaderData()!; + const { settings } = useRootLoaderData()!; const [isCookieModalOpen, setIsCookieModalOpen] = useState(false); const [isEnvironmentModalOpen, setEnvironmentModalOpen] = useState(false); const [isEnvironmentPickerOpen, setIsEnvironmentPickerOpen] = useState(false); const [isCertificatesModalOpen, setCertificatesModalOpen] = useState(false); - const { apiSpec, rulesetPath, parsedSpec } = useLoaderData() as LoaderData; + const { apiSpec, rulesetPath, parsedSpec } = useLoaderData(); const [lintMessages, setLintMessages] = useState([]); const editor = useRef(null); - const updateApiSpecFetcher = useFetcher(); - const generateRequestCollectionFetcher = useFetcher(); + const { submit: updateApiSpec } = useSpecUpdateActionFetcher(); + const generateRequestCollectionFetcher = useSpecGenerateRequestCollectionActionFetcher(); const [isLintPaneOpen, setIsLintPaneOpen] = useState(false); const [isSpecPaneOpen, setIsSpecPaneOpen] = useState(Boolean(parsedSpec)); @@ -235,26 +216,23 @@ const Design: FC = () => { editor.current?.tryToSetOption('lint', { ...lintOptions }); }, [rulesetPath]); - useUnmount(() => { + reactUse.useUnmount(() => { // delete the helper to avoid it run multiple times when user enter the page next time CodeMirror.registerHelper('lint', 'openapi', undefined); }); const onCodeEditorChange = useMemo(() => { const handler = async (contents: string) => { - updateApiSpecFetcher.submit( - { - contents: contents, - }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}/update`, - method: 'post', - }, - ); + return updateApiSpec({ + organizationId, + projectId, + workspaceId, + contents: contents, + }); }; return debounce(handler, 500); - }, [organizationId, projectId, updateApiSpecFetcher, workspaceId]); + }, [organizationId, projectId, updateApiSpec, workspaceId]); const handleScrollToSelection = useCallback( (chStart: number, chEnd: number, lineStart: number, lineEnd: number) => { @@ -356,13 +334,11 @@ const Design: FC = () => { icon: , isDisabled: !apiSpec.contents || lintErrors.length > 0 || generateRequestCollectionFetcher.state !== 'idle', action: () => - generateRequestCollectionFetcher.submit( - {}, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}/generate-request-collection`, - method: 'POST', - }, - ), + generateRequestCollectionFetcher.submit({ + organizationId, + projectId, + workspaceId, + }), }, { id: 'toggle-preview', @@ -910,16 +886,13 @@ const Design: FC = () => { {apiSpec.contents ? null : ( { - updateApiSpecFetcher.submit( - { - contents: value, - fromSync: 'true', - }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/${ACTIVITY_SPEC}/update`, - method: 'post', - }, - ); + updateApiSpec({ + organizationId, + projectId, + workspaceId, + contents: value, + fromSync: true, + }); }} /> )} @@ -1028,4 +1001,4 @@ const Design: FC = () => { ); }; -export default Design; +export default Component; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update.tsx new file mode 100644 index 0000000000..8ccaecb8fb --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update.tsx @@ -0,0 +1,76 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.spec.update'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { workspaceId } = params; + + const formData = await request.formData(); + const contents = formData.get('contents'); + const fromSync = Boolean(formData.get('fromSync')); + + invariant(typeof contents === 'string', 'Contents is required'); + + const apiSpec = await models.apiSpec.getByParentId(workspaceId); + + invariant(apiSpec, 'API Spec not found'); + await database.update( + { + ...apiSpec, + modified: Date.now(), + created: fromSync ? Date.now() : apiSpec.created, + contents, + }, + fromSync, + ); + + return null; +} + +export function useSpecUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + contents, + fromSync = false, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + contents: string; + fromSync?: boolean; + }) => { + const url = href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/spec/update', { + organizationId, + projectId, + workspaceId, + }); + + const formData = new FormData(); + formData.append('contents', contents); + if (fromSync) { + formData.append('fromSync', 'true'); + } + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test._index.tsx new file mode 100644 index 0000000000..0136e8320e --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test._index.tsx @@ -0,0 +1,39 @@ +import { href, redirect } from 'react-router'; + +import * as models from '~/models'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test._index'; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId, workspaceId } = params; + + const workspaceMeta = await models.workspaceMeta.getByParentId(workspaceId); + if (workspaceMeta?.activeUnitTestSuiteId) { + const unitTestSuite = await models.unitTestSuite.getById(workspaceMeta.activeUnitTestSuiteId); + + if (unitTestSuite) { + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId', { + organizationId, + projectId, + workspaceId, + testSuiteId: unitTestSuite._id, + }), + ); + } + } + + const unitTestSuites = await models.unitTestSuite.findByParentId(workspaceId); + if (unitTestSuites.length > 0) { + return redirect( + href('/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId', { + organizationId, + projectId, + workspaceId, + testSuiteId: unitTestSuites[0]._id, + }), + ); + } + + return null; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId._index.tsx new file mode 100644 index 0000000000..25e8830f3f --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId._index.tsx @@ -0,0 +1,28 @@ +import { href, redirect } from 'react-router'; + +import * as models from '~/models'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId._index'; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId, workspaceId, testSuiteId } = params; + + const testResult = await models.unitTestResult.getLatestByParentId(workspaceId); + + if (testResult) { + return redirect( + href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId`, + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testResultId: testResult._id, + }, + ), + ); + } + + return null; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.delete.tsx new file mode 100644 index 0000000000..092ac7174f --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.delete.tsx @@ -0,0 +1,70 @@ +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { SegmentEvent } from '~/ui/analytics'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.delete'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId, workspaceId, projectId, testSuiteId } = params; + + const unitTestSuite = await models.unitTestSuite.getById(testSuiteId); + + invariant(unitTestSuite, 'Test Suite not found'); + + await models.unitTestSuite.remove(unitTestSuite); + + window.main.trackSegmentEvent({ event: SegmentEvent.testSuiteDelete }); + + return redirect( + href(`/organization/:organizationId/project/:projectId/workspace/:workspaceId/test`, { + organizationId, + projectId, + workspaceId, + }), + ); +} + +export function useTestSuiteDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/delete', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx new file mode 100644 index 0000000000..9fcc4339a6 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests.tsx @@ -0,0 +1,153 @@ +import { generate, runTests, type Test, type TestResults } from 'insomnia-testing'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { UnitTest } from '~/models/unit-test'; +import { getSendRequestCallback } from '~/network/unit-test-feature'; +import { SegmentEvent } from '~/ui/analytics'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId, testSuiteId } = params; + + const unitTests = await database.find(models.unitTest.type, { parentId: testSuiteId }, { metaSortKey: 1 }); + invariant(unitTests, 'No unit tests found'); + + const tests: Test[] = unitTests + .filter(t => t !== null) + .map(t => ({ + name: t.name, + code: t.code, + defaultRequestId: t.requestId, + })); + + const src = generate([{ name: 'My Suite', suites: [], tests }]); + + const sendRequest = getSendRequestCallback(); + + let results: TestResults = { + failures: [], + passes: [], + pending: [], + stats: { + suites: 0, + tests: 0, + passes: 0, + pending: 0, + failures: 0, + start: undefined, + end: undefined, + duration: undefined, + }, + tests: [], + }; + + try { + results = await runTests(src, { sendRequest }); + const testResult = await models.unitTestResult.create({ + results, + parentId: workspaceId, + }); + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestRunAll, properties: { organizationId, projectId } }); + + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testResultId: testResult._id, + }, + ), + ); + } catch (err) { + const errorMessage = err instanceof Error ? err.toString() : 'Unknown error occurred'; + + // create a result manually so that it can be displayed in the UI + results.stats.failures = 1; + results.stats.tests = 1; + results.tests.push({ + currentRetry: 0, + duration: 0, + err: { + actual: undefined, + expected: undefined, + message: errorMessage, + multiple: [], + operator: undefined, + showDiff: false, + stack: '', + }, + file: '', + fullTitle: 'Test Error', + id: '', + title: 'Test Error', + }); + const testResult = await models.unitTestResult.create({ + results, + parentId: workspaceId, + }); + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestRunAll, properties: { organizationId, projectId } }); + + return redirect( + href( + `/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId`, + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testResultId: testResult._id, + }, + ), + ); + } +} + +export function useRunAllTestsActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/run-all-tests', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test-result.$testResultId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId.tsx similarity index 63% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test-result.$testResultId.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId.tsx index a082d9997c..469f1b04d2 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.test-result.$testResultId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId.tsx @@ -1,37 +1,17 @@ -import React, { type FC } from 'react'; import { Heading } from 'react-aria-components'; -import { type LoaderFunction, redirect, useRouteLoaderData } from 'react-router'; +import { useRouteLoaderData } from 'react-router'; -import { database } from '../../common/database'; -import * as models from '../../models'; -import type { UnitTestResult } from '../../models/unit-test-result'; -import { invariant } from '../../utils/invariant'; -import { Icon } from '../components/icon'; +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { UnitTestResult } from '~/models/unit-test-result'; +import { Icon } from '~/ui/components/icon'; +import { invariant } from '~/utils/invariant'; -interface TestResultsData { - testResult: UnitTestResult; -} +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId'; -export const indexLoader: LoaderFunction = async ({ params }) => { - const { organizationId, projectId, workspaceId, testSuiteId } = params; - invariant(projectId, 'Project ID is required'); - invariant(organizationId, 'Organization ID is required'); - invariant(workspaceId, 'Workspace ID is required'); - invariant(testSuiteId, 'Test suite ID is required'); - - const testResult = await models.unitTestResult.getLatestByParentId(workspaceId); - if (testResult) { - return redirect( - `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${testSuiteId}/test-result/${testResult._id}`, - ); - } - - return null; -}; - -export const loader: LoaderFunction = async ({ params }): Promise => { +export async function clientLoader({ params }: Route.ClientLoaderArgs) { const { testResultId } = params; - invariant(testResultId, 'Test Result ID is required'); + const testResult = await database.getWhere(models.unitTestResult.type, { _id: testResultId, }); @@ -39,10 +19,16 @@ export const loader: LoaderFunction = async ({ params }): Promise { - const { testResult } = useRouteLoaderData(':testResultId') as TestResultsData; +function useTestResultLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result.$testResultId', + ); +} + +export const TestRunStatus = () => { + const { testResult } = useTestResultLoaderData()!; if (!testResult) { return null; @@ -95,3 +81,5 @@ export const TestRunStatus: FC = () => {
); }; + +export default TestRunStatus; diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result._index.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result._index.tsx new file mode 100644 index 0000000000..648cc1302e --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result._index.tsx @@ -0,0 +1,21 @@ +import { href, redirect } from 'react-router'; + +import * as models from '~/models'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test-result._index'; + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId, projectId, workspaceId, testSuiteId } = params; + + const testResult = await models.unitTestResult.getLatestByParentId(workspaceId); + if (testResult) { + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId', + { organizationId, projectId, workspaceId, testSuiteId, testResultId: testResult._id }, + ), + ); + } + + return null; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete.tsx new file mode 100644 index 0000000000..12d9229e5b --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete.tsx @@ -0,0 +1,69 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { UnitTest } from '~/models/unit-test'; +import { SegmentEvent } from '~/ui/analytics'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { testId } = params; + + const unitTest = await database.getWhere(models.unitTest.type, { + _id: testId, + }); + invariant(unitTest, 'Test not found'); + + await models.unitTest.remove(unitTest); + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestDelete }); + + return null; +} + +export function useTestDeleteActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + testId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test/:testId/delete', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx new file mode 100644 index 0000000000..5baa8eb8ec --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run.tsx @@ -0,0 +1,157 @@ +import { generate, runTests, type Test, type TestResults } from 'insomnia-testing'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { UnitTest } from '~/models/unit-test'; +import { getSendRequestCallback } from '~/network/unit-test-feature'; +import { SegmentEvent } from '~/ui/analytics'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId, projectId, workspaceId, testSuiteId, testId } = params; + + const unitTest = await database.getWhere(models.unitTest.type, { + _id: testId, + }); + invariant(unitTest, 'Test not found'); + + const tests: Test[] = [ + { + name: unitTest.name, + code: unitTest.code, + defaultRequestId: unitTest.requestId, + }, + ]; + const src = generate([{ name: 'My Suite', suites: [], tests }]); + + const sendRequest = getSendRequestCallback(); + + let results: TestResults = { + failures: [], + passes: [], + pending: [], + stats: { + suites: 0, + tests: 0, + passes: 0, + pending: 0, + failures: 0, + start: undefined, + end: undefined, + duration: undefined, + }, + tests: [], + }; + + try { + results = await runTests(src, { sendRequest }); + const testResult = await models.unitTestResult.create({ + results, + parentId: unitTest.parentId, + }); + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestRun, properties: { organizationId, projectId } }); + + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testResultId: testResult._id, + }, + ), + ); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + + // create a result manually so that it can be displayed in the UI + results.stats.failures = 1; + results.stats.tests = 1; + results.tests.push({ + currentRetry: 0, + duration: 0, + err: { + actual: undefined, + expected: undefined, + message: errorMessage, + multiple: [], + operator: undefined, + showDiff: false, + stack: '', + }, + file: '', + fullTitle: unitTest.name, + id: '', + title: unitTest.name, + }); + const testResult = await models.unitTestResult.create({ + results, + parentId: unitTest.parentId, + }); + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestRun, properties: { organizationId, projectId } }); + + return redirect( + href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test-result/:testResultId', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testResultId: testResult._id, + }, + ), + ); + } +} + +export function useTestRunActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + testId: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test/:testId/run', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + }, + ); + + return fetcherSubmit( + {}, + { + action: url, + method: 'POST', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update.tsx new file mode 100644 index 0000000000..75f52ca5b5 --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update.tsx @@ -0,0 +1,68 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { database } from '~/common/database'; +import * as models from '~/models'; +import type { UnitTest } from '~/models/unit-test'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { testId } = params; + const data = (await request.json()) as Partial; + + const unitTest = await database.getWhere(models.unitTest.type, { + _id: testId, + }); + invariant(unitTest, 'Test not found'); + + await models.unitTest.update(unitTest, data); + + return null; +} + +export function useTestUpdateActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + data, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + testId: string; + data: Partial; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test/:testId/update', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + testId, + }, + ); + + return fetcherSubmit(JSON.stringify(data), { + action: url, + method: 'POST', + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new.tsx new file mode 100644 index 0000000000..a52cc8ee8b --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new.tsx @@ -0,0 +1,72 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import * as models from '~/models'; +import { SegmentEvent } from '~/ui/analytics'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new'; + +export async function clientAction({ request, params }: Route.ClientActionArgs) { + const { testSuiteId } = params; + + const formData = await request.formData(); + + const name = formData.get('name'); + invariant(typeof name === 'string', 'Name is required'); + + await models.unitTest.create({ + parentId: testSuiteId, + code: `const response1 = await insomnia.send(); +expect(response1.status).to.equal(200);`, + name, + }); + + window.main.trackSegmentEvent({ event: SegmentEvent.unitTestCreate }); + + return null; +} + +export function useTestNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ + organizationId, + projectId, + workspaceId, + testSuiteId, + name, + }: { + organizationId: string; + projectId: string; + workspaceId: string; + testSuiteId: string; + name: string; + }) => { + const url = href( + '/organization/:organizationId/project/:projectId/workspace/:workspaceId/test/test-suite/:testSuiteId/test/new', + { + organizationId, + projectId, + workspaceId, + testSuiteId, + }, + ); + + const formData = new FormData(); + formData.append('name', name); + + return fetcherSubmit(formData, { + action: url, + method: 'POST', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.tsx similarity index 74% rename from packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.tsx index 9e76092ad8..2ec0497a9d 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.$projectId.workspace.$workspaceId.test-suite.$testSuiteId.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.tsx @@ -12,23 +12,37 @@ import { SelectValue, useDragAndDrop, } from 'react-aria-components'; -import { type LoaderFunction, redirect, useFetcher, useParams, useRouteLoaderData } from 'react-router'; +import { useParams, useRouteLoaderData } from 'react-router'; -import { database } from '../../common/database'; -import { documentationLinks } from '../../common/documentation'; -import * as models from '../../models'; -import { isGrpcRequest } from '../../models/grpc-request'; -import { isRequest, type Request } from '../../models/request'; -import type { UnitTest } from '../../models/unit-test'; -import type { UnitTestSuite } from '../../models/unit-test-suite'; -import { isWebSocketRequest } from '../../models/websocket-request'; -import { invariant } from '../../utils/invariant'; -import { CodeEditor, type CodeEditorHandle } from '../components/codemirror/code-editor'; -import { EditableInput } from '../components/editable-input'; -import { Icon } from '../components/icon'; -import { showModal } from '../components/modals'; -import { AskModal } from '../components/modals/ask-modal'; -import { getMethodShortHand } from '../components/tags/method-tag'; +import { database } from '~/common/database'; +import { documentationLinks } from '~/common/documentation'; +import * as models from '~/models'; +import { isGrpcRequest } from '~/models/grpc-request'; +import { isRequest, type Request } from '~/models/request'; +import type { UnitTest } from '~/models/unit-test'; +import type { UnitTestSuite } from '~/models/unit-test-suite'; +import { isWebSocketRequest } from '~/models/websocket-request'; +import { useRunAllTestsActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.run-all-tests'; +import { useTestDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.delete'; +import { useTestRunActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.run'; +import { useTestUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.$testId.update'; +import { useTestNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.test.new'; +import { useTestSuiteUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId.update'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; +import { EditableInput } from '~/ui/components/editable-input'; +import { Icon } from '~/ui/components/icon'; +import { showModal } from '~/ui/components/modals'; +import { AskModal } from '~/ui/components/modals/ask-modal'; +import { getMethodShortHand } from '~/ui/components/tags/method-tag'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId'; + +export function useUnitTestSuiteLoaderData() { + return useRouteLoaderData( + 'routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.test.test-suite.$testSuiteId', + ); +} const UnitTestItemView = ({ unitTest }: { unitTest: UnitTest; testsRunning: boolean }) => { const editorRef = useRef(null); @@ -37,11 +51,11 @@ const UnitTestItemView = ({ unitTest }: { unitTest: UnitTest; testsRunning: bool projectId: string; organizationId: string; }; - const { unitTestSuite, requests } = useRouteLoaderData(':testSuiteId') as LoaderData; + const { unitTestSuite, requests } = useUnitTestSuiteLoaderData()!; - const deleteUnitTestFetcher = useFetcher(); - const runTestFetcher = useFetcher(); - const updateUnitTestFetcher = useFetcher(); + const deleteUnitTestFetcher = useTestDeleteActionFetcher(); + const runTestFetcher = useTestRunActionFetcher(); + const updateUnitTestFetcher = useTestUpdateActionFetcher(); const lintOptions = { globals: { @@ -76,16 +90,16 @@ const UnitTestItemView = ({ unitTest }: { unitTest: UnitTest; testsRunning: bool className="w-full px-1" onSubmit={name => { if (name) { - updateUnitTestFetcher.submit( - { + updateUnitTestFetcher.submit({ + organizationId, + projectId, + workspaceId, + testSuiteId: unitTestSuite._id, + testId: unitTest._id, + data: { name, }, - { - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/test/test-suite/${unitTestSuite._id}/test/${unitTest._id}/update`, - method: 'POST', - encType: 'application/json', - }, - ); + }); } }} value={unitTest.name} @@ -94,17 +108,18 @@ const UnitTestItemView = ({ unitTest }: { unitTest: UnitTest; testsRunning: bool { + navigate(`/organization/${id}`); + }} + selectedKey={organizationId} + > + + + + {item => ( + + {({ isSelected }) => ( + + {item.display_name} + {isSelected && } + + )} + + )} + + + +
+
+ Projects ({projectsCount}) +
+ + +
+ +
+
+ +
+ + { + if (keys !== 'all') { + const [value] = keys.values(); + + navigate({ + pathname: `/organization/${organizationId}/project/${value}`, + }); + } + }} + > + {item => { + return ( + +
+ + + {item.name} + + {item.presence.length > 0 && ( + + )} + {item._id !== SCRATCHPAD_PROJECT_ID && ( + + )} +
+
+ ); + }} +
+
+ {activeProject && ( + <> + { + if (keys !== 'all') { + const [value] = keys.values(); + + setWorkspaceListScope(value.toString()); + } + }} + > + {item => { + return ( + +
+ + + + + {item.label} + + {item.action && ( + + )} +
+
+ ); + }} +
+ {isGitProject(activeProject) && ( + + )} + {isLocalProject(activeProject) && !isGitProject(activeProject) && } + {isRemoteProject(activeProject) && } + + )} + {!isLearningFeatureDismissed && learningFeature?.active && ( +
+
+ + + {learningFeature.title} + + +
+

{learningFeature.message}

+ + {learningFeature.cta} + + +
+ )} +
+ + + + + {activeProject ? ( +
+ {billing.isActive ? null : ( +
+
+

+ + {isUserOwner + ? `Your ${isPersonalOrg ? 'personal account' : 'organization'} has unpaid past invoices. Please enter a new payment method to continue using Insomnia.` + : 'This organization has unpaid past invoices. Please ask the organization owner to enter a new payment method to continue using Insomnia.'} +

+ {isUserOwner && ( + + Update payment method + + )} +
+
+ )} + {billing?.expirationErrorMessage || billing?.expirationWarningMessage ? ( +
+
+

+ + {billing?.expirationErrorMessage || billing?.expirationWarningMessage} +

+ {isUserOwner && ( + + Contact sales + + )} +
+
+ ) : null} + {isProjectInconsistent && ( +
+
+

+ + The organization owner mandates that projects must be created and stored using{' '} + {getProjectStorageTypeLabel(storageRules)}. +

+ +
+
+ )} + {/* Show filter UI if there are files with presence or if the user has entered any filter input(even no match) */} + {(filesWithPresence.length > 0 || workspaceListFilter) && ( +
+ setWorkspaceListFilter(filter)} + > + +
+ +
+
+ + + + + + { + const item = createInProjectActionList.find(item => item.id === key); + if (item) { + item.action(); + } + }} + items={createInProjectActionList} + className="min-w-max select-none overflow-y-auto rounded-md border border-solid border-[--hl-sm] bg-[--color-bg] py-2 text-sm shadow-lg focus:outline-none" + > + {item => ( + + + {item.name} + + )} + + + + + +
+ )} + +
+ { + if (workspaceListFilter) { + return ( +
+

+ No documents found for {workspaceListFilter} +

+
+ ); + } + + return ( +
+ setImportModalType('file')} + /> + {createNewWorkspaceFetcher.data?.error && ( +
+
+ + {createNewWorkspaceFetcher.data?.error} +
+
+ )} +
+ ); + }} + > + {item => { + return ( + +
+
+
+ +
+ {item.label} +
+ + {item.presence.length > 0 && ( + + )} + {activeProject && item.scope !== 'unsynced' && item.workspace && ( + + )} +
+ + + {item.name} + + + {item.name} + + +
+ {item.gitFilePath && ( +
+ + + {item.gitFilePath} + +
+ )} + {item.version &&
{item.version}
} + {item.oasFormat && ( +
+ + {item.oasFormat} +
+ )} + {item.branch && ( +
+ + {item.branch} +
+ )} + {Boolean(item.lastModifiedTimestamp) && ( +
+ + + `Last updated ${text}, and created on ${new Date(item.created).toLocaleDateString()}` + } + timestamp={item.lastModifiedTimestamp} + /> + {item.lastCommit} +
+ )} + {(item.hasUncommittedChanges || item.hasUnpushedChanges) && ( +
+ {item.hasUncommittedChanges ? 'Uncommitted changes' : 'Unpushed changes'} +
+ )} +
+
+ ); + }} +
+
+
+ ) : ( + + )} +
+ + + {isGitRepositoryCloneModalOpen && ( + setIsGitRepositoryCloneModalOpen(false)} /> + )} + {isNewProjectModalOpen && ( + + )} + {isUpdateProjectModalOpen && ( + + )} + {activeProject && newWorkspaceModalState?.isOpen && ( + { + setNewWorkspaceModalState({ + scope: newWorkspaceModalState.scope, + isOpen, + }); + }} + /> + )} + {activeProject && importModalType && ( + setImportModalType(null)} + projectName={activeProject.name} + from={{ type: importModalType }} + organizationId={organizationId} + defaultProjectId={activeProject._id} + /> + )} + + + ); +}; + +export default Component; diff --git a/packages/insomnia/src/ui/routes/$organizationId.project.new.tsx b/packages/insomnia/src/routes/organization.$organizationId.project.new.tsx similarity index 57% rename from packages/insomnia/src/ui/routes/$organizationId.project.new.tsx rename to packages/insomnia/src/routes/organization.$organizationId.project.new.tsx index f930b94b32..71d52ed013 100644 --- a/packages/insomnia/src/ui/routes/$organizationId.project.new.tsx +++ b/packages/insomnia/src/routes/organization.$organizationId.project.new.tsx @@ -1,30 +1,31 @@ -import { type ActionFunctionArgs, redirect } from 'react-router'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; -import * as models from '../../models'; -import type { OauthProviderName } from '../../models/git-repository'; -import { invariant } from '../../utils/invariant'; -import { SegmentEvent } from '../analytics'; -import { insomniaFetch } from '../insomniaFetch'; +import * as models from '~/models'; +import type { GitCredentials, OauthProviderName } from '~/models/git-repository'; +import { SegmentEvent } from '~/ui/analytics'; +import { insomniaFetch } from '~/ui/insomniaFetch'; +import { invariant } from '~/utils/invariant'; + +import type { Route } from './+types/organization.$organizationId.project.new'; export interface CreateProjectActionResult { id?: string; error?: string; } -type CreateProjectData = - | { name: string; storageType: 'local' | 'remote' } - | { - name: string; - storageType: 'git'; - authorName: string; - authorEmail: string; - uri: string; - username: string; - password: string; - token: string; - oauth2format?: OauthProviderName; - connectRepositoryLater: boolean; - }; +export interface CreateProjectData { + name: string; + storageType: 'local' | 'remote' | 'git'; + authorName?: string; + authorEmail?: string; + uri?: string; + username?: string; + password?: string; + token?: string; + oauth2format?: OauthProviderName; + connectRepositoryLater?: boolean; +} export const createProject = async (organizationId: string, newProjectData: CreateProjectData) => { const createProjectImpl = async (organizationId: string, newProjectData: CreateProjectData) => { @@ -52,9 +53,36 @@ export const createProject = async (organizationId: string, newProjectData: Crea return project._id; } + let credentials: GitCredentials | undefined = undefined; + if (newProjectData.oauth2format === 'custom') { + credentials = { + username: newProjectData.username || '', + password: newProjectData.token || '', + }; + } else if (newProjectData.oauth2format) { + credentials = { + oauth2format: newProjectData.oauth2format, + token: newProjectData.token || '', + username: newProjectData.username || '', + }; + } else if (newProjectData.username && newProjectData.password) { + credentials = { + username: newProjectData.username, + password: newProjectData.password, + }; + } + const { projectId, errors } = await window.main.git.cloneGitRepo({ organizationId, - ...newProjectData, + uri: newProjectData.uri || '', + author: { + name: newProjectData.authorName || '', + email: newProjectData.authorEmail || '', + }, + credentials: credentials || { + username: '', + password: '', + }, }); if (errors) { @@ -120,7 +148,7 @@ export const createProject = async (organizationId: string, newProjectData: Crea return newProjectId; }; -export async function action({ request, params }: ActionFunctionArgs) { +export async function clientAction({ request, params }: Route.ClientActionArgs) { const { organizationId } = params; invariant(organizationId, 'Organization ID is required'); @@ -131,6 +159,7 @@ export async function action({ request, params }: ActionFunctionArgs) { return redirect(`/organization/${organizationId}/project/${newProjectId}`); } catch (err) { console.log(err); + return { error: err instanceof Error @@ -139,3 +168,25 @@ export async function action({ request, params }: ActionFunctionArgs) { }; } } + +export function useProjectNewActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ organizationId, projectData }: { organizationId: string; projectData: CreateProjectData }) => { + return fetcherSubmit(JSON.stringify(projectData), { + method: 'POST', + action: href('/organization/:organizationId/project/new', { + organizationId, + }), + encType: 'application/json', + }); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.storage-rules.tsx b/packages/insomnia/src/routes/organization.$organizationId.storage-rules.tsx new file mode 100644 index 0000000000..6efd1f0b2c --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.storage-rules.tsx @@ -0,0 +1,68 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import type { StorageRules } from '~/models/organization'; +import { fetchAndCacheOrganizationStorageRule } from '~/ui/organization-utils'; + +import type { Route } from './+types/organization.$organizationId.storage-rules'; + +export interface OrganizationStorageLoaderData { + storagePromise: Promise; +} + +export async function clientLoader({ params }: Route.ClientLoaderArgs) { + const { organizationId } = params as { organizationId: string }; + return { + storagePromise: fetchAndCacheOrganizationStorageRule(organizationId), + }; +} + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId } = params; + await fetchAndCacheOrganizationStorageRule(organizationId, true); + return null; +} + +export function useStorageRulesLoaderFetcher(args?: Parameters[0]) { + const { load: fetcherLoad, ...fetcherRest } = useFetcher(args); + + const load = useCallback( + ({ organizationId }: { organizationId: string }) => { + return fetcherLoad( + href('/organization/:organizationId/storage-rules', { + organizationId, + }), + ); + }, + [fetcherLoad], + ); + + return { + ...fetcherRest, + load, + }; +} + +export function useStorageRulesActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ organizationId }: { organizationId: string }) => { + return fetcherSubmit( + {}, + { + method: 'POST', + action: href('/organization/:organizationId/storage-rules', { + organizationId, + }), + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx b/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx new file mode 100644 index 0000000000..17928a966c --- /dev/null +++ b/packages/insomnia/src/routes/organization.$organizationId.sync-projects.tsx @@ -0,0 +1,38 @@ +import { useCallback } from 'react'; +import { href, useFetcher } from 'react-router'; + +import { syncProjects } from '~/ui/organization-utils'; + +import type { Route } from './+types/organization.$organizationId.sync-projects'; + +export async function clientAction({ params }: Route.ClientActionArgs) { + const { organizationId } = params; + + await syncProjects(organizationId); + + return null; +} + +export function useOrganizationSyncProjectsActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback( + ({ organizationId }: { organizationId: string }) => { + return fetcherSubmit( + {}, + { + method: 'POST', + action: href(`/organization/:organizationId/sync-projects`, { + organizationId, + }), + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/organization._index.tsx b/packages/insomnia/src/routes/organization._index.tsx similarity index 75% rename from packages/insomnia/src/ui/routes/organization._index.tsx rename to packages/insomnia/src/routes/organization._index.tsx index 84add42447..6732a0eae0 100644 --- a/packages/insomnia/src/ui/routes/organization._index.tsx +++ b/packages/insomnia/src/routes/organization._index.tsx @@ -1,12 +1,14 @@ -import { type LoaderFunctionArgs, redirect } from 'react-router'; +import { href, redirect } from 'react-router'; -import * as session from '../../account/session'; -import { userSession } from '../../models'; -import { findPersonalOrganization, type Organization } from '../../models/organization'; -import { invariant } from '../../utils/invariant'; -import { migrateProjectsUnderOrganization, syncOrganizations } from '../organization-utils'; +import * as session from '~/account/session'; +import { userSession } from '~/models'; +import { findPersonalOrganization, type Organization } from '~/models/organization'; +import { migrateProjectsUnderOrganization, syncOrganizations } from '~/ui/organization-utils'; +import { invariant } from '~/utils/invariant'; -export async function loader(_args: LoaderFunctionArgs) { +import type { Route } from './+types/organization._index'; + +export async function clientLoader(_args: Route.ClientLoaderArgs) { const { id: sessionId, accountId } = await userSession.getOrCreate(); if (sessionId) { await syncOrganizations(sessionId, accountId); @@ -38,5 +40,5 @@ export async function loader(_args: LoaderFunctionArgs) { } await session.logout(); - return redirect('/auth/login'); + return redirect(href('/auth/login')); } diff --git a/packages/insomnia/src/ui/routes/organization.sync-organizations-and-projects.tsx b/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx similarity index 53% rename from packages/insomnia/src/ui/routes/organization.sync-organizations-and-projects.tsx rename to packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx index 04dfbd0fef..52a90563f9 100644 --- a/packages/insomnia/src/ui/routes/organization.sync-organizations-and-projects.tsx +++ b/packages/insomnia/src/routes/organization.sync-organizations-and-projects.tsx @@ -1,13 +1,15 @@ -import { type ActionFunctionArgs, redirect } from 'react-router'; +import { useCallback } from 'react'; +import { href, redirect, useFetcher } from 'react-router'; -import { database } from '../../common/database'; -import { project, userSession } from '../../models'; -import { findPersonalOrganization, type Organization } from '../../models/organization'; -import type { Project } from '../../models/project'; -import { invariant } from '../../utils/invariant'; -import { AsyncTask } from '../../utils/router'; -import { migrateProjectsUnderOrganization, syncOrganizations } from '../organization-utils'; -import { syncProjects } from './$organizationId.project.$projectId'; +import { database } from '~/common/database'; +import { project, userSession } from '~/models'; +import { findPersonalOrganization, type Organization } from '~/models/organization'; +import type { Project } from '~/models/project'; +import { migrateProjectsUnderOrganization, syncOrganizations, syncProjects } from '~/ui/organization-utils'; +import { invariant } from '~/utils/invariant'; +import { AsyncTask } from '~/utils/router'; + +import type { Route } from './+types/organization.sync-organizations-and-projects'; interface SyncOrgsAndProjectsActionRequest { organizationId: string; @@ -16,7 +18,7 @@ interface SyncOrgsAndProjectsActionRequest { } // this action is used to run task that we dont want to block the UI -export async function action({ request }: ActionFunctionArgs) { +export async function clientAction({ request }: Route.ClientActionArgs) { try { const { organizationId, @@ -53,15 +55,56 @@ export async function action({ request }: ActionFunctionArgs) { if (!projectId && asyncTaskList.includes(AsyncTask.SyncProjects)) { const firstProject = await database.getWhere(project.type, { parentId: organizationId }); if (firstProject?._id) { - return redirect(`/organization/${organizationId}/project/${firstProject?._id}`); + return redirect( + href('/organization/:organizationId/project/:projectId', { + organizationId, + projectId: firstProject._id, + }), + ); } } return {}; } catch (error) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; console.log('Failed to run async task', error); return { - error: error.message, + error: errorMessage, }; } } + +export function useSyncOrganizationsAndProjectsActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcher } = useFetcher(args); + + const submit = useCallback( + function submit({ + organizationId, + projectId, + asyncTaskList, + }: { + organizationId: string; + projectId?: string; + asyncTaskList: AsyncTask[]; + }) { + return fetcherSubmit( + JSON.stringify({ + organizationId, + projectId, + asyncTaskList, + }), + { + method: 'POST', + action: '/organization/sync-organizations-and-projects', + encType: 'application/json', + }, + ); + }, + [fetcherSubmit], + ); + + return { + ...fetcher, + submit, + }; +} diff --git a/packages/insomnia/src/routes/organization.sync.tsx b/packages/insomnia/src/routes/organization.sync.tsx new file mode 100644 index 0000000000..37e1198e1a --- /dev/null +++ b/packages/insomnia/src/routes/organization.sync.tsx @@ -0,0 +1,36 @@ +import { useCallback } from 'react'; +import { useFetcher } from 'react-router'; + +import { userSession } from '~/models'; +import { syncOrganizations } from '~/ui/organization-utils'; + +import type { Route } from './+types/organization.sync'; + +export async function clientAction(_args: Route.ClientActionArgs) { + const { id: sessionId, accountId } = await userSession.getOrCreate(); + + if (sessionId) { + await syncOrganizations(sessionId, accountId); + } + + return null; +} + +export function useOrganizationSyncActionFetcher(args?: Parameters[0]) { + const { submit: fetcherSubmit, ...fetcherRest } = useFetcher(args); + + const submit = useCallback(() => { + return fetcherSubmit( + {}, + { + method: 'POST', + action: '/organization/sync', + }, + ); + }, [fetcherSubmit]); + + return { + ...fetcherRest, + submit, + }; +} diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/routes/organization.tsx similarity index 89% rename from packages/insomnia/src/ui/routes/organization.tsx rename to packages/insomnia/src/routes/organization.tsx index f43b208968..2e0ec8f718 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/routes/organization.tsx @@ -10,47 +10,41 @@ import { Tooltip, TooltipTrigger, } from 'react-aria-components'; -import { - type LoaderFunctionArgs, - NavLink, - Outlet, - useFetcher, - useLoaderData, - useLocation, - useNavigate, - useParams, - useRouteLoaderData, -} from 'react-router'; -import { useLocalStorage } from 'react-use'; +import { href, NavLink, Outlet, useLocation, useNavigate, useParams, useRouteLoaderData } from 'react-router'; +import * as reactUse from 'react-use'; -import { getAppWebsiteBaseURL } from '../../common/constants'; -import { userSession } from '../../models'; -import { isOwnerOfOrganization, isPersonalOrganization, type Organization } from '../../models/organization'; -import type { Settings } from '../../models/settings'; -import { isScratchpad } from '../../models/workspace'; -import { AsyncTask, getInitialRouteForOrganization } from '../../utils/router'; -import { getLoginUrl } from '../auth-session-provider'; -import { CommandPalette } from '../components/command-palette'; -import { GitHubStarsButton } from '../components/github-stars-button'; -import { HeaderInviteButton } from '../components/header-invite-button'; -import { HeaderUserButton } from '../components/header-user-button'; -import { Hotkey } from '../components/hotkey'; -import { Icon } from '../components/icon'; -import { InsomniaLogo } from '../components/insomnia-icon'; -import { showModal } from '../components/modals'; -import { AlertModal } from '../components/modals/alert-modal'; -import { SettingsModal, showSettingsModal } from '../components/modals/settings-modal'; -import { OrganizationAvatar } from '../components/organization-avatar'; -import { PresentUsers } from '../components/present-users'; -import { Toast } from '../components/toast'; -import { InsomniaEventStreamProvider } from '../context/app/insomnia-event-stream-context'; -import { InsomniaTabProvider } from '../context/app/insomnia-tab-context'; -import { RunnerProvider } from '../context/app/runner-context'; -import { useOrganizationPermissions } from '../hooks/use-organization-features'; -import { type CurrentPlan, sortOrganizations, type UserProfileResponse } from '../organization-utils'; -import type { WorkspaceLoaderData } from './$organizationId.project.$projectId.workspace.$workspaceId'; -import { useRootLoaderData } from './root'; -import type { UntrackedProjectsLoaderData } from './untracked-projects'; +import { getAppWebsiteBaseURL } from '~/common/constants'; +import { userSession } from '~/models'; +import { isOwnerOfOrganization, isPersonalOrganization, type Organization } from '~/models/organization'; +import { type CurrentPlan, type UserProfileResponse } from '~/models/organization'; +import type { Settings } from '~/models/settings'; +import { isScratchpad } from '~/models/workspace'; +import { useRootLoaderData } from '~/root'; +import { useWorkspaceLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { useSyncOrganizationsAndProjectsActionFetcher } from '~/routes/organization.sync-organizations-and-projects'; +import { useUntrackedProjectsLoaderFetcher } from '~/routes/untracked-projects'; +import { getLoginUrl } from '~/ui/auth-session-provider.client'; +import { CommandPalette } from '~/ui/components/command-palette'; +import { GitHubStarsButton } from '~/ui/components/github-stars-button'; +import { HeaderInviteButton } from '~/ui/components/header-invite-button'; +import { HeaderUserButton } from '~/ui/components/header-user-button'; +import { Hotkey } from '~/ui/components/hotkey'; +import { Icon } from '~/ui/components/icon'; +import { InsomniaLogo } from '~/ui/components/insomnia-icon'; +import { showModal } from '~/ui/components/modals'; +import { AlertModal } from '~/ui/components/modals/alert-modal'; +import { SettingsModal, showSettingsModal } from '~/ui/components/modals/settings-modal'; +import { OrganizationAvatar } from '~/ui/components/organization-avatar'; +import { PresentUsers } from '~/ui/components/present-users'; +import { Toast } from '~/ui/components/toast'; +import { InsomniaEventStreamProvider } from '~/ui/context/app/insomnia-event-stream-context'; +import { InsomniaTabProvider } from '~/ui/context/app/insomnia-tab-context'; +import { RunnerProvider } from '~/ui/context/app/runner-context'; +import { useOrganizationPermissions } from '~/ui/hooks/use-organization-features'; +import { sortOrganizations } from '~/ui/organization-utils'; +import { AsyncTask, getInitialRouteForOrganization } from '~/utils/router'; + +import type { Route } from './+types/organization'; export interface OrganizationLoaderData { organizations: Organization[]; @@ -58,7 +52,7 @@ export interface OrganizationLoaderData { currentPlan?: CurrentPlan; } -export async function loader(_args: LoaderFunctionArgs) { +export async function clientLoader(_args: Route.ClientLoaderArgs) { const { id, accountId } = await userSession.getOrCreate(); if (id) { const organizations = JSON.parse(localStorage.getItem(`${accountId}:organizations`) || '[]') as Organization[]; @@ -103,7 +97,7 @@ export interface OrganizationFeatureLoaderData { } export const useOrganizationLoaderData = () => { - return useRouteLoaderData('/organization') as OrganizationLoaderData; + return useRouteLoaderData('routes/organization'); }; interface IndicatorProps { @@ -160,7 +154,7 @@ const NetworkAndSyncIndicator = ({ user, asyncTaskStatus, settings, sync }: Indi diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx index a030aef728..34c71a3fbc 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-dropdown.tsx @@ -3,7 +3,7 @@ import { exportGlobalEnvironmentToFile, exportMockServerToFile, } from 'insomnia/src/ui/components/settings/import-export'; -import React, { type FC, type ReactNode, useCallback, useEffect, useState } from 'react'; +import { type FC, type ReactNode, useCallback, useEffect, useState } from 'react'; import { Button, Collection, @@ -18,7 +18,10 @@ import { ModalOverlay, Popover, } from 'react-aria-components'; -import { useFetcher, useNavigate, useParams, useRouteLoaderData } from 'react-router'; +import { href, useNavigate, useParams } from 'react-router'; + +import { useWorkspaceDeleteActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.delete'; +import { useWorkspaceUpdateActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.update'; import { getProductName } from '../../../common/constants'; import { database as db } from '../../../common/database'; @@ -34,9 +37,9 @@ import * as pluginApp from '../../../plugins/context/app'; import * as pluginData from '../../../plugins/context/data'; import * as pluginNetwork from '../../../plugins/context/network'; import * as pluginStore from '../../../plugins/context/store'; +import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { invariant } from '../../../utils/invariant'; import { SegmentEvent } from '../../analytics'; -import type { WorkspaceLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; import { DropdownHint } from '../base/dropdown/dropdown-hint'; import { Icon } from '../icon'; import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; @@ -54,17 +57,16 @@ export const WorkspaceDropdown: FC<{}> = () => { workspaceId: string; }>(); invariant(organizationId, 'Expected organizationId'); - const { activeWorkspace, activeWorkspaceMeta, activeProject, activeMockServer } = useRouteLoaderData( - ':workspaceId', - ) as WorkspaceLoaderData; + const { activeWorkspace, activeWorkspaceMeta, activeProject, activeMockServer } = useWorkspaceLoaderData()!; const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false); const [isImportModalOpen, setIsImportModalOpen] = useState(false); const [isExportModalOpen, setIsExportModalOpen] = useState(false); const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); - const fetcher = useFetcher(); + + const updateWorkspaceFetcher = useWorkspaceUpdateActionFetcher(); const [isDeleteRemoteWorkspaceModalOpen, setIsDeleteRemoteWorkspaceModalOpen] = useState(false); - const deleteWorkspaceFetcher = useFetcher(); + const deleteWorkspaceFetcher = useWorkspaceDeleteActionFetcher(); const [actionPlugins, setActionPlugins] = useState([]); const [loadingActions, setLoadingActions] = useState>({}); const navigate = useNavigate(); @@ -245,14 +247,11 @@ export const WorkspaceDropdown: FC<{}> = () => { selectText: true, label: 'Name', onComplete: name => - fetcher.submit( - { name, workspaceId: activeWorkspace._id }, - { - action: `/organization/${organizationId}/project/${activeWorkspace.parentId}/workspace/update`, - method: 'post', - encType: 'application/json', - }, - ), + updateWorkspaceFetcher.submit({ + organizationId, + projectId: activeWorkspace.parentId, + patch: { name, workspaceId: activeWorkspace._id }, + }), }), }, { @@ -413,7 +412,10 @@ export const WorkspaceDropdown: FC<{}> = () => { diff --git a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx index f034f61f0d..b57d105722 100644 --- a/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx +++ b/packages/insomnia/src/ui/components/dropdowns/workspace-sync-dropdown.tsx @@ -1,21 +1,19 @@ import React, { type FC } from 'react'; -import { useRouteLoaderData } from 'react-router'; + +import { useRootLoaderData } from '~/root'; import { isGitProject, isRemoteProject } from '../../../models/project'; +import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { useOrganizationPermissions } from '../../hooks/use-organization-features'; -import type { WorkspaceLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; -import { useRootLoaderData } from '../../routes/root'; import { GitProjectSyncDropdown } from './git-project-sync-dropdown'; import { GitSyncDropdown } from './git-sync-dropdown'; import { LocalProjectBar } from './local-project-bar'; import { SyncDropdown } from './sync-dropdown'; export const WorkspaceSyncDropdown: FC = () => { - const { activeProject, activeWorkspace, gitRepository, activeWorkspaceMeta } = useRouteLoaderData( - ':workspaceId', - ) as WorkspaceLoaderData; + const { activeProject, activeWorkspace, gitRepository, activeWorkspaceMeta } = useWorkspaceLoaderData()!; - const { userSession } = useRootLoaderData(); + const { userSession } = useRootLoaderData()!; const { features } = useOrganizationPermissions(); diff --git a/packages/insomnia/src/ui/components/editors/__tests__/environment-editor.test.ts b/packages/insomnia/src/ui/components/editors/__tests__/environment-editor.test.ts index 87eabec399..dd425a4f38 100644 --- a/packages/insomnia/src/ui/components/editors/__tests__/environment-editor.test.ts +++ b/packages/insomnia/src/ui/components/editors/__tests__/environment-editor.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { describe, expect, it } from 'vitest'; import { NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME } from '../../../../templating'; diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-accordion.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-accordion.tsx index db80dc8592..57273d5a56 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-accordion.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-accordion.tsx @@ -1,10 +1,12 @@ import classnames from 'classnames'; import React, { type FC, type PropsWithChildren } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import type { RequestAccordionKeys } from '../../../../../models/request-meta'; -import { useRequestMetaPatcher } from '../../../../hooks/use-request'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import type { RequestAccordionKeys } from '~/models/request-meta'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { useRequestMetaPatcher } from '~/ui/hooks/use-request'; interface Props { label: string; @@ -12,7 +14,7 @@ interface Props { } export const AuthAccordion: FC> = ({ accordionKey, label, children }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; const expanded = !reqData || Boolean(reqData.activeRequestMeta?.expandedAccordionKeys[accordionKey]); const patchRequestMeta = useRequestMetaPatcher(); const toggle = () => { diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-input-row.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-input-row.tsx index 5f6ca3eb6d..b05645d425 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-input-row.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-input-row.tsx @@ -1,13 +1,19 @@ import React, { type ComponentProps, type FC, type ReactNode, useCallback, useEffect, useRef } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import { useToggle } from 'react-use'; +import * as reactUse from 'react-use'; + +import { useRootLoaderData } from '~/root'; +import { OneLineEditor, type OneLineEditorHandle } from '~/ui/components/.client/codemirror/one-line-editor'; import { toKebabCase } from '../../../../../common/misc'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '../../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '../../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; import { useRequestGroupPatcher, useRequestPatcher } from '../../../../hooks/use-request'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; -import { useRootLoaderData } from '../../../../routes/root'; -import { OneLineEditor, type OneLineEditorHandle } from '../../../codemirror/one-line-editor'; import { AuthRow } from './auth-row'; interface Props extends Pick, 'getAutocompleteConstants'> { @@ -30,15 +36,15 @@ export const AuthInputRow: FC = ({ overrideValueWhenDisabled, copyBtn = false, }) => { - const { settings } = useRootLoaderData(); + const { settings } = useRootLoaderData()!; const { showPasswords } = settings; - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const patchRequest = useRequestPatcher(); const patchRequestGroup = useRequestGroupPatcher(); - const { authentication, _id } = reqData?.activeRequest || groupData.activeRequestGroup; + const { authentication, _id } = reqData?.activeRequest || groupData?.activeRequestGroup || {}; const patcher = reqData ? patchRequest : patchRequestGroup; - const [masked, toggleMask] = useToggle(true); + const [masked, toggleMask] = reactUse.useToggle(true); const canBeMasked = !showPasswords && mask; const isMasked = canBeMasked && masked; diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-private-key-row.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-private-key-row.tsx index afbc36e45e..dca68c311d 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-private-key-row.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-private-key-row.tsx @@ -1,14 +1,20 @@ import React, { type FC, type ReactNode, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import { toKebabCase } from '../../../../../common/misc'; -import { invariant } from '../../../../../utils/invariant'; -import { useNunjucks } from '../../../../context/nunjucks/use-nunjucks'; -import { useRequestGroupPatcher, useRequestPatcher } from '../../../../hooks/use-request'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; -import { showModal } from '../../../modals'; -import { CodePromptModal } from '../../../modals/code-prompt-modal'; +import { toKebabCase } from '~/common/misc'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { showModal } from '~/ui/components/modals'; +import { CodePromptModal } from '~/ui/components/modals/code-prompt-modal'; +import { useNunjucks } from '~/ui/context/nunjucks/use-nunjucks'; +import { useRequestGroupPatcher, useRequestPatcher } from '~/ui/hooks/use-request'; +import { invariant } from '~/utils/invariant'; + import { AuthRow } from './auth-row'; const PRIVATE_KEY_PLACEHOLDER = ` @@ -31,8 +37,8 @@ interface Props { } export const AuthPrivateKeyRow: FC = ({ label, property, help }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const patchRequest = useRequestPatcher(); const patchRequestGroup = useRequestGroupPatcher(); const patcher = reqData ? patchRequest : patchRequestGroup; diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-row.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-row.tsx index 8a13339a20..67250a7555 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-row.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-row.tsx @@ -1,10 +1,15 @@ import classnames from 'classnames'; import React, { type FC, type PropsWithChildren, type ReactNode } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; -import { HelpTooltip } from '../../../help-tooltip'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { HelpTooltip } from '~/ui/components/help-tooltip'; interface Props { labelFor: string; @@ -14,9 +19,9 @@ interface Props { } export const AuthRow: FC> = ({ labelFor, label, help, disabled, children }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; - const { authentication } = reqData?.activeRequest || groupData.activeRequestGroup; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; + const { authentication } = reqData?.activeRequest || groupData.activeRequestGroup || {}; const isDisabled = (authentication && 'disabled' in authentication && authentication.disabled) || disabled; return ( diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-select-row.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-select-row.tsx index 162e65fa7e..7b3c8bdff7 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-select-row.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-select-row.tsx @@ -1,12 +1,18 @@ import React, { type ChangeEvent, type FC, type ReactNode, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import { toKebabCase } from '../../../../../common/misc'; -import type { RequestAuthentication } from '../../../../../models/request'; -import { getAuthObjectOrNull } from '../../../../../network/authentication'; -import { useRequestGroupPatcher, useRequestPatcher } from '../../../../hooks/use-request'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { toKebabCase } from '~/common/misc'; +import type { RequestAuthentication } from '~/models/request'; +import { getAuthObjectOrNull } from '~/network/authentication'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { useRequestGroupPatcher, useRequestPatcher } from '~/ui/hooks/use-request'; + import { AuthRow } from './auth-row'; interface Props { @@ -21,8 +27,8 @@ interface Props { } export const AuthSelectRow: FC = ({ label, property, help, options, disabled }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const patchRequest = useRequestPatcher(); const patchRequestGroup = useRequestGroupPatcher(); const patcher = reqData ? patchRequest : patchRequestGroup; diff --git a/packages/insomnia/src/ui/components/editors/auth/components/auth-toggle-row.tsx b/packages/insomnia/src/ui/components/editors/auth/components/auth-toggle-row.tsx index 8b133b804c..18f7c43f52 100644 --- a/packages/insomnia/src/ui/components/editors/auth/components/auth-toggle-row.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/components/auth-toggle-row.tsx @@ -1,10 +1,16 @@ import React, { type FC, type ReactNode, useCallback } from 'react'; -import { useRouteLoaderData } from 'react-router'; -import { toKebabCase } from '../../../../../common/misc'; -import { useRequestGroupPatcher, useRequestPatcher } from '../../../../hooks/use-request'; -import type { RequestLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { toKebabCase } from '~/common/misc'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { useRequestGroupPatcher, useRequestPatcher } from '~/ui/hooks/use-request'; + import { AuthRow } from './auth-row'; interface Props { @@ -33,10 +39,10 @@ export const AuthToggleRow: FC = ({ offTitle = 'Enable item', disabled = false, }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const patchRequestGroup = useRequestGroupPatcher(); - const { authentication, _id } = reqData?.activeRequest || groupData.activeRequestGroup; + const { authentication, _id } = reqData?.activeRequest || groupData?.activeRequestGroup || {}; const patchRequest = useRequestPatcher(); const patcher = reqData ? patchRequest : patchRequestGroup; diff --git a/packages/insomnia/src/ui/components/editors/auth/o-auth-1-auth.tsx b/packages/insomnia/src/ui/components/editors/auth/o-auth-1-auth.tsx index c846a6b3aa..9d635d24ac 100644 --- a/packages/insomnia/src/ui/components/editors/auth/o-auth-1-auth.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/o-auth-1-auth.tsx @@ -1,5 +1,4 @@ import React, { type FC } from 'react'; -import { useRouteLoaderData } from 'react-router'; import type { AuthTypeOAuth1 } from '../../../../models/request'; import { @@ -9,8 +8,14 @@ import { SIGNATURE_METHOD_PLAINTEXT, SIGNATURE_METHOD_RSA_SHA1, } from '../../../../network/o-auth-1/constants'; -import type { RequestLoaderData } from '../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; import { AuthInputRow } from './components/auth-input-row'; import { AuthPrivateKeyRow } from './components/auth-private-key-row'; import { AuthSelectRow } from './components/auth-select-row'; @@ -38,8 +43,8 @@ export const signatureMethodOptions: { name: OAuth1SignatureMethod; value: OAuth ]; export const OAuth1Auth: FC = () => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const authentication = (reqData?.activeRequest || groupData.activeRequestGroup).authentication as AuthTypeOAuth1; const { signatureMethod } = authentication; diff --git a/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx b/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx index 246ca901e2..befadbeb5e 100644 --- a/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx +++ b/packages/insomnia/src/ui/components/editors/auth/o-auth-2-auth.tsx @@ -1,5 +1,4 @@ import React, { type ChangeEvent, type FC, type ReactNode, useEffect, useMemo, useState } from 'react'; -import { useRouteLoaderData } from 'react-router'; import { type AUTH_OAUTH_2, getOauthRedirectUrl } from '../../../../common/constants'; import { toKebabCase } from '../../../../common/misc'; @@ -18,9 +17,15 @@ import { } from '../../../../network/o-auth-2/constants'; import { getOAuth2Token } from '../../../../network/o-auth-2/get-token'; import { initNewOAuthSession } from '../../../../network/o-auth-2/get-token'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { + type RequestGroupLoaderData, + useRequestGroupLoaderData, +} from '../../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; import { useNunjucks } from '../../../context/nunjucks/use-nunjucks'; -import type { RequestLoaderData } from '../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import type { RequestGroupLoaderData } from '../../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request-group.$requestGroupId'; import { Link } from '../../base/link'; import { showModal } from '../../modals'; import { ResponseDebugModal } from '../../modals/response-debug-modal'; @@ -274,8 +279,8 @@ const getFieldsForGrantType = (authentication: Extract { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const { authentication } = reqData?.activeRequest || groupData.activeRequestGroup; const { basic, advanced } = getFieldsForGrantType(authentication as AuthTypeOAuth2); @@ -373,8 +378,8 @@ const OAuth2TokenInput: FC<{ label: string; property: keyof Pick; }> = ({ token, label, property }) => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const { _id } = reqData?.activeRequest || groupData.activeRequestGroup; const onChange = async ({ currentTarget: { value } }: ChangeEvent) => { if (token) { @@ -451,8 +456,8 @@ const OAuth2Error: FC<{ token: OAuth2Token | null }> = ({ token }) => { }; const OAuth2Tokens: FC = () => { - const reqData = useRouteLoaderData('request/:requestId') as RequestLoaderData; - const groupData = useRouteLoaderData('request-group/:requestGroupId') as RequestGroupLoaderData; + const reqData = useRequestLoaderData() as RequestLoaderData; + const groupData = useRequestGroupLoaderData() as RequestGroupLoaderData; const { authentication, _id } = reqData?.activeRequest || groupData.activeRequestGroup; const [token, setToken] = useState(null); useEffect(() => { diff --git a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx index 80e2018782..1a2dc6b4ad 100644 --- a/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/graph-ql-editor.tsx @@ -22,7 +22,9 @@ import React, { type FC, useCallback, useEffect, useRef, useState } from 'react' import { Button, Group, Heading, Toolbar, Tooltip, TooltipTrigger } from 'react-aria-components'; import ReactDOM from 'react-dom'; import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; -import { useLocalStorage } from 'react-use'; +import * as reactUse from 'react-use'; + +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; import { CONTENT_TYPE_JSON } from '../../../../common/constants'; import { database as db } from '../../../../common/database'; @@ -40,7 +42,6 @@ import { import { invariant } from '../../../../utils/invariant'; import { jsonPrettify } from '../../../../utils/prettify/json'; import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../../base/dropdown'; -import { CodeEditor, type CodeEditorHandle } from '../../codemirror/code-editor'; import { GraphQLExplorer } from '../../graph-ql-explorer/graph-ql-explorer'; import type { ActiveReference } from '../../graph-ql-explorer/graph-ql-types'; import { HelpTooltip } from '../../help-tooltip'; @@ -49,6 +50,8 @@ import { useDocBodyKeyboardShortcuts } from '../../keydown-binder'; import { TimeFromNow } from '../../time-from-now'; import { prettifyGraphql } from './prettify-graphql.mjs'; +const { useLocalStorage } = reactUse; + // Type guard to ensure loc is non-nullable const hasLocation = ( def: OperationDefinitionNode, diff --git a/packages/insomnia/src/ui/components/editors/body/raw-editor.tsx b/packages/insomnia/src/ui/components/editors/body/raw-editor.tsx index f386e9bc13..85eac5add7 100644 --- a/packages/insomnia/src/ui/components/editors/body/raw-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/body/raw-editor.tsx @@ -1,6 +1,6 @@ import React, { type FC, Fragment } from 'react'; -import { CodeEditor } from '../../codemirror/code-editor'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; interface Props { onChange: (value: string) => void; diff --git a/packages/insomnia/src/ui/components/editors/environment-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-editor.tsx index 6aa0512ade..d9f3e648fc 100644 --- a/packages/insomnia/src/ui/components/editors/environment-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-editor.tsx @@ -1,8 +1,9 @@ import orderedJSON from 'json-order'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; + import { JSON_ORDER_PREFIX, JSON_ORDER_SEPARATOR } from '../../../common/constants'; -import { CodeEditor, type CodeEditorHandle } from '../codemirror/code-editor'; import { checkNestedKeys } from './environment-utils'; export interface EnvironmentInfo { diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx index 78207f01f6..78a41f7c0b 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/key-value-editor.tsx @@ -13,6 +13,8 @@ import { useDragAndDrop, } from 'react-aria-components'; +import { OneLineEditor } from '~/ui/components/.client/codemirror/one-line-editor'; + import { generateId } from '../../../../common/misc'; import { decryptSecretValue, @@ -22,7 +24,6 @@ import { } from '../../../../models/environment'; import { base64decode } from '../../../../utils/vault'; import { PromptButton } from '../../base/prompt-button'; -import { OneLineEditor } from '../../codemirror/one-line-editor'; import { Icon } from '../../icon'; import { showModal } from '../../modals'; import { AskModal } from '../../modals/ask-modal'; diff --git a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/password-input.tsx b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/password-input.tsx index 8263b94602..a2360f4094 100644 --- a/packages/insomnia/src/ui/components/editors/environment-key-value-editor/password-input.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-key-value-editor/password-input.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import { OneLineEditor } from '../../codemirror/one-line-editor'; +import { OneLineEditor } from '~/ui/components/.client/codemirror/one-line-editor'; export interface PasswordInputProps { value: string; diff --git a/packages/insomnia/src/ui/components/editors/environment-utils.tsx b/packages/insomnia/src/ui/components/editors/environment-utils.tsx index b8b70f05b2..d0f7ad28df 100644 --- a/packages/insomnia/src/ui/components/editors/environment-utils.tsx +++ b/packages/insomnia/src/ui/components/editors/environment-utils.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { type Environment, type EnvironmentKvPairData, diff --git a/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx b/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx index 3881097ca4..6a0b073623 100644 --- a/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx +++ b/packages/insomnia/src/ui/components/editors/mock-response-extractor.tsx @@ -3,18 +3,19 @@ import fs from 'node:fs/promises'; import React, { useState } from 'react'; import { Button } from 'react-aria-components'; import { useNavigate, useParams } from 'react-router'; -import { useFetcher, useRouteLoaderData } from 'react-router'; -import { getContentTypeName, getMimeTypeFromContentType } from '../../../common/constants'; -import type { ResponseHeader } from '../../../models/response'; -import { invariant } from '../../../utils/invariant'; -import type { WorkspaceLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; -import type { RequestLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { useOrganizationLoaderData } from '~/routes/organization'; +import { useRequestLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; import { isInMockContentTypeList, useMockRoutePatcher, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; -import type { OrganizationLoaderData } from '../../routes/organization'; +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; +import { useMockRouteNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.new'; + +import { getContentTypeName, getMimeTypeFromContentType } from '../../../common/constants'; +import type { ResponseHeader } from '../../../models/response'; +import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; +import { invariant } from '../../../utils/invariant'; import { HelpTooltip } from '../help-tooltip'; import { Icon } from '../icon'; import { showModal } from '../modals'; @@ -22,15 +23,13 @@ import { AlertModal } from '../modals/alert-modal'; import { PromptModal } from '../modals/prompt-modal'; export const MockResponseExtractor = () => { - // file://./../../routes/request.tsx#loader - const requestLoaderData = useRouteLoaderData('request/:requestId') as RequestLoaderData; + const requestLoaderData = useRequestLoaderData()!; const { activeResponse } = requestLoaderData; - let { mockServerAndRoutes } = requestLoaderData; + let mockServerAndRoutes = 'mockServerAndRoutes' in requestLoaderData ? requestLoaderData.mockServerAndRoutes : []; - // file://./../../routes/$organizationId.project.$projectId.workspace.tsx#workspaceLoader - const { activeProject, activeWorkspace } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const { activeProject, activeWorkspace } = useWorkspaceLoaderData()!; const isLocalProject = !activeProject?.remoteId; - const { currentPlan } = useRouteLoaderData('/organization') as OrganizationLoaderData; + const { currentPlan } = useOrganizationLoaderData()!; const isEnterprise = currentPlan?.type.includes('enterprise'); // In a local project, users are not allowed to create a cloud mock server, only enterprise users can create a self-hosted mock server. @@ -59,8 +58,12 @@ If you want to create a self-hosted mock server route from a request response in const patchMockRoute = useMockRoutePatcher(); const navigate = useNavigate(); - const { organizationId, projectId, workspaceId } = useParams(); - const fetcher = useFetcher(); + const { organizationId, projectId, workspaceId } = useParams() as { + organizationId: string; + projectId: string; + workspaceId: string; + }; + const createMockRouteFetcher = useMockRouteNewActionFetcher(); const [selectedMockServer, setSelectedMockServer] = useState( canOnlyChooseExistingMockServer ? mockServerAndRoutes[0]._id : '', ); @@ -91,7 +94,7 @@ If you want to create a self-hosted mock server route from a request response in onSubmit={async e => { e.preventDefault(); if (selectedMockServer && selectedMockRoute) { - if (activeResponse) { + if (activeResponse && 'bodyPath' in activeResponse) { // TODO: move this out of the renderer, and upsert mock const body = await fs.readFile(activeResponse.bodyPath); @@ -118,27 +121,25 @@ If you want to create a self-hosted mock server route from a request response in label: 'Name', onComplete: async name => { invariant(activeResponse, 'Active response must be defined'); - const body = await fs.readFile(activeResponse.bodyPath); + const body = 'bodyPath' in activeResponse ? await fs.readFile(activeResponse.bodyPath) : ''; // auth mechanism is too sensitive to allow content length checks const headersWithoutContentLength: ResponseHeader[] = activeResponse.headers.filter( h => h.name.toLowerCase() !== 'content-length', ); - // file://./../../routes/actions.tsx#createMockRouteAction - fetcher.submit( - JSON.stringify({ + + createMockRouteFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { name: name, body: body.toString(), mimeType, statusCode: activeResponse.statusCode, headers: headersWithoutContentLength, mockServerName: activeWorkspace.name, - }), - { - encType: 'application/json', - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`, }, - ); + }); }, }); return; @@ -151,7 +152,7 @@ If you want to create a self-hosted mock server route from a request response in label: 'Name', onComplete: async name => { invariant(activeResponse, 'Active response must be defined'); - const body = await fs.readFile(activeResponse.bodyPath); + const body = 'bodyPath' in activeResponse ? await fs.readFile(activeResponse.bodyPath) : ''; const hasRouteInServer = mockServerAndRoutes .find(s => s._id === selectedMockServer) ?.routes.find(r => r.name === name && r.method.toUpperCase() === 'GET'); @@ -166,21 +167,19 @@ If you want to create a self-hosted mock server route from a request response in const headersWithoutContentLength: ResponseHeader[] = activeResponse.headers.filter( h => h.name.toLowerCase() !== 'content-length', ); - fetcher.submit( - JSON.stringify({ + createMockRouteFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch: { name: name, parentId: selectedMockServer, body: body.toString(), mimeType, statusCode: activeResponse.statusCode, headers: headersWithoutContentLength, - }), - { - encType: 'application/json', - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/mock-server/mock-route/new`, }, - ); + }); }, }); } diff --git a/packages/insomnia/src/ui/components/editors/mock-response-headers-editor.tsx b/packages/insomnia/src/ui/components/editors/mock-response-headers-editor.tsx index 6db58ac594..1ef59171c9 100644 --- a/packages/insomnia/src/ui/components/editors/mock-response-headers-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/mock-response-headers-editor.tsx @@ -1,13 +1,14 @@ import React, { type FC, useCallback } from 'react'; -import { useParams, useRouteLoaderData } from 'react-router'; +import { useParams } from 'react-router'; + +import { + useMockRouteLoaderData, + useMockRoutePatcher, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers'; import type { RequestHeader } from '../../../models/request'; -import { - type MockRouteLoaderData, - useMockRoutePatcher, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; -import { CodeEditor } from '../codemirror/code-editor'; import { KeyValueEditor } from '../key-value-editor/key-value-editor'; interface Props { @@ -17,7 +18,7 @@ interface Props { } export const MockResponseHeadersEditor: FC = ({ bulk, isDisabled, onBlur }) => { - const { mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData; + const { mockRoute } = useMockRouteLoaderData()!; const patchMockRoute = useMockRoutePatcher(); const { mockRouteId } = useParams() as { mockRouteId: string }; diff --git a/packages/insomnia/src/ui/components/editors/request-headers-editor.tsx b/packages/insomnia/src/ui/components/editors/request-headers-editor.tsx index 96eb1b3e83..bf08d84cd4 100644 --- a/packages/insomnia/src/ui/components/editors/request-headers-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-headers-editor.tsx @@ -1,12 +1,13 @@ import React, { type FC, useCallback } from 'react'; import { useParams } from 'react-router'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; + import { getCommonHeaderNames, getCommonHeaderValues } from '../../../common/common-headers'; import { generateId } from '../../../common/misc'; import type { RequestHeader } from '../../../models/request'; import { invariant } from '../../../utils/invariant'; import { useRequestGroupPatcher, useRequestPatcher } from '../../hooks/use-request'; -import { CodeEditor } from '../codemirror/code-editor'; import { KeyValueEditor } from '../key-value-editor/key-value-editor'; interface Props { diff --git a/packages/insomnia/src/ui/components/editors/request-parameters-editor.tsx b/packages/insomnia/src/ui/components/editors/request-parameters-editor.tsx index 53f237f813..c092afb9ae 100644 --- a/packages/insomnia/src/ui/components/editors/request-parameters-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-parameters-editor.tsx @@ -1,13 +1,14 @@ import React, { type FC, useCallback } from 'react'; -import { useParams, useRouteLoaderData } from 'react-router'; +import { useParams } from 'react-router'; + +import type { RequestParameter } from '~/models/request'; +import { + type RequestLoaderData, + useRequestLoaderData, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; +import { useRequestPatcher } from '~/ui/hooks/use-request'; -import type { RequestParameter } from '../../../models/request'; -import { useRequestPatcher } from '../../hooks/use-request'; -import type { - RequestLoaderData, - WebSocketRequestLoaderData, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.debug.request.$requestId'; -import { CodeEditor } from '../codemirror/code-editor'; import { KeyValueEditor } from '../key-value-editor/key-value-editor'; interface Props { @@ -17,7 +18,7 @@ interface Props { export const RequestParametersEditor: FC = ({ bulk, disabled = false }) => { const { requestId } = useParams() as { requestId: string }; - const { activeRequest } = useRouteLoaderData('request/:requestId') as RequestLoaderData | WebSocketRequestLoaderData; + const { activeRequest } = useRequestLoaderData() as RequestLoaderData; const patchRequest = useRequestPatcher(); const handleBulkUpdate = useCallback( (paramsString: string) => { diff --git a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx index 759c57d9dd..90dde0d9fe 100644 --- a/packages/insomnia/src/ui/components/editors/request-script-editor.tsx +++ b/packages/insomnia/src/ui/components/editors/request-script-editor.tsx @@ -12,6 +12,8 @@ import { Toolbar, } from 'react-aria-components'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; + import { CookieObject, Environment, @@ -27,7 +29,6 @@ import { import { ParentFolders } from '../../../../../insomnia-scripting-environment/src/objects/folders'; import type { Settings } from '../../../models/settings'; import { translateHandlersInScript } from '../../../utils/importers/importers/postman'; -import { CodeEditor, type CodeEditorHandle } from '../codemirror/code-editor'; import { Icon } from '../icon'; interface Props { diff --git a/packages/insomnia/src/ui/components/encoding-picker.tsx b/packages/insomnia/src/ui/components/encoding-picker.tsx index 41c684aa22..768e1f066e 100644 --- a/packages/insomnia/src/ui/components/encoding-picker.tsx +++ b/packages/insomnia/src/ui/components/encoding-picker.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Button, ComboBox, Group, Input, ListBox, ListBoxItem, Popover } from 'react-aria-components'; import { fuzzyMatch } from '../../common/misc'; diff --git a/packages/insomnia/src/ui/components/environment-picker.tsx b/packages/insomnia/src/ui/components/environment-picker.tsx index 550be6657c..1016e5c112 100644 --- a/packages/insomnia/src/ui/components/environment-picker.tsx +++ b/packages/insomnia/src/ui/components/environment-picker.tsx @@ -12,13 +12,16 @@ import { Popover, Text, } from 'react-aria-components'; -import { useFetcher, useNavigate, useParams, useRouteLoaderData } from 'react-router'; +import { useNavigate, useParams } from 'react-router'; + +import { useSetActiveEnvironmentFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active'; +import { useEnvironmentSetActiveGlobalActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.environment.set-active-global'; import { fuzzyMatch } from '../../common/misc'; import { isRemoteProject } from '../../models/project'; +import { useWorkspaceLoaderData } from '../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import uiEventBus from '../eventBus'; import { useOrganizationPermissions } from '../hooks/use-organization-features'; -import type { WorkspaceLoaderData } from '../routes/$organizationId.project.$projectId.workspace.$workspaceId'; import { Icon } from './icon'; export const EnvironmentPicker = ({ @@ -39,7 +42,7 @@ export const EnvironmentPicker = ({ baseEnvironment, globalBaseEnvironments, globalSubEnvironments, - } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + } = useWorkspaceLoaderData()!; const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; @@ -53,8 +56,8 @@ export const EnvironmentPicker = ({ const isUsingInsomniaCloudSync = Boolean(isRemoteProject(activeProject) && !activeWorkspaceMeta?.gitRepositoryId); const isUsingGitSync = Boolean(features.gitSync.enabled && activeWorkspaceMeta?.gitRepositoryId); - const setActiveEnvironmentFetcher = useFetcher(); - const setActiveGlobalEnvironmentFetcher = useFetcher(); + const setActiveEnvironmentFetcher = useSetActiveEnvironmentFetcher(); + const setActiveGlobalEnvironmentFetcher = useEnvironmentSetActiveGlobalActionFetcher(); const collectionEnvironmentList = [baseEnvironment, ...subEnvironments].map(({ type, ...environment }) => ({ ...environment, @@ -170,15 +173,12 @@ export const EnvironmentPicker = ({ return; } - setActiveGlobalEnvironmentFetcher.submit( - { - environmentId: key.toString(), - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active-global`, - }, - ); + setActiveGlobalEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + environmentId: key.toString(), + }); }} defaultInputValue={ selectedGlobalBaseEnvironment?.workspaceName || @@ -243,15 +243,12 @@ export const EnvironmentPicker = ({ } const [environmentId] = keys.values(); - setActiveGlobalEnvironmentFetcher.submit( - { - environmentId, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active-global`, - }, - ); + setActiveGlobalEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + environmentId: environmentId.toString(), + }); }} className="flex max-h-[fit-content] min-w-max flex-1 select-none flex-col overflow-y-auto p-2 text-sm empty:p-0 focus:outline-none" > @@ -315,15 +312,12 @@ export const EnvironmentPicker = ({ return; } const [environmentId] = keys.values(); - setActiveEnvironmentFetcher.submit( - { - environmentId, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/environment/set-active`, - }, - ); + setActiveEnvironmentFetcher.submit({ + organizationId, + projectId, + workspaceId, + environmentId: environmentId.toString(), + }); uiEventBus.emit('CHANGE_ACTIVE_ENV', workspaceId); }} className="max-h-[fit-content] flex-1 select-none overflow-y-auto p-2 text-sm focus:outline-none" diff --git a/packages/insomnia/src/ui/components/git-credentials/git-remote-branch-select.tsx b/packages/insomnia/src/ui/components/git-credentials/git-remote-branch-select.tsx index 4a73736103..c35da5c6ea 100644 --- a/packages/insomnia/src/ui/components/git-credentials/git-remote-branch-select.tsx +++ b/packages/insomnia/src/ui/components/git-credentials/git-remote-branch-select.tsx @@ -1,8 +1,10 @@ import React, { useDeferredValue, useEffect } from 'react'; import { Button, ComboBox, Input, Label, ListBox, ListBoxItem, Popover } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import type { GitCredentials } from '~/models/git-repository'; +import { useGitRemoteBranchesActionFetcher } from '~/routes/git.remote-branches'; -import type { GitCredentials } from '../../../sync/git/git-vcs'; import { Icon } from '../icon'; export const GitRemoteBranchSelect = ({ @@ -15,25 +17,17 @@ export const GitRemoteBranchSelect = ({ credentials: GitCredentials; }) => { const uri = useDeferredValue(url); - const remoteBranchesFetcher = useFetcher<{ branches: string[] }>({ key: `branch-select:${uri}` }); - const { organizationId } = useParams<{ organizationId: string }>(); + const remoteBranchesFetcher = useGitRemoteBranchesActionFetcher({ key: `branch-select:${uri}` }); + const { organizationId } = useParams() as { organizationId: string }; const isLoadingRemoteBranches = remoteBranchesFetcher.state !== 'idle'; useEffect(() => { if (uri && remoteBranchesFetcher.state === 'idle' && !remoteBranchesFetcher.data) { - remoteBranchesFetcher.submit( - // @ts-expect-error credentials is not defined in the type, but it is used here - { - uri, - credentials, - }, - { - method: 'POST', - encType: 'application/json', - action: `/organization/${organizationId}/git/remote-branches`, - }, - ); + remoteBranchesFetcher.submit({ + uri, + credentials, + }); } }, [organizationId, remoteBranchesFetcher, uri, credentials]); @@ -98,18 +92,10 @@ export const GitRemoteBranchSelect = ({ aria-label="Refresh repositories" onPress={() => { if (uri && remoteBranchesFetcher.state === 'idle') { - remoteBranchesFetcher.submit( - // @ts-expect-error credentials is not defined in the type, but it is used here - { - uri, - credentials, - }, - { - method: 'POST', - encType: 'application/json', - action: `/organization/${organizationId}/git/remote-branches`, - }, - ); + remoteBranchesFetcher.submit({ + uri, + credentials, + }); } }} > diff --git a/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx b/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx index affe67b24a..10951ea6e0 100644 --- a/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx +++ b/packages/insomnia/src/ui/components/git-credentials/github-repository-settings-form.tsx @@ -1,10 +1,14 @@ import React, { useEffect, useState } from 'react'; import { Button } from 'react-aria-components'; -import { useFetcher } from 'react-router'; -import type { GitCredentials } from '../../../models/git-credentials'; -import type { GitRepository } from '../../../models/git-repository'; -import { PromptButton } from '../base/prompt-button'; +import type { GitCredentials } from '~/models/git-credentials'; +import type { GitRepository } from '~/models/git-repository'; +import { useGitHubCredentialsFetcher } from '~/routes/git-credentials.github'; +import { useGithubCompleteSignInFetcher } from '~/routes/git-credentials.github.complete-sign-in'; +import { useInitSignInToGitHubFetcher } from '~/routes/git-credentials.github.init-sign-in'; +import { useGithubSignOutFetcher } from '~/routes/git-credentials.github.sign-out'; +import { PromptButton } from '~/ui/components/base/prompt-button'; + import { GitHubRepositorySelect } from './github-repository-select'; interface Props { @@ -14,11 +18,11 @@ interface Props { export const GitHubRepositorySetupFormGroup = (props: Props) => { const { onSubmit, uri } = props; - const githubTokenLoader = useFetcher(); + const githubTokenLoader = useGitHubCredentialsFetcher(); useEffect(() => { if (!githubTokenLoader.data && githubTokenLoader.state === 'idle') { - githubTokenLoader.load('/git-credentials/github'); + githubTokenLoader.load(); } }, [githubTokenLoader]); @@ -67,7 +71,7 @@ interface GitHubRepositoryFormProps { const GitHubRepositoryForm = ({ uri, credentials, onSubmit }: GitHubRepositoryFormProps) => { const [error, setError] = useState(''); - const signOutFetcher = useFetcher(); + const signOutFetcher = useGithubSignOutFetcher(); return (
{ - signOutFetcher.submit({}, { action: '/git-credentials/github/sign-out', method: 'POST' }); + signOutFetcher.submit(); }} > Disconnect @@ -127,8 +131,8 @@ const GitHubRepositoryForm = ({ uri, credentials, onSubmit }: GitHubRepositoryFo const GitHubSignInForm = () => { const [error, setError] = useState(''); const [isAuthenticating, setIsAuthenticating] = useState(false); - const initSignInFetcher = useFetcher(); - const completeSignInFetcher = useFetcher(); + const initSignInFetcher = useInitSignInToGitHubFetcher(); + const completeSignInFetcher = useGithubCompleteSignInFetcher(); return (
@@ -138,7 +142,7 @@ const GitHubSignInForm = () => { isDisabled={isAuthenticating} onPress={() => { setIsAuthenticating(true); - initSignInFetcher.submit({}, { action: '/git-credentials/github/init-sign-in', method: 'POST' }); + initSignInFetcher.submit(); }} > @@ -156,7 +160,7 @@ const GitHubSignInForm = () => { let parsedURL: URL; try { parsedURL = new URL(link); - } catch (error) { + } catch { setError('Invalid URL'); return; } @@ -169,10 +173,7 @@ const GitHubSignInForm = () => { return; } - completeSignInFetcher.submit( - { code, state }, - { action: '/git-credentials/github/complete-sign-in', method: 'POST', encType: 'application/json' }, - ); + completeSignInFetcher.submit({ code, state }); } }} > diff --git a/packages/insomnia/src/ui/components/git-credentials/gitlab-repository-settings-form.tsx b/packages/insomnia/src/ui/components/git-credentials/gitlab-repository-settings-form.tsx index f551eac2dc..f825715755 100644 --- a/packages/insomnia/src/ui/components/git-credentials/gitlab-repository-settings-form.tsx +++ b/packages/insomnia/src/ui/components/git-credentials/gitlab-repository-settings-form.tsx @@ -1,11 +1,15 @@ import React, { useEffect, useState } from 'react'; import { Button, Input, Label, TextField } from 'react-aria-components'; -import { useFetcher } from 'react-router'; -import type { GitCredentials } from '../../../models/git-credentials'; -import type { GitRepository } from '../../../models/git-repository'; -import { PromptButton } from '../base/prompt-button'; -import { Icon } from '../icon'; +import type { GitCredentials } from '~/models/git-credentials'; +import type { GitRepository } from '~/models/git-repository'; +import { useGitLabCredentialsFetcher } from '~/routes/git-credentials.gitlab'; +import { useGitLabCompleteSignInFetcher } from '~/routes/git-credentials.gitlab.complete-sign-in'; +import { useInitSignInToGitLabFetcher } from '~/routes/git-credentials.gitlab.init-sign-in'; +import { useGitLabSignOutFetcher } from '~/routes/git-credentials.gitlab.sign-out'; +import { PromptButton } from '~/ui/components/base/prompt-button'; +import { Icon } from '~/ui/components/icon'; + import { GitRemoteBranchSelect } from './git-remote-branch-select'; interface Props { @@ -15,11 +19,11 @@ interface Props { export const GitLabRepositorySetupFormGroup = (props: Props) => { const { onSubmit, uri } = props; - const gitlabTokenLoader = useFetcher(); + const gitlabTokenLoader = useGitLabCredentialsFetcher(); useEffect(() => { if (!gitlabTokenLoader.data && gitlabTokenLoader.state === 'idle') { - gitlabTokenLoader.load('/git-credentials/gitlab'); + gitlabTokenLoader.load(); } }, [gitlabTokenLoader]); @@ -69,7 +73,7 @@ interface GitLabRepositoryFormProps { const GitLabRepositoryForm = ({ uri, credentials, onSubmit }: GitLabRepositoryFormProps) => { const [error, setError] = useState(''); const [gitlabUri, setGitlabUri] = useState(uri || ''); - const signOutFetcher = useFetcher(); + const signOutFetcher = useGitLabSignOutFetcher(); return ( { - signOutFetcher.submit({}, { action: '/git-credentials/gitlab/sign-out', method: 'POST' }); + signOutFetcher.submit(); }} > Disconnect @@ -143,8 +147,8 @@ const GitLabRepositoryForm = ({ uri, credentials, onSubmit }: GitLabRepositoryFo const GitLabSignInForm = () => { const [error, setError] = useState(''); const [isAuthenticating, setIsAuthenticating] = useState(false); - const initSignInFetcher = useFetcher(); - const completeSignInFetcher = useFetcher(); + const initSignInFetcher = useInitSignInToGitLabFetcher(); + const completeSignInFetcher = useGitLabCompleteSignInFetcher(); return (
@@ -154,7 +158,7 @@ const GitLabSignInForm = () => { isDisabled={isAuthenticating} onPress={() => { setIsAuthenticating(true); - initSignInFetcher.submit({}, { action: '/git-credentials/gitlab/init-sign-in', method: 'POST' }); + initSignInFetcher.submit(); }} > @@ -185,10 +189,7 @@ const GitLabSignInForm = () => { return; } - completeSignInFetcher.submit( - { code, state }, - { action: '/git-credentials/gitlab/complete-sign-in', method: 'POST', encType: 'application/json' }, - ); + completeSignInFetcher.submit({ code, state }); } }} > diff --git a/packages/insomnia/src/ui/components/github-app-config-link.tsx b/packages/insomnia/src/ui/components/github-app-config-link.tsx index f9f51c1349..53257c6df4 100644 --- a/packages/insomnia/src/ui/components/github-app-config-link.tsx +++ b/packages/insomnia/src/ui/components/github-app-config-link.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { getAppWebsiteBaseURL } from '../../common/constants'; import type { GitRepository } from '../../models/git-repository'; import { getOauth2FormatName } from '../../sync/git/utils'; diff --git a/packages/insomnia/src/ui/components/github-stars-button.tsx b/packages/insomnia/src/ui/components/github-stars-button.tsx index b30c9bdfcf..b3eeb9080d 100644 --- a/packages/insomnia/src/ui/components/github-stars-button.tsx +++ b/packages/insomnia/src/ui/components/github-stars-button.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Link } from 'react-aria-components'; -import { useMount, useMountedState } from 'react-use'; +import * as reactUse from 'react-use'; import { getGitHubRestApiUrl } from '../../common/constants'; import { SegmentEvent } from '../analytics'; @@ -9,7 +9,7 @@ import { Icon } from './icon'; const LOCALSTORAGE_GITHUB_STARS_KEY = 'insomnia:github-stars'; export const GitHubStarsButton = () => { - const isMounted = useMountedState(); + const isMounted = reactUse.useMountedState(); const localStorageStars = localStorage.getItem(LOCALSTORAGE_GITHUB_STARS_KEY); const initialState = parseInt(localStorageStars || '30000', 10); const [starCount, setStarCount] = useState(initialState); @@ -20,7 +20,7 @@ export const GitHubStarsButton = () => { const [error, setError] = useState(null); - useMount(() => { + reactUse.useMount(() => { if (!isMounted()) { return; } diff --git a/packages/insomnia/src/ui/components/header-user-button.tsx b/packages/insomnia/src/ui/components/header-user-button.tsx index fdd344067c..a9fb73957e 100644 --- a/packages/insomnia/src/ui/components/header-user-button.tsx +++ b/packages/insomnia/src/ui/components/header-user-button.tsx @@ -1,11 +1,10 @@ -import React from 'react'; import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; -import { useFetcher } from 'react-router'; -import { getAppWebsiteBaseURL } from '../../common/constants'; -import type { CurrentPlan, PersonalPlanType, UserProfileResponse } from '../organization-utils'; -import { Avatar } from './avatar'; -import { Icon } from './icon'; +import { getAppWebsiteBaseURL } from '~/common/constants'; +import type { CurrentPlan, PersonalPlanType, UserProfileResponse } from '~/models/organization'; +import { useLogoutFetcher } from '~/routes/auth.logout'; +import { Avatar } from '~/ui/components/avatar'; +import { Icon } from '~/ui/components/icon'; const formatCurrentPlanType = (type: PersonalPlanType) => { switch (type) { @@ -70,7 +69,7 @@ interface UserButtonProps { isMinimal?: boolean; } export const HeaderUserButton = ({ user, currentPlan, isMinimal = false }: UserButtonProps) => { - const logoutFetcher = useFetcher(); + const logoutFetcher = useLogoutFetcher(); return ( @@ -93,13 +92,7 @@ export const HeaderUserButton = ({ user, currentPlan, isMinimal = false }: UserB className="focus:outline-none" onAction={action => { if (action === 'logout') { - logoutFetcher.submit( - {}, - { - action: '/auth/logout', - method: 'POST', - }, - ); + logoutFetcher.submit(); } if (action === 'account-settings') { diff --git a/packages/insomnia/src/ui/components/icon.tsx b/packages/insomnia/src/ui/components/icon.tsx index 97ddfe404b..96e4ff6a7c 100644 --- a/packages/insomnia/src/ui/components/icon.tsx +++ b/packages/insomnia/src/ui/components/icon.tsx @@ -3,7 +3,6 @@ import { fab } from '@fortawesome/free-brands-svg-icons'; import { far } from '@fortawesome/free-regular-svg-icons'; import { fas } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon, type FontAwesomeIconProps } from '@fortawesome/react-fontawesome'; -import React from 'react'; library.add(fas, far, fab); diff --git a/packages/insomnia/src/ui/components/insomnia-icon.tsx b/packages/insomnia/src/ui/components/insomnia-icon.tsx index 529442cd0c..1e47d655fa 100644 --- a/packages/insomnia/src/ui/components/insomnia-icon.tsx +++ b/packages/insomnia/src/ui/components/insomnia-icon.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - export const InsomniaLogo = ({ ...props }: {} & React.SVGProps) => ( `${meta ? 'Meta+' : ''}${alt ? 'Alt+' : ''}${ctrl ? 'Control+' : ''}${shift ? 'Shift+' : ''}` + @@ -20,7 +20,7 @@ export function useKeyboardShortcuts( getTarget: () => HTMLElement, listeners: Partial any>>, ) { - const { settings } = useRouteLoaderData('root') as RootLoaderData; + const { settings } = useRootLoaderData()!; const { hotKeyRegistry } = settings; useEffect(() => { diff --git a/packages/insomnia/src/ui/components/markdown-editor.tsx b/packages/insomnia/src/ui/components/markdown-editor.tsx index cf7a8bcb0f..cc3449d2d7 100644 --- a/packages/insomnia/src/ui/components/markdown-editor.tsx +++ b/packages/insomnia/src/ui/components/markdown-editor.tsx @@ -1,7 +1,8 @@ import React, { forwardRef, type ReactElement, useCallback, useState } from 'react'; import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; -import { CodeEditor, type CodeEditorHandle } from './codemirror/code-editor'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; + import { ErrorBoundary } from './error-boundary'; import { MarkdownPreview } from './markdown-preview'; diff --git a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx index 3e2eb29cce..c985401645 100644 --- a/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx +++ b/packages/insomnia/src/ui/components/mocks/mock-response-pane.tsx @@ -3,9 +3,12 @@ import fs from 'node:fs'; import type * as Har from 'har-format'; import React, { Fragment, useCallback, useEffect, useState } from 'react'; import { Button, Tab, TabList, TabPanel, Tabs, Toolbar } from 'react-aria-components'; -import { useRouteLoaderData } from 'react-router'; -import { useFetcher } from 'react-router'; -import { useInterval } from 'react-use'; +import * as reactUse from 'react-use'; + +import { useRootLoaderData } from '~/root'; +import { useRequestNewMockSendActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new-mock-send'; +import { useMockRouteLoaderData } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; import { getMockServiceURL, @@ -24,10 +27,7 @@ import { cancelRequestById } from '../../../network/cancellation'; import { insomniaFetch } from '../../../ui/insomniaFetch'; import { jsonPrettify } from '../../../utils/prettify/json'; import { useExecutionState } from '../../hooks/use-execution-state'; -import type { MockRouteLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; -import { useRootLoaderData } from '../../routes/root'; import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; -import { CodeEditor } from '../codemirror/code-editor'; import { Pane, PaneHeader } from '../panes/pane'; import { PlaceholderResponsePane } from '../panes/placeholder-response-pane'; import { ResponseTimer } from '../response-timer'; @@ -39,6 +39,8 @@ import { ResponseHeadersViewer } from '../viewers/response-headers-viewer'; import { ResponseTimelineViewer } from '../viewers/response-timeline-viewer'; import { ResponseViewer } from '../viewers/response-viewer'; +const { useInterval } = reactUse; + interface MockbinLogOutput { log: { version: string; @@ -57,11 +59,11 @@ interface MockbinLogOutput { } export const MockResponsePane = () => { - const { mockServer, mockRoute, activeResponse } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData; - const { settings } = useRootLoaderData(); + const { mockServer, mockRoute, activeResponse } = useMockRouteLoaderData()!; + const { settings } = useRootLoaderData()!; const [timeline, setTimeline] = useState([]); const [previewMode, setPreviewMode] = useState(PREVIEW_MODE_FRIENDLY); - const requestFetcher = useFetcher({ key: 'mock-request-fetcher' }); + const requestFetcher = useRequestNewMockSendActionFetcher({ key: 'mock-request-fetcher' }); const { steps } = useExecutionState({ requestId: activeResponse?.parentId }); useEffect(() => { @@ -181,7 +183,7 @@ const HistoryViewWrapperComponentFactory = ({ }) => { const [logs, setLogs] = useState(null); const [logEntryId, setLogEntryId] = useState(null); - const { userSession } = useRootLoaderData(); + const { userSession } = useRootLoaderData()!; const fetchLogs = useCallback(async () => { const compoundId = mockRoute.parentId + mockRoute.name; diff --git a/packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx b/packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx index 4fd1847f9f..dcc327d4e9 100644 --- a/packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx +++ b/packages/insomnia/src/ui/components/mocks/mock-url-bar.tsx @@ -1,18 +1,18 @@ import React, { useRef, useState } from 'react'; import { Button } from 'react-aria-components'; -import { useRouteLoaderData } from 'react-router'; -import { useInterval } from 'react-use'; +import * as reactUse from 'react-use'; + +import { useRootLoaderData } from '~/root'; +import { + useMockRouteLoaderData, + useMockRoutePatcher, +} from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; +import type { OneLineEditorHandle } from '~/ui/components/.client/codemirror/one-line-editor'; import { getMockServiceBinURL, HTTP_METHODS } from '../../../common/constants'; import * as models from '../../../models'; import { useTimeoutWhen } from '../../hooks/useTimeoutWhen'; -import { - type MockRouteLoaderData, - useMockRoutePatcher, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.mock-server.mock-route.$mockRouteId'; -import { useRootLoaderData } from '../../routes/root'; import { Dropdown, type DropdownHandle, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; -import type { OneLineEditorHandle } from '../codemirror/one-line-editor'; import { Icon } from '../icon'; import { useDocBodyKeyboardShortcuts } from '../keydown-binder'; import { showModal } from '../modals'; @@ -27,8 +27,8 @@ export const MockUrlBar = ({ onPathUpdate: (path: string) => void; onSend: (path: string) => void; }) => { - const { mockServer, mockRoute } = useRouteLoaderData(':mockRouteId') as MockRouteLoaderData; - const { settings } = useRootLoaderData(); + const { mockServer, mockRoute } = useMockRouteLoaderData()!; + const { settings } = useRootLoaderData()!; const { hotKeyRegistry } = settings; const patchMockRoute = useMockRoutePatcher(); const [pathInput, setPathInput] = useState(mockRoute.name); @@ -41,7 +41,7 @@ export const MockUrlBar = ({ setCurrentTimeout(undefined); onSend(pathInput); }; - useInterval(send, currentInterval ? currentInterval : null); + reactUse.useInterval(send, currentInterval ? currentInterval : null); useTimeoutWhen(send, currentTimeout, !!currentTimeout); useDocBodyKeyboardShortcuts({ request_focusUrl: () => { diff --git a/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx b/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx index 64f82ead35..1ff490f89f 100644 --- a/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/add-request-to-collection-modal.tsx @@ -1,6 +1,8 @@ import React, { type FC, type MouseEventHandler, useEffect, useRef, useState } from 'react'; import { OverlayContainer } from 'react-aria'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import { useRequestNewActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.debug.request.new'; import { database } from '../../../common/database'; import { strings } from '../../../common/strings'; @@ -18,13 +20,21 @@ interface AddRequestModalProps extends ModalProps { } export const AddRequestToCollectionModal: FC = ({ onHide }) => { - const { organizationId, projectId: currentProjectId, workspaceId: currentWorkspaceId } = useParams(); + const { + organizationId, + projectId: currentProjectId, + workspaceId: currentWorkspaceId, + } = useParams() as { + organizationId: string; + projectId: string; + workspaceId: string; + }; const [projectOptions, setProjectOptions] = useState([]); const [workspaceOptions, setWorkspaceOptions] = useState([]); const [selectedProjectId, setSelectedProjectId] = useState(''); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(''); - const requestFetcher = useFetcher(); + const requestFetcher = useRequestNewActionFetcher(); useEffect(() => { (async () => { @@ -55,14 +65,13 @@ export const AddRequestToCollectionModal: FC = ({ onHide } const previousRequestFetcherState = useRef('idle'); const createNewRequest = async () => { - requestFetcher.submit( - { requestType: 'HTTP', parentId: selectedWorkspaceId }, - { - action: `/organization/${organizationId}/project/${selectedProjectId}/workspace/${selectedWorkspaceId}/debug/request/new`, - method: 'post', - encType: 'application/json', - }, - ); + requestFetcher.submit({ + organizationId, + projectId: selectedProjectId, + workspaceId: selectedWorkspaceId, + requestType: 'HTTP', + parentId: selectedWorkspaceId, + }); previousRequestFetcherState.current = 'loading'; }; @@ -131,9 +140,6 @@ export const AddRequestToCollectionModal: FC = ({ onHide } Collection is required

)} - {requestFetcher.data?.error && ( -

{requestFetcher.data.error}

- )}
diff --git a/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx b/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx index d00843ea75..7f0d320798 100644 --- a/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cli-preview-modal.tsx @@ -1,8 +1,7 @@ -import React from 'react'; import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components'; -import { useParams, useRouteLoaderData } from 'react-router'; +import { useParams } from 'react-router'; -import type { WorkspaceLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; +import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { CopyButton } from '../base/copy-button'; import { Icon } from '../icon'; @@ -42,7 +41,7 @@ export const CLIPreviewModal = ({ bail: boolean; }) => { const { workspaceId } = useParams() as { workspaceId: string }; - const { activeEnvironment, activeGlobalEnvironment } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; + const { activeEnvironment, activeGlobalEnvironment } = useWorkspaceLoaderData()!; const workspaceIdAndRequestIds = generateCommandArgumentsForRequests( workspaceId, targetFolderId, diff --git a/packages/insomnia/src/ui/components/modals/cloud-credential-modal/cloud-credential-modal.tsx b/packages/insomnia/src/ui/components/modals/cloud-credential-modal/cloud-credential-modal.tsx index 1dff93bf3b..b3c7424209 100644 --- a/packages/insomnia/src/ui/components/modals/cloud-credential-modal/cloud-credential-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cloud-credential-modal/cloud-credential-modal.tsx @@ -1,6 +1,8 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components'; -import { useFetcher } from 'react-router'; + +import { useUpdateCloudCredentialActionFetcher } from '~/routes/cloud-credentials.$cloudCredentialId.update'; +import { useCreateCloudCredentialActionFetcher } from '~/routes/cloud-credentials.create'; import { EXTERNAL_VAULT_PLUGIN_NAME } from '../../../../common/constants'; import { @@ -29,31 +31,33 @@ export const CloudCredentialModal = (props: CloudCredentialModalProps) => { const [error, setError] = useState(''); const [manulInputUrl, setManualInputUrl] = useState(''); const providerDisplayName = getProviderDisplayName(provider); - const cloudCredentialFetcher = useFetcher(); - const isEditing = !!providerCredential; - const fetchErrorMessage = useMemo(() => { - if ( - cloudCredentialFetcher.data && - 'error' in cloudCredentialFetcher.data && - cloudCredentialFetcher.data.error && - cloudCredentialFetcher.state === 'idle' - ) { - const errorMessage: string = - cloudCredentialFetcher.data.error || - `An unexpected error occurred while authenticating with ${getProviderDisplayName(provider)}.`; - return errorMessage; - } - return undefined; - }, [cloudCredentialFetcher.data, cloudCredentialFetcher.state, provider]); + const updateCloudCredentialsFetcher = useUpdateCloudCredentialActionFetcher(); + const createCloudCredentialsFetcher = useCreateCloudCredentialActionFetcher(); + const isEditing = !!providerCredential; + const upsertFetcher = isEditing ? updateCloudCredentialsFetcher : createCloudCredentialsFetcher; + + const fetchErrorMessage = upsertFetcher.data && 'error' in upsertFetcher.data ? upsertFetcher.data.error : ''; + + const isLoading = upsertFetcher.state !== 'idle'; const handleFormSubmit = (data: BaseCloudCredential & { isAuthenticated?: boolean }) => { const { name, credentials, isAuthenticated = false } = data; - const formAction = isEditing ? `/cloud-credential/${providerCredential._id}/update` : '/cloud-credential/new'; - cloudCredentialFetcher.submit(JSON.stringify({ name, credentials, provider, isAuthenticated }), { - action: formAction, - method: 'post', - encType: 'application/json', + + if (isEditing) { + return updateCloudCredentialsFetcher.submit({ + // @ts-expect-error: TODO - @cwangsmv Type inference doesn't work well with BaseCloudCredential + patch: { name, credentials, provider }, + cloudCredentialId: providerCredential._id, + }); + } + + // @ts-expect-error: TODO - @cwangsmv Type inference doesn't work well with BaseCloudCredential + return createCloudCredentialsFetcher.submit({ + name, + credentials, + provider, + isAuthenticated, }); }; @@ -94,12 +98,12 @@ export const CloudCredentialModal = (props: CloudCredentialModalProps) => { useEffect(() => { // close modal if submit success - if (cloudCredentialFetcher.data && !cloudCredentialFetcher.data.error && cloudCredentialFetcher.state === 'idle') { - const newCredentialData = cloudCredentialFetcher.data; + if (upsertFetcher.data && !('error' in upsertFetcher.data) && upsertFetcher.state === 'idle') { + const newCredentialData = upsertFetcher.data; onClose(newCredentialData); onComplete && onComplete(newCredentialData); } - }, [cloudCredentialFetcher.data, cloudCredentialFetcher.state, onClose, onComplete]); + }, [upsertFetcher.data, upsertFetcher.state, onClose, onComplete]); return ( { {provider === 'aws' && ( @@ -144,7 +148,7 @@ export const CloudCredentialModal = (props: CloudCredentialModalProps) => { {provider === 'gcp' && ( @@ -152,7 +156,7 @@ export const CloudCredentialModal = (props: CloudCredentialModalProps) => { {provider === 'hashicorp' && ( diff --git a/packages/insomnia/src/ui/components/modals/code-prompt-modal.tsx b/packages/insomnia/src/ui/components/modals/code-prompt-modal.tsx index 7c5b6d14a3..4608bb239b 100644 --- a/packages/insomnia/src/ui/components/modals/code-prompt-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/code-prompt-modal.tsx @@ -1,6 +1,8 @@ import React, { forwardRef, useImperativeHandle, useRef, useState } from 'react'; import { Button } from 'react-aria-components'; +import { CodeEditor } from '~/ui/components/.client/codemirror/code-editor'; + import { NunjucksEnabledProvider } from '../../context/nunjucks/nunjucks-enabled-context'; import { CopyButton } from '../base/copy-button'; import { Dropdown, DropdownItem, DropdownSection, ItemContent } from '../base/dropdown'; @@ -8,7 +10,6 @@ import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalFooter } from '../base/modal-footer'; import { ModalHeader } from '../base/modal-header'; -import { CodeEditor } from '../codemirror/code-editor'; import { MarkdownEditor } from '../markdown-editor'; const MODES: Record = { diff --git a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx index 853897587b..081cff9b06 100644 --- a/packages/insomnia/src/ui/components/modals/cookies-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/cookies-modal.tsx @@ -17,17 +17,19 @@ import { Tabs, TextField, } from 'react-aria-components'; -import { useFetcher, useParams, useRouteLoaderData } from 'react-router'; +import { useParams } from 'react-router'; import { Cookie as ToughCookie } from 'tough-cookie'; import { v4 as uuidv4 } from 'uuid'; +import { useUpdateCookieJarActionFetcher } from '~/routes/organization.$organizationId.project.$projectId.workspace.$workspaceId.update-cookie-jar'; +import { OneLineEditor } from '~/ui/components/.client/codemirror/one-line-editor'; + import { cookieToString } from '../../../common/cookies'; import { fuzzyMatch } from '../../../common/misc'; import type { Cookie, CookieJar } from '../../../models/cookie-jar'; +import { useWorkspaceLoaderData } from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { useNunjucks } from '../../context/nunjucks/use-nunjucks'; -import type { WorkspaceLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; import { PromptButton } from '../base/prompt-button'; -import { OneLineEditor } from '../codemirror/one-line-editor'; import { Icon } from '../icon'; import { RenderedText } from '../rendered-text'; @@ -51,23 +53,26 @@ interface Props { export const CookiesModal = ({ setIsOpen }: Props) => { const { handleRender } = useNunjucks(); - const { organizationId, projectId, workspaceId } = useParams<{ + const { organizationId, projectId, workspaceId } = useParams() as { organizationId: string; projectId: string; workspaceId: string; - }>(); - const { activeCookieJar } = useRouteLoaderData(':workspaceId') as WorkspaceLoaderData; - const updateCookieJarFetcher = useFetcher(); + }; + + const { activeCookieJar } = useWorkspaceLoaderData()!; + const updateCookieJarFetcher = useUpdateCookieJarActionFetcher(); const [page, setPage] = useState(0); const [filter, setFilter] = useState(''); const [filteredCookies, setFilteredCookies] = useState(chunkArray(activeCookieJar?.cookies || [])); const updateCookieJar = (cookieJarId: string, patch: CookieJar) => { - updateCookieJarFetcher.submit(JSON.stringify({ patch, cookieJarId }), { - encType: 'application/json', - method: 'post', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/update-cookie-jar`, + updateCookieJarFetcher.submit({ + organizationId, + projectId, + workspaceId, + patch, + cookieJarId, }); setFilteredCookies(chunkArray(patch.cookies)); diff --git a/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx b/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx index c830e90726..3bac5492d9 100644 --- a/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/export-requests-modal.tsx @@ -1,7 +1,7 @@ import { exportRequestsToFile } from 'insomnia/src/ui/components/settings/import-export'; import React, { type FC, type ReactNode, useEffect, useState } from 'react'; import { Button, Checkbox, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; import { requestGroup } from '../../../models'; import { type GrpcRequest, isGrpcRequest } from '../../../models/grpc-request'; @@ -9,11 +9,12 @@ import { isRequest, type Request } from '../../../models/request'; import type { RequestGroup } from '../../../models/request-group'; import { isSocketIORequest, type SocketIORequest } from '../../../models/socket-io-request'; import { isWebSocketRequest, type WebSocketRequest } from '../../../models/websocket-request'; +import { + type Child, + useWorkspaceLoaderFetcher, + type WorkspaceLoaderData, +} from '../../../routes/organization.$organizationId.project.$projectId.workspace.$workspaceId'; import { SegmentEvent } from '../../analytics'; -import type { - Child, - WorkspaceLoaderData, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId'; import { Icon } from '../icon'; import { getMethodShortHand } from '../tags/method-tag'; @@ -192,7 +193,7 @@ export const ExportRequestsModal = ({ onClose: () => void; }) => { const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string }; - const workspaceFetcher = useFetcher(); + const workspaceFetcher = useWorkspaceLoaderFetcher(); const [state, setState] = useState<{ treeRoot: Node | null; }>(); @@ -200,7 +201,11 @@ export const ExportRequestsModal = ({ useEffect(() => { const isIdleAndUninitialized = workspaceFetcher.state === 'idle' && !workspaceFetcher.data; if (isIdleAndUninitialized) { - workspaceFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceIdToExport}`); + workspaceFetcher.load({ + organizationId, + projectId, + workspaceId: workspaceIdToExport, + }); } }, [organizationId, projectId, workspaceFetcher, workspaceIdToExport]); const workspaceLoaderData = workspaceFetcher?.data as WorkspaceLoaderData; diff --git a/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx b/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx index 7d9d3409de..5c975788bd 100644 --- a/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/generate-code-modal.tsx @@ -2,6 +2,8 @@ import type { HTTPSnippetClient, HTTPSnippetTarget } from 'httpsnippet'; import React, { forwardRef, useCallback, useImperativeHandle, useRef, useState } from 'react'; import { Button } from 'react-aria-components'; +import { CodeEditor, type CodeEditorHandle } from '~/ui/components/.client/codemirror/code-editor'; + import { exportHarWithRequest } from '../../../common/har'; import type { Request } from '../../../models/request'; import { CopyButton } from '../base/copy-button'; @@ -11,7 +13,6 @@ import { Modal, type ModalHandle, type ModalProps } from '../base/modal'; import { ModalBody } from '../base/modal-body'; import { ModalFooter } from '../base/modal-footer'; import { ModalHeader } from '../base/modal-header'; -import { CodeEditor, type CodeEditorHandle } from '../codemirror/code-editor'; const MODE_MAP: Record = { c: 'clike', diff --git a/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx index ba632a7b05..97847dfcb2 100644 --- a/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-branches-modal.tsx @@ -11,17 +11,15 @@ import { ModalOverlay, TextField, } from 'react-aria-components'; -import { useFetcher, useParams, useRevalidator } from 'react-router'; +import { useParams, useRevalidator } from 'react-router'; + +import { useGitProjectCheckoutBranchActionFetcher } from '~/routes/git.branch.checkout'; +import { useGitProjectDeleteBranchActionFetcher } from '~/routes/git.branch.delete'; +import { useGitProjectNewBranchActionFetcher } from '~/routes/git.branch.new'; +import { useGitProjectBranchesLoaderFetcher } from '~/routes/git.branches'; +import { useGitProjectChangesFetcher } from '~/routes/git.changes'; import type { MergeConflict } from '../../../sync/types'; -import { - checkGitCanPush, - continueMerge, - type CreateNewGitBranchResult, - type GitBranchesLoaderData, - type GitChangesLoaderData, - mergeGitBranch, -} from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.git'; import { PromptButton } from '../base/prompt-button'; import { Icon } from '../icon'; import { showModal } from '.'; @@ -31,31 +29,28 @@ import { SyncMergeModal } from './sync-merge-modal'; const LocalBranchItem = ({ branch, isCurrent, - organizationId, projectId, workspaceId, hasUncommittedChanges, }: { branch: string; isCurrent: boolean; - organizationId: string; projectId: string; workspaceId: string; hasUncommittedChanges: boolean; }) => { - const checkoutBranchFetcher = useFetcher<{} | { error: string }>(); - const mergeBranchFetcher = useFetcher(); - const deleteBranchFetcher = useFetcher(); + const checkoutBranchFetcher = useGitProjectCheckoutBranchActionFetcher(); + const deleteBranchFetcher = useGitProjectDeleteBranchActionFetcher(); useEffect(() => { if ( checkoutBranchFetcher.data && - 'error' in checkoutBranchFetcher.data && - checkoutBranchFetcher.data.error && + 'errors' in checkoutBranchFetcher.data && + checkoutBranchFetcher.data.errors && checkoutBranchFetcher.state === 'idle' ) { const error: string = - checkoutBranchFetcher.data.error || 'An unexpected error occurred while checking out the branch.'; + checkoutBranchFetcher.data.errors[0] || 'An unexpected error occurred while checking out the branch.'; showModal(AlertModal, { title: 'Error while checking out branch.', message: error, @@ -63,29 +58,15 @@ const LocalBranchItem = ({ } }, [checkoutBranchFetcher.data, checkoutBranchFetcher.state]); - useEffect(() => { - if ( - mergeBranchFetcher.data && - 'error' in mergeBranchFetcher.data && - mergeBranchFetcher.data.error && - mergeBranchFetcher.state === 'idle' - ) { - const error: string = mergeBranchFetcher.data.error || 'An unexpected error occurred while merging the branches.'; - showModal(AlertModal, { - title: 'Error while merging branches.', - message: error, - }); - } - }, [mergeBranchFetcher.data, mergeBranchFetcher.state]); - useEffect(() => { if ( deleteBranchFetcher.data && - 'error' in deleteBranchFetcher.data && - deleteBranchFetcher.data.error && + 'errors' in deleteBranchFetcher.data && + deleteBranchFetcher.data.errors && deleteBranchFetcher.state === 'idle' ) { - const error: string = deleteBranchFetcher.data.error || 'An unexpected error occurred while deleting the branch.'; + const error: string = + deleteBranchFetcher.data.errors[0] || 'An unexpected error occurred while deleting the branch.'; showModal(AlertModal, { title: 'Error while deleting branch', message: error, @@ -112,15 +93,11 @@ const LocalBranchItem = ({ disabled={isCurrent || branch === 'master'} onClick={() => { setErrorMessage(''); - deleteBranchFetcher.submit( - { - branch, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/delete`, - }, - ); + deleteBranchFetcher.submit({ + projectId, + workspaceId, + branch, + }); }} > { setErrorMessage(''); - // file://./../../routes/git-actions.tsx#checkoutGitBranchAction - checkoutBranchFetcher.submit( - { - branch, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branch/checkout`, - }, - ); + + checkoutBranchFetcher.submit({ + projectId, + workspaceId, + branch, + }); }} > { if (Array.isArray(conflicts) && conflicts.length > 0) { - continueMerge({ - projectId, - workspaceId, - handledMergeConflicts: conflicts, - commitMessage: result.commitMessage, - commitParent: result.commitParent, - }) + window.main.git + .continueMerge({ + projectId, + workspaceId, + handledMergeConflicts: conflicts, + commitMessage: result.commitMessage, + commitParent: result.commitParent, + }) .then(resolve, reject) .finally(() => { - checkGitCanPush({ projectId, workspaceId }); + window.main.git.canPushLoader({ projectId, workspaceId }); revalidate(); }); } else { @@ -217,10 +191,7 @@ const LocalBranchItem = ({ } }} > - + Merge
@@ -232,32 +203,30 @@ const LocalBranchItem = ({ const RemoteBranchItem = ({ branch, - organizationId, projectId, workspaceId, }: { branch: string; - isCurrent: boolean; - organizationId: string; projectId: string; workspaceId: string; }) => { - const pullBranchFetcher = useFetcher(); + const checkoutBranchFetcher = useGitProjectCheckoutBranchActionFetcher(); useEffect(() => { if ( - pullBranchFetcher.data && - 'error' in pullBranchFetcher.data && - pullBranchFetcher.data.error && - pullBranchFetcher.state === 'idle' + checkoutBranchFetcher.data && + 'errors' in checkoutBranchFetcher.data && + checkoutBranchFetcher.data.errors && + checkoutBranchFetcher.state === 'idle' ) { - const error: string = pullBranchFetcher.data.error || 'An unexpected error occurred while pulling the branch.'; + const error: string = + checkoutBranchFetcher.data.errors[0] || 'An unexpected error occurred while pulling the branch.'; showModal(AlertModal, { title: 'Error while pulling branch.', message: error, }); } - }, [pullBranchFetcher.data, pullBranchFetcher.state]); + }, [checkoutBranchFetcher.data, checkoutBranchFetcher.state]); return (
@@ -266,20 +235,16 @@ const RemoteBranchItem = ({ @@ -310,8 +275,8 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose } workspaceId: string; }; - const branchesFetcher = useFetcher(); - const createBranchFetcher = useFetcher(); + const branchesFetcher = useGitProjectBranchesLoaderFetcher(); + const createBranchFetcher = useGitProjectNewBranchActionFetcher(); const errors = branchesFetcher.data && 'errors' in branchesFetcher.data ? branchesFetcher.data.errors : []; const { remoteBranches, branches: localBranches } = @@ -325,9 +290,10 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose } useEffect(() => { if (branchesFetcher.state === 'idle' && !branchesFetcher.data) { - branchesFetcher.load( - `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/branches`, - ); + branchesFetcher.load({ + projectId, + workspaceId, + }); } }, [branchesFetcher, organizationId, projectId, workspaceId]); @@ -336,13 +302,13 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose } ? createBranchFetcher.data.errors[0] : null; - const gitChangesFetcher = useFetcher(); + const gitChangesFetcher = useGitProjectChangesFetcher(); useEffect(() => { if (gitChangesFetcher.state === 'idle' && !gitChangesFetcher.data) { - // file://./../../routes/git-actions.tsx#gitChangesLoader - gitChangesFetcher.load( - `/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/changes`, - ); + gitChangesFetcher.load({ + projectId, + workspaceId, + }); } }, [organizationId, projectId, workspaceId, gitChangesFetcher]); @@ -380,8 +346,18 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose }
- { + e.preventDefault(); + + const formData = new FormData(e.currentTarget); + const branch = formData.get('branch')?.toString().trim() || ''; + createBranchFetcher.submit({ + branch, + projectId, + workspaceId, + }); + }} method="POST" className="flex flex-shrink-0 flex-col gap-2" > @@ -416,7 +392,7 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose }

)} - +
Local Branches @@ -441,7 +417,6 @@ export const GitBranchesModal: FC = ({ currentBranch, branches, onClose } = ({ currentBranch, branches, onClose } textValue={item.name} className="w-full p-2 transition-colors focus:bg-[--hl-sm] focus:outline-none" > - + )} diff --git a/packages/insomnia/src/ui/components/modals/git-log-modal.tsx b/packages/insomnia/src/ui/components/modals/git-log-modal.tsx index 396d1bd5aa..efe1ec7b19 100644 --- a/packages/insomnia/src/ui/components/modals/git-log-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-log-modal.tsx @@ -14,9 +14,10 @@ import { Tooltip, TooltipTrigger, } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import { useGitProjectLogLoaderFetcher } from '~/routes/git.log'; -import type { GitLogLoaderData } from '../../routes/$organizationId.project.$projectId.workspace.$workspaceId.git'; import { Icon } from '../icon'; import { TimeFromNow } from '../time-from-now'; @@ -31,14 +32,16 @@ export const GitLogModal: FC = ({ onClose }) => { workspaceId: string; }; - const gitLogFetcher = useFetcher(); + const gitLogFetcher = useGitProjectLogLoaderFetcher(); const isLoading = gitLogFetcher.state !== 'idle'; useEffect(() => { if (gitLogFetcher.state === 'idle' && !gitLogFetcher.data) { - // file://./../../routes/git-actions.tsx#gitLogLoader - gitLogFetcher.load(`/organization/${organizationId}/project/${projectId}/workspace/${workspaceId}/git/log`); + gitLogFetcher.load({ + projectId, + workspaceId, + }); } }, [organizationId, projectId, workspaceId, gitLogFetcher]); diff --git a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx index b5f66a0925..792722d4b9 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-branches-modal.tsx @@ -11,51 +11,46 @@ import { ModalOverlay, TextField, } from 'react-aria-components'; -import { useFetcher, useParams, useRevalidator } from 'react-router'; +import { useParams, useRevalidator } from 'react-router'; + +import { useGitProjectCheckoutBranchActionFetcher } from '~/routes/git.branch.checkout'; +import { useGitProjectDeleteBranchActionFetcher } from '~/routes/git.branch.delete'; +import { useGitProjectNewBranchActionFetcher } from '~/routes/git.branch.new'; +import { useGitProjectBranchesLoaderFetcher } from '~/routes/git.branches'; +import { useGitProjectChangesFetcher } from '~/routes/git.changes'; +import type { MergeConflict } from '~/sync/types'; +import { SyncMergeModal } from '~/ui/components/modals/sync-merge-modal'; -import type { MergeConflict } from '../../../sync/types'; -import { - checkGitCanPush, - continueMerge, - type CreateNewGitBranchResult, - type GitBranchesLoaderData, - type GitChangesLoaderData, - mergeGitBranch, -} from '../../routes/$organizationId.project.$projectId.git'; import { PromptButton } from '../base/prompt-button'; import { Icon } from '../icon'; import { showModal } from '.'; import { AlertModal } from './alert-modal'; -import { SyncMergeModal } from './sync-merge-modal'; const LocalBranchItem = ({ branch, isCurrent, - organizationId, projectId, - workspaceId, hasUncommittedChanges, }: { branch: string; isCurrent: boolean; - organizationId: string; projectId: string; - workspaceId: string; hasUncommittedChanges: boolean; }) => { - const checkoutBranchFetcher = useFetcher<{} | { error: string }>(); - const mergeBranchFetcher = useFetcher(); - const deleteBranchFetcher = useFetcher(); + const checkoutBranchFetcher = useGitProjectCheckoutBranchActionFetcher(); + + const deleteBranchFetcher = useGitProjectDeleteBranchActionFetcher(); + const { revalidate } = useRevalidator(); useEffect(() => { if ( checkoutBranchFetcher.data && - 'error' in checkoutBranchFetcher.data && - checkoutBranchFetcher.data.error && + 'errors' in checkoutBranchFetcher.data && + checkoutBranchFetcher.data.errors && checkoutBranchFetcher.state === 'idle' ) { const error: string = - checkoutBranchFetcher.data.error || 'An unexpected error occurred while checking out the branch.'; + checkoutBranchFetcher.data.errors[0] || 'An unexpected error occurred while checking out the branch.'; showModal(AlertModal, { title: 'Error while checking out branch.', message: error, @@ -63,29 +58,15 @@ const LocalBranchItem = ({ } }, [checkoutBranchFetcher.data, checkoutBranchFetcher.state]); - useEffect(() => { - if ( - mergeBranchFetcher.data && - 'error' in mergeBranchFetcher.data && - mergeBranchFetcher.data.error && - mergeBranchFetcher.state === 'idle' - ) { - const error: string = mergeBranchFetcher.data.error || 'An unexpected error occurred while merging the branches.'; - showModal(AlertModal, { - title: 'Error while merging branches.', - message: error, - }); - } - }, [mergeBranchFetcher.data, mergeBranchFetcher.state]); - useEffect(() => { if ( deleteBranchFetcher.data && - 'error' in deleteBranchFetcher.data && - deleteBranchFetcher.data.error && + 'errors' in deleteBranchFetcher.data && + deleteBranchFetcher.data.errors && deleteBranchFetcher.state === 'idle' ) { - const error: string = deleteBranchFetcher.data.error || 'An unexpected error occurred while deleting the branch.'; + const error: string = + deleteBranchFetcher.data.errors[0] || 'An unexpected error occurred while deleting the branch.'; showModal(AlertModal, { title: 'Error while deleting branch', message: error, @@ -95,8 +76,6 @@ const LocalBranchItem = ({ const [errMsg, setErrorMessage] = useState(''); - const { revalidate } = useRevalidator(); - return (
@@ -112,15 +91,10 @@ const LocalBranchItem = ({ disabled={isCurrent || branch === 'master'} onClick={() => { setErrorMessage(''); - deleteBranchFetcher.submit( - { - branch, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/branch/delete`, - }, - ); + deleteBranchFetcher.submit({ + branch, + projectId, + }); }} > { setErrorMessage(''); - // file://./../../routes/git-actions.tsx#checkoutGitBranchAction - checkoutBranchFetcher.submit( - { - branch, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/branch/checkout`, - }, - ); + checkoutBranchFetcher.submit({ + branch, + projectId, + }); }} > { showModal(SyncMergeModal, { @@ -184,15 +151,15 @@ const LocalBranchItem = ({ labels: result.labels, handleDone: (conflicts?: MergeConflict[]) => { if (Array.isArray(conflicts) && conflicts.length > 0) { - continueMerge({ - projectId, - handledMergeConflicts: conflicts, - commitMessage: result.commitMessage, - commitParent: result.commitParent, - }) + window.main.git + .continueMerge({ + projectId, + handledMergeConflicts: conflicts, + commitMessage: result.commitMessage, + commitParent: result.commitParent, + }) .then(resolve, reject) .finally(() => { - checkGitCanPush(workspaceId); revalidate(); }); } else { @@ -203,12 +170,9 @@ const LocalBranchItem = ({ }); }); } - if ('errors' in result && result.errors && result.errors?.length > 0) { setErrorMessage(result.errors.join('\n')); } - - checkGitCanPush(workspaceId); revalidate(); } catch (err) { const errorMessage = @@ -218,10 +182,7 @@ const LocalBranchItem = ({ } }} > - + Merge
@@ -231,33 +192,23 @@ const LocalBranchItem = ({ ); }; -const RemoteBranchItem = ({ - branch, - organizationId, - projectId, -}: { - branch: string; - isCurrent: boolean; - organizationId: string; - projectId: string; - workspaceId: string; -}) => { - const pullBranchFetcher = useFetcher(); +const RemoteBranchItem = ({ branch, projectId }: { branch: string; isCurrent: boolean; projectId: string }) => { + const checkoutBranch = useGitProjectCheckoutBranchActionFetcher(); useEffect(() => { if ( - pullBranchFetcher.data && - 'error' in pullBranchFetcher.data && - pullBranchFetcher.data.error && - pullBranchFetcher.state === 'idle' + checkoutBranch.data && + checkoutBranch.data?.errors && + checkoutBranch.data.errors.length > 0 && + checkoutBranch.state === 'idle' ) { - const error: string = pullBranchFetcher.data.error || 'An unexpected error occurred while pulling the branch.'; + const error: string = checkoutBranch.data.errors[0] || 'An unexpected error occurred while pulling the branch.'; showModal(AlertModal, { title: 'Error while pulling branch.', message: error, }); } - }, [pullBranchFetcher.data, pullBranchFetcher.state]); + }, [checkoutBranch.data, checkoutBranch.state]); return (
@@ -266,20 +217,15 @@ const RemoteBranchItem = ({ @@ -304,14 +250,13 @@ function sortBranches(branchA: string, branchB: string) { } export const GitProjectBranchesModal: FC = ({ currentBranch, branches, onClose }) => { - const { organizationId, projectId, workspaceId } = useParams() as { + const { organizationId, projectId } = useParams() as { organizationId: string; projectId: string; - workspaceId: string; }; - const branchesFetcher = useFetcher(); - const createBranchFetcher = useFetcher(); + const branchesFetcher = useGitProjectBranchesLoaderFetcher(); + const createBranchFetcher = useGitProjectNewBranchActionFetcher(); const errors = branchesFetcher.data && 'errors' in branchesFetcher.data ? branchesFetcher.data.errors : []; const { remoteBranches, branches: localBranches } = @@ -325,22 +270,25 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on useEffect(() => { if (branchesFetcher.state === 'idle' && !branchesFetcher.data) { - branchesFetcher.load(`/organization/${organizationId}/project/${projectId}/git/branches`); + branchesFetcher.load({ + projectId, + }); } - }, [branchesFetcher, organizationId, projectId, workspaceId]); + }, [branchesFetcher, organizationId, projectId]); const createNewBranchError = createBranchFetcher.data?.errors && createBranchFetcher.data.errors.length > 0 ? createBranchFetcher.data.errors[0] : null; - const gitChangesFetcher = useFetcher(); + const gitChangesFetcher = useGitProjectChangesFetcher(); useEffect(() => { if (gitChangesFetcher.state === 'idle' && !gitChangesFetcher.data) { - // file://./../../routes/git-actions.tsx#gitChangesLoader - gitChangesFetcher.load(`/organization/${organizationId}/project/${projectId}/git/changes`); + gitChangesFetcher.load({ + projectId, + }); } - }, [organizationId, projectId, workspaceId, gitChangesFetcher]); + }, [organizationId, projectId, gitChangesFetcher]); const hasUncommittedChanges = Boolean( gitChangesFetcher.data?.changes && @@ -376,8 +324,17 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on
- { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const branch = (formData.get('branch') as string) || ''; + + createBranchFetcher.submit({ + projectId, + branch, + }); + }} method="POST" className="flex flex-shrink-0 flex-col gap-2" > @@ -412,7 +369,7 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on

)} - +
Local Branches @@ -437,9 +394,7 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on @@ -472,13 +427,7 @@ export const GitProjectBranchesModal: FC = ({ currentBranch, branches, on textValue={item.name} className="w-full p-2 transition-colors focus:bg-[--hl-sm] focus:outline-none" > - + )} diff --git a/packages/insomnia/src/ui/components/modals/git-project-log-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-log-modal.tsx index d58cc5afa8..8a7dd60f8a 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-log-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-log-modal.tsx @@ -14,9 +14,10 @@ import { Tooltip, TooltipTrigger, } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import { useGitProjectLogLoaderFetcher } from '~/routes/git.log'; -import type { GitLogLoaderData } from '../../routes/$organizationId.project.$projectId.git'; import { Icon } from '../icon'; import { TimeFromNow } from '../time-from-now'; @@ -31,14 +32,15 @@ export const GitProjectLogModal: FC = ({ onClose }) => { workspaceId: string; }; - const gitLogFetcher = useFetcher(); + const gitLogFetcher = useGitProjectLogLoaderFetcher(); const isLoading = gitLogFetcher.state !== 'idle'; useEffect(() => { if (gitLogFetcher.state === 'idle' && !gitLogFetcher.data) { - // file://./../../routes/git-actions.tsx#gitLogLoader - gitLogFetcher.load(`/organization/${organizationId}/project/${projectId}/git/log`); + gitLogFetcher.load({ + projectId, + }); } }, [organizationId, projectId, gitLogFetcher]); diff --git a/packages/insomnia/src/ui/components/modals/git-project-migration-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-migration-modal.tsx index 5d44beb3dc..1a5000be35 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-migration-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-migration-modal.tsx @@ -12,7 +12,9 @@ import { TableBody, TableHeader, } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import { useGitProjectMigrateLegacyInsomniaFolderActionFetcher } from '~/routes/git.migrate-legacy-insomnia-folder-to-file'; import type { WorkspaceScope } from '../../../models/workspace'; import { @@ -20,30 +22,23 @@ import { scopeToIconMap, scopeToLabelMap, scopeToTextColorMap, -} from '../../routes/$organizationId.project.$projectId'; +} from '../../../routes/organization.$organizationId.project.$projectId._index'; import { Icon } from '../icon'; export const GitProjectMigrationModal: FC<{ onClose: () => void; legacyFile: { name: string; scope: WorkspaceScope; path: string }; }> = ({ onClose, legacyFile }) => { - const { organizationId, projectId } = useParams() as { - organizationId: string; + const { projectId } = useParams() as { projectId: string; - workspaceId: string; }; - const migrateLegacyWorkspaceFetcher = useFetcher(); + const migrateLegacyWorkspaceFetcher = useGitProjectMigrateLegacyInsomniaFolderActionFetcher(); const migrateLegacyWorkspace = () => { - migrateLegacyWorkspaceFetcher.submit( - {}, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/migrate-legacy-insomnia-folder-to-file`, - encType: 'application/json', - }, - ); + migrateLegacyWorkspaceFetcher.submit({ + projectId, + }); }; return ( diff --git a/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx b/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx index 12ca6d8e30..a2d930fa5b 100644 --- a/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx +++ b/packages/insomnia/src/ui/components/modals/git-project-staging-modal.tsx @@ -13,9 +13,15 @@ import { Tooltip, TooltipTrigger, } from 'react-aria-components'; -import { useFetcher, useParams } from 'react-router'; +import { useParams } from 'react-router'; + +import { useGitProjectChangesFetcher } from '~/routes/git.changes'; +import { useGitProjectCommitActionFetcher } from '~/routes/git.commit'; +import { useGitProjectDiffLoaderFetcher } from '~/routes/git.diff'; +import { useGitProjectDiscardActionFetcher } from '~/routes/git.discard'; +import { useGitProjectStageActionFetcher } from '~/routes/git.stage'; +import { useGitProjectUnstageActionFetcher } from '~/routes/git.unstage'; -import type { GitChangesLoaderData, GitDiffResult } from '../../routes/$organizationId.project.$projectId.git'; import { DiffEditor } from '../diff-view-editor'; import { Icon } from '../icon'; import { AlertModal } from './alert-modal'; @@ -27,52 +33,34 @@ export const GitProjectStagingModal: FC<{ onClose: () => void }> = ({ onClose }) projectId: string; workspaceId: string; }; - const gitChangesFetcher = useFetcher(); + const gitChangesFetcher = useGitProjectChangesFetcher(); - const stageChangesFetcher = useFetcher<{ - errors?: string[]; - }>(); - const unstageChangesFetcher = useFetcher<{ - errors?: string[]; - }>(); - const undoUnstagedChangesFetcher = useFetcher<{ - errors?: string[]; - }>(); - const diffChangesFetcher = useFetcher(); + const stageChangesFetcher = useGitProjectStageActionFetcher(); + const unstageChangesFetcher = useGitProjectUnstageActionFetcher(); + const undoUnstagedChangesFetcher = useGitProjectDiscardActionFetcher(); + const diffChangesFetcher = useGitProjectDiffLoaderFetcher(); + const commitFetcher = useGitProjectCommitActionFetcher(); function diffChanges({ path, staged }: { path: string; staged: boolean }) { - let url = `/organization/${organizationId}/project/${projectId}/git/diff`; - const params = new URLSearchParams(); - params.set('filepath', path); - params.set('staged', staged ? 'true' : 'false'); - url += '?' + params.toString(); - diffChangesFetcher.load(`${url}`); + diffChangesFetcher.load({ + projectId, + filePath: path, + staged, + }); } function stageChanges(paths: string[]) { - stageChangesFetcher.submit( - { - paths, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/stage`, - encType: 'application/json', - }, - ); + stageChangesFetcher.submit({ + projectId, + paths, + }); } function unstageChanges(paths: string[]) { - unstageChangesFetcher.submit( - { - paths, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/unstage`, - encType: 'application/json', - }, - ); + unstageChangesFetcher.submit({ + projectId, + paths, + }); } function undoUnstagedChanges(paths: string[], filesCount: number) { @@ -81,16 +69,10 @@ export const GitProjectStagingModal: FC<{ onClose: () => void }> = ({ onClose }) title: 'Discard changes', okLabel: 'Discard', onConfirm: () => { - undoUnstagedChangesFetcher.submit( - { - paths, - }, - { - method: 'POST', - action: `/organization/${organizationId}/project/${projectId}/git/discard`, - encType: 'application/json', - }, - ); + undoUnstagedChangesFetcher.submit({ + projectId, + paths, + }); }, addCancel: true, }); @@ -98,8 +80,9 @@ export const GitProjectStagingModal: FC<{ onClose: () => void }> = ({ onClose }) useEffect(() => { if (gitChangesFetcher.state === 'idle' && !gitChangesFetcher.data) { - // file://./../../routes/git-actions.tsx#gitChangesLoader - gitChangesFetcher.load(`/organization/${organizationId}/project/${projectId}/git/changes`); + gitChangesFetcher.load({ + projectId, + }); } }, [organizationId, projectId, workspaceId, gitChangesFetcher]); @@ -112,23 +95,20 @@ export const GitProjectStagingModal: FC<{ onClose: () => void }> = ({ onClose }) statusNames: {}, }; - const { Form, formAction, state, data } = useFetcher<{ errors?: string[] }>(); + const isCommiting = commitFetcher.state !== 'idle'; - const isCreatingSnapshot = - state !== 'idle' && formAction === `/organization/${organizationId}/project/${projectId}/git/commit`; - const isPushing = - state !== 'idle' && formAction === `/organization/${organizationId}/project/${projectId}/git/commit-and-push`; const previewDiffItem = diffChangesFetcher.data && 'diff' in diffChangesFetcher.data ? diffChangesFetcher.data : null; const allChanges = [...changes.staged, ...changes.unstaged]; const allChangesLength = allChanges.length; - const noCommitErrors = data && 'errors' in data && data.errors?.length === 0; + const hasNoCommitErrors = + commitFetcher.data && 'errors' in commitFetcher.data && commitFetcher.data.errors?.length === 0; useEffect(() => { - if (allChangesLength === 0 && noCommitErrors) { + if (allChangesLength === 0 && hasNoCommitErrors) { onClose(); } - }, [allChangesLength, onClose, noCommitErrors]); + }, [allChangesLength, onClose, hasNoCommitErrors]); return ( void }> = ({ onClose })
-
+ { + e.preventDefault(); + const submitter = e.nativeEvent instanceof SubmitEvent ? e.nativeEvent.submitter : null; + const formData = new FormData(e.currentTarget, submitter); + const message = formData.get('message')?.toString() || ''; + const push = Boolean(formData.get('push') === 'true'); + + commitFetcher.submit({ + projectId, + message, + push, + }); + }} + className="flex flex-col gap-2" + >