From e2fccb604ad21c006a6fd9e40ac621756f9883cb Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:38:45 +0800 Subject: [PATCH] refactor(web): rebuild goto anything dialog (#38914) --- oxlint-suppressions.json | 69 +- .../src/dialog/__tests__/index.spec.tsx | 30 +- packages/dify-ui/src/dialog/index.tsx | 1 + pnpm-lock.yaml | 1687 ++--------------- pnpm-workspace.yaml | 1 - .../goto-anything/command-selector.test.tsx | 310 --- .../goto-anything/match-action.test.ts | 234 --- .../search-error-handling.test.ts | 209 -- .../__tests__/app-detail-top.spec.tsx | 39 +- .../__tests__/dataset-detail-top.spec.tsx | 37 +- .../components/app-sidebar/app-detail-top.tsx | 24 +- .../app-sidebar/dataset-detail-top.tsx | 24 +- web/app/components/goto-anything/README.md | 33 + .../__tests__/command-selector.spec.tsx | 285 --- .../goto-anything/__tests__/context.spec.tsx | 131 -- .../goto-anything/__tests__/index.spec.tsx | 627 +++--- .../actions/__tests__/app.spec.ts | 197 +- .../actions/__tests__/index.spec.ts | 290 +-- .../actions/__tests__/knowledge.spec.ts | 142 +- .../actions/__tests__/node-actions.spec.ts | 78 +- .../actions/__tests__/plugin.spec.ts | 90 +- .../components/goto-anything/actions/app.tsx | 135 +- .../commands/__tests__/create.spec.tsx | 7 - .../commands/__tests__/refine.spec.tsx | 5 - .../commands/__tests__/registry.spec.ts | 4 +- .../actions/commands/__tests__/slash.spec.tsx | 2 + .../actions/commands/account.tsx | 6 +- .../actions/commands/community.tsx | 6 +- .../goto-anything/actions/commands/create.tsx | 20 +- .../goto-anything/actions/commands/docs.tsx | 6 +- .../goto-anything/actions/commands/forum.tsx | 6 +- .../goto-anything/actions/commands/go.tsx | 25 +- .../actions/commands/language.tsx | 2 +- .../goto-anything/actions/commands/refine.tsx | 8 +- .../actions/commands/registry.ts | 36 +- .../goto-anything/actions/commands/slash.tsx | 3 +- .../goto-anything/actions/commands/theme.tsx | 12 +- .../goto-anything/actions/commands/types.ts | 6 +- .../components/goto-anything/actions/index.ts | 287 +-- .../goto-anything/actions/knowledge.tsx | 41 +- .../goto-anything/actions/plugin.tsx | 51 +- .../actions/rag-pipeline-nodes.tsx | 12 +- .../components/goto-anything/actions/types.ts | 31 +- .../goto-anything/actions/workflow-nodes.tsx | 12 +- web/app/components/goto-anything/atoms.ts | 13 - .../goto-anything/command-selector.tsx | 147 -- .../components/__tests__/empty-state.spec.tsx | 8 +- .../components/__tests__/footer.spec.tsx | 286 +-- .../components/__tests__/result-item.spec.tsx | 71 - .../components/__tests__/result-list.spec.tsx | 76 - .../__tests__/search-input.spec.tsx | 191 -- .../goto-anything/components/empty-state.tsx | 14 +- .../goto-anything/components/footer.tsx | 33 +- .../goto-anything/components/index.ts | 7 - .../goto-anything/components/result-item.tsx | 32 - .../goto-anything/components/result-list.tsx | 50 - .../goto-anything/components/search-input.tsx | 64 - web/app/components/goto-anything/context.tsx | 68 - .../components/goto-anything/dialog-handle.ts | 5 + .../__tests__/use-goto-anything-modal.spec.ts | 213 --- .../use-goto-anything-navigation.spec.ts | 496 ----- .../use-goto-anything-results.spec.ts | 453 ----- .../use-goto-anything-search.spec.ts | 298 --- .../hooks/use-goto-anything-modal.ts | 43 - .../hooks/use-goto-anything-navigation.ts | 109 -- .../hooks/use-goto-anything-results.ts | 147 -- .../hooks/use-goto-anything-search.ts | 77 - web/app/components/goto-anything/index.tsx | 742 ++++++-- .../main-nav/__tests__/index.spec.tsx | 31 +- .../main-nav/components/search-button.tsx | 20 +- .../rag-pipeline/goto-anything-search.ts | 17 + .../use-rag-pipeline-search.spec.tsx | 68 +- .../hooks/use-rag-pipeline-search.tsx | 17 +- .../components/snippet-detail-top.tsx | 24 +- .../workflow/goto-anything-search.ts | 17 + .../__tests__/use-workflow-search.spec.tsx | 9 +- .../workflow/hooks/use-workflow-search.tsx | 17 +- .../agent-v2/agent-detail/navigation.tsx | 24 +- .../deployments/detail/deployment-sidebar.tsx | 24 +- web/package.json | 1 - web/service/apps.ts | 11 - 81 files changed, 1889 insertions(+), 7295 deletions(-) delete mode 100644 web/__tests__/goto-anything/command-selector.test.tsx delete mode 100644 web/__tests__/goto-anything/match-action.test.ts delete mode 100644 web/__tests__/goto-anything/search-error-handling.test.ts create mode 100644 web/app/components/goto-anything/README.md delete mode 100644 web/app/components/goto-anything/__tests__/command-selector.spec.tsx delete mode 100644 web/app/components/goto-anything/__tests__/context.spec.tsx delete mode 100644 web/app/components/goto-anything/atoms.ts delete mode 100644 web/app/components/goto-anything/command-selector.tsx delete mode 100644 web/app/components/goto-anything/components/__tests__/result-item.spec.tsx delete mode 100644 web/app/components/goto-anything/components/__tests__/result-list.spec.tsx delete mode 100644 web/app/components/goto-anything/components/__tests__/search-input.spec.tsx delete mode 100644 web/app/components/goto-anything/components/index.ts delete mode 100644 web/app/components/goto-anything/components/result-item.tsx delete mode 100644 web/app/components/goto-anything/components/result-list.tsx delete mode 100644 web/app/components/goto-anything/components/search-input.tsx delete mode 100644 web/app/components/goto-anything/context.tsx create mode 100644 web/app/components/goto-anything/dialog-handle.ts delete mode 100644 web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts delete mode 100644 web/app/components/goto-anything/hooks/__tests__/use-goto-anything-navigation.spec.ts delete mode 100644 web/app/components/goto-anything/hooks/__tests__/use-goto-anything-results.spec.ts delete mode 100644 web/app/components/goto-anything/hooks/__tests__/use-goto-anything-search.spec.ts delete mode 100644 web/app/components/goto-anything/hooks/use-goto-anything-modal.ts delete mode 100644 web/app/components/goto-anything/hooks/use-goto-anything-navigation.ts delete mode 100644 web/app/components/goto-anything/hooks/use-goto-anything-results.ts delete mode 100644 web/app/components/goto-anything/hooks/use-goto-anything-search.ts create mode 100644 web/app/components/rag-pipeline/goto-anything-search.ts create mode 100644 web/app/components/workflow/goto-anything-search.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 82e93dda6a4..06e93398aac 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -122,22 +122,6 @@ "count": 1 } }, - "web/__tests__/goto-anything/command-selector.test.tsx": { - "jsx_a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx_a11y/no-static-element-interactions": { - "count": 1 - }, - "typescript/no-explicit-any": { - "count": 2 - } - }, - "web/__tests__/goto-anything/search-error-handling.test.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "web/__tests__/i18n-upload-features.test.ts": { "no-console": { "count": 3 @@ -3220,62 +3204,11 @@ "count": 2 } }, - "web/app/components/goto-anything/actions/commands/registry.ts": { - "typescript/no-explicit-any": { - "count": 3 - } - }, - "web/app/components/goto-anything/actions/commands/types.ts": { - "typescript/no-explicit-any": { - "count": 1 - } - }, - "web/app/components/goto-anything/actions/plugin.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "web/app/components/goto-anything/actions/recent-store.ts": { "no-restricted-globals": { "count": 2 } }, - "web/app/components/goto-anything/actions/types.ts": { - "typescript/no-explicit-any": { - "count": 2 - } - }, - "web/app/components/goto-anything/components/__tests__/search-input.spec.tsx": { - "jsx_a11y/no-autofocus": { - "count": 1 - } - }, - "web/app/components/goto-anything/components/index.ts": { - "no-barrel-files/no-barrel-files": { - "count": 4 - } - }, - "web/app/components/goto-anything/components/search-input.tsx": { - "jsx_a11y/no-autofocus": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/goto-anything/context.tsx": { - "eslint-react/set-state-in-effect": { - "count": 4 - }, - "react/only-export-components": { - "count": 1 - } - }, - "web/app/components/goto-anything/hooks/use-goto-anything-results.ts": { - "@tanstack/query/exhaustive-deps": { - "count": 1 - } - }, "web/app/components/header/account-setting/data-source-page-new/__tests__/item.spec.tsx": { "jsx_a11y/click-events-have-key-events": { "count": 1 @@ -6846,7 +6779,7 @@ "count": 1 }, "typescript/no-explicit-any": { - "count": 7 + "count": 6 } }, "web/service/base.ts": { diff --git a/packages/dify-ui/src/dialog/__tests__/index.spec.tsx b/packages/dify-ui/src/dialog/__tests__/index.spec.tsx index 1c7906ad85c..7d954862436 100644 --- a/packages/dify-ui/src/dialog/__tests__/index.spec.tsx +++ b/packages/dify-ui/src/dialog/__tests__/index.spec.tsx @@ -1,5 +1,13 @@ import { render } from 'vitest-browser-react' -import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '../index' +import { + createDialogHandle, + Dialog, + DialogCloseButton, + DialogContent, + DialogDescription, + DialogTitle, + DialogTrigger, +} from '../index' const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement @@ -18,6 +26,26 @@ describe('Dialog wrapper', () => { await expect.element(screen.getByRole('dialog')).toHaveTextContent('Dialog Title') await expect.element(screen.getByRole('dialog')).toHaveTextContent('Dialog Description') }) + + it('should connect a detached trigger to the dialog', async () => { + const handle = createDialogHandle() + const screen = await render( + <> + Open dialog + + + Detached dialog + + + , + ) + + await screen.getByRole('button', { name: 'Open dialog' }).click() + + await expect + .element(screen.getByRole('dialog', { name: 'Detached dialog' })) + .toBeInTheDocument() + }) }) describe('Props', () => { diff --git a/packages/dify-ui/src/dialog/index.tsx b/packages/dify-ui/src/dialog/index.tsx index 827c72a5e57..7e1a0d40439 100644 --- a/packages/dify-ui/src/dialog/index.tsx +++ b/packages/dify-ui/src/dialog/index.tsx @@ -9,6 +9,7 @@ export const DialogTrigger = BaseDialog.Trigger export const DialogTitle = BaseDialog.Title export const DialogDescription = BaseDialog.Description export const DialogPortal = BaseDialog.Portal +export const createDialogHandle = BaseDialog.createHandle type DialogBackdropProps = Omit & { className?: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccc3da07be3..a4b76fc7e4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -279,9 +279,6 @@ catalogs: clsx: specifier: 2.1.1 version: 2.1.1 - cmdk: - specifier: 1.1.1 - version: 1.1.1 code-inspector-plugin: specifier: 1.6.6 version: 1.6.6 @@ -912,10 +909,10 @@ importers: version: '@typescript/typescript6@6.0.2' vite-plus: specifier: 'catalog:' - version: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + version: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) vitest: specifier: 'catalog:' - version: 4.1.10(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(happy-dom@20.10.6) + version: 4.1.10(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(happy-dom@20.10.6) packages/dev-proxy: dependencies: @@ -1251,9 +1248,6 @@ importers: class-variance-authority: specifier: 'catalog:' version: 0.7.1 - cmdk: - specifier: 'catalog:' - version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) copy-to-clipboard: specifier: 'catalog:' version: 4.0.2 @@ -1797,7 +1791,6 @@ packages: '@babel/parser@7.29.7': resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} - hasBin: true '@babel/runtime@7.29.7': resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} @@ -1892,11 +1885,9 @@ packages: '@cucumber/cucumber@13.0.0': resolution: {integrity: sha512-lUD/IxGZXbfSP+pd7zaYvJIJXBaTZ36CmQxcLDYkilGH8y4ycaOnXe7ll0QFukYFh9/1x6S7pw6lYspJg6g7jw==} engines: {node: 22 || 24 || >=26} - hasBin: true '@cucumber/gherkin-streams@6.0.0': resolution: {integrity: sha512-HLSHMmdDH0vCr7vsVEURcDA4WwnRLdjkhqr6a4HQ3i4RFK1wiDGPjBGVdGJLyuXuRdJpJbFc6QxHvT8pU4t6jw==} - hasBin: true peerDependencies: '@cucumber/gherkin': '>=22.0.0' '@cucumber/message-streams': '>=4.0.0' @@ -1904,7 +1895,6 @@ packages: '@cucumber/gherkin-utils@11.0.0': resolution: {integrity: sha512-LJ+s4+TepHTgdKWDR4zbPyT7rQjmYIcukTwNbwNwgqr6i8Gjcmzf6NmtbYDA19m1ZFg6kWbFsmHnj37ZuX+kZA==} - hasBin: true '@cucumber/gherkin@38.0.0': resolution: {integrity: sha512-duEXK+KDfQUzu3vsSzXjkxQ2tirF5PRsc1Xrts6THKHJO6mjw4RjM8RV+vliuDasmhhrmdLcOcM7d9nurNTJKw==} @@ -2298,7 +2288,6 @@ packages: '@hey-api/openapi-ts@0.98.2': resolution: {integrity: sha512-2nVJXH8tpFPGTBOhxyjEd1Jw0hsRqJqeTQW3kltAjVdSU4YWxeu97x5sgNOmsbsfeg6Dqz7Wfzs26walBOuswA==} engines: {node: '>=22.18.0'} - hasBin: true peerDependencies: typescript: '>=5.5.3 || >=6.0.0 || 6.0.1-rc' @@ -3939,7 +3928,6 @@ packages: '@playwright/test@1.61.1': resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==} engines: {node: '>=18'} - hasBin: true '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} @@ -3947,177 +3935,6 @@ packages: '@preact/signals-core@1.14.3': resolution: {integrity: sha512-m0K3vnbSLC5rHs2ZVfeAMvBtT1zIyq4mxx5OlNncSgMj5Iz6W5Rn3kPrDxAC+iIKmiVe0lSl6U37t5ZkEWoVAw==} - '@radix-ui/primitive@1.1.4': - resolution: {integrity: sha512-7AdCK9PQyiljKoBDbN8OuctCbd/esdwZPQ8RtOE3SsyQtUpiPb+ND75q0jEhC1m1ecBI0MFNeLJvwIh9iKHRcQ==} - - '@radix-ui/react-compose-refs@1.1.3': - resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-context@1.1.4': - resolution: {integrity: sha512-QwH4PO5urrbO+FaGd5Aglg+YJgWTyyuZ3g/6mKvsqraLkglDdckw9JafgL5McL5VEJ6EPNduPaT3ZE9BttDAqg==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-dialog@1.1.17': - resolution: {integrity: sha512-TDTYmpdq8dI2+Xgvgj9AJ8Ghqq+Eph/TRVEdaFQPDItIY+6QSkU7MJMeevw1568Yw/2Ijz8BTphPSP2XejKphw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-dismissable-layer@1.1.13': - resolution: {integrity: sha512-2v+zNAWWe0ySxgC0D0yeXMPQ23xZVgXZTerTz+JKlmdRj6gfTqmCcR29jb6d290DezXPGgruHWDX/vYUebtErg==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-focus-guards@1.1.4': - resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-focus-scope@1.1.10': - resolution: {integrity: sha512-Fas/lXQqhVvqwAb64s5RFeHiHYElZ6SUQbZaNd6EkfhP/Al7wTIQ9WIR4QVX475tlu5yFCEdDcJH6/UwsZjMWw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-id@1.1.2': - resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-portal@1.1.12': - resolution: {integrity: sha512-m309havGzsjLHHaIX50G5PlvRs3xkgPCsGk/5PTvYm8D5q33yG0J7w/712PTOhid7NTaFETtnSXjngHQavvhVw==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-presence@1.1.6': - resolution: {integrity: sha512-zdTk4PlUO0E18HnZ3wYbW0KkJJxWCdiNYp6g6X1PtONFhxVkg01vliTJAmwIszU6mHiyBOoW9P0rAugl5/hULQ==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-primitive@2.1.6': - resolution: {integrity: sha512-wetd0QI77DbvrPpTAvH1SqOxsYF2wZe5TNxqwOd5Ty4XDpV3dpV0s8K/1MGMJBeY5o7lg8ub5VIt1Ub+yVen6g==} - peerDependencies: - '@types/react': '*' - '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@radix-ui/react-slot@1.3.0': - resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-callback-ref@1.1.2': - resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-controllable-state@1.2.3': - resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-effect-event@0.0.3': - resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-escape-keydown@1.1.2': - resolution: {integrity: sha512-2uVLvLjgO7NZCWw01/FdqRwmA42J0BcjPMUCA+koFEOAb+zjqIP7SiFz/7zWPrKnVmSqr76Omq2ALyCuX4dhLw==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - - '@radix-ui/react-use-layout-effect@1.1.2': - resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==} - peerDependencies: - '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - '@reactflow/background@11.3.14': resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==} peerDependencies: @@ -4256,7 +4073,6 @@ packages: '@shuding/opentype.js@1.4.0-beta.0': resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} engines: {node: '>= 8.0.0'} - hasBin: true '@sindresorhus/base62@1.0.0': resolution: {integrity: sha512-TeheYy0ILzBEI/CO55CP6zJCSdSWeRtGnHy8U8dWSUH4I68iqTsy7HkMktR4xakThc9jotkPQUXT4ITdbV7cHA==} @@ -4569,7 +4385,6 @@ packages: '@tanstack/devtools-event-client@0.4.4': resolution: {integrity: sha512-6T5Yop/793YI+H+5J8Hsyj4kCih9sl4t3ElLgKioW5hk3ocn+ZdSJ94tT7vL7uabxSugWYBZlOTMPzEw2puvQw==} engines: {node: '>=18'} - hasBin: true '@tanstack/eslint-plugin-query@5.101.2': resolution: {integrity: sha512-cPE99s3XZwlObfn8lCezT4j4JLj2CVzpIEywx0H4hzfPsX/o9QhdwaOwcDXxrQAqx2ds7TbvTinxhB8B/ywb6w==} @@ -4669,7 +4484,6 @@ packages: '@tsslint/cli@3.1.4': resolution: {integrity: sha512-svoLfFkoWmdsDrIRLllFnrxydfMjKKZ1UBjv7Sua1KjFkx6VaJ88+YGYqNiTbB/dDcU10qnSMXRavNTJ0fjBkQ==} engines: {node: '>=22.6.0'} - hasBin: true peerDependencies: typescript: '*' @@ -4681,7 +4495,6 @@ packages: '@tsslint/config@3.1.4': resolution: {integrity: sha512-6VcUimc170M1v3b0vmOhRW7NI/b7DqXB5Wpo+1wCNLprDTN1HwjsbfdFNm/nsd0jXWChNr/cJhmsnVp4xHDCKw==} engines: {node: '>=22.6.0'} - hasBin: true peerDependencies: '@tsslint/compat-eslint': ^3.0.0 tsl: ^1.0.28 @@ -5092,7 +4905,6 @@ packages: '@typescript/typescript6@6.0.2': resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} - hasBin: true '@ungap/structured-clone@1.3.2': resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} @@ -5368,7 +5180,6 @@ packages: acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} - hasBin: true agentation@3.0.2: resolution: {integrity: sha512-iGzBxFVTuZEIKzLY6AExSLAQH6i6SwxV4pAu7v7m3X6bInZ7qlZXAwrEqyc4+EfP4gM7z2RXBF6SF4DeH0f2lA==} @@ -5425,10 +5236,6 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - aria-hidden@1.2.6: - resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} - engines: {node: '>=10'} - aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -5436,26 +5243,6 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - assertion-error-formatter@3.0.0: resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} @@ -5463,9 +5250,6 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types-flow@0.0.8: - resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} - ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -5475,33 +5259,17 @@ packages: astring@1.9.0: resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - axe-core@4.12.1: resolution: {integrity: sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==} engines: {node: '>=4'} - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -5520,7 +5288,6 @@ packages: baseline-browser-mapping@2.10.40: resolution: {integrity: sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==} engines: {node: '>=6.0.0'} - hasBin: true birecord@0.1.2: resolution: {integrity: sha512-5PAPTTmMpMEb+GuMb5DebfBkipRGyIW9+gtwEBSoDA9xkhHILm04+hZQ702pMksu3d8YAuGkmgTzQWcKqTPScA==} @@ -5534,9 +5301,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - brace-expansion@1.1.16: - resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} - brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} @@ -5548,7 +5312,6 @@ packages: browserslist@4.28.4: resolution: {integrity: sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -5607,18 +5370,6 @@ packages: resolution: {integrity: sha512-8PWf78SYJg9kGpUNlqOlg2iE4nu/iW7FidBVdvEZPy0GJRUyN91X7q0VzX/oyJE6bGrgr/bR0VhcGF87vgeQaw==} engines: {node: '>=18'} - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -5691,7 +5442,6 @@ packages: chromatic@16.10.0: resolution: {integrity: sha512-nFsztmnu7rFiGafUJgXvLUNpqmRylz92eNvzBoJNTKKQj4EQUyxznwnfpf1dTs7hXtWD8JwcH92jADydaHA1sw==} - hasBin: true peerDependencies: '@chromatic-com/cypress': ^0.*.* || ^1.0.0 '@chromatic-com/playwright': ^0.*.* || ^1.0.0 @@ -5743,12 +5493,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - cmdk@1.1.1: - resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} - peerDependencies: - react: ^18 || ^19 || ^19.0.0-rc - react-dom: ^18 || ^19 || ^19.0.0-rc - code-inspector-plugin@1.6.6: resolution: {integrity: sha512-NQ8CWmMbFiZ+ck34Zxw4/awjIYcyutNPiVCdPe2hVXMD/NG2D+aWAz8NI2krOIagGnbny8j/YCyoiDsNRbnAbQ==} @@ -5764,7 +5508,6 @@ packages: color-support@1.1.3: resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} - hasBin: true comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -5800,13 +5543,9 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - concurrently@10.0.3: resolution: {integrity: sha512-hc3LH4UaKWd/bbyDK/IGVa4RB6PtQ3CUYwtrkzqHn+wIG3Hr5fhpRlk0L/gCa8ZE1L/Ufj50Zho69cI5w8SQBA==} engines: {node: '>=22'} - hasBin: true confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -5879,7 +5618,6 @@ packages: cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} - hasBin: true cssfontparser@1.2.1: resolution: {integrity: sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==} @@ -5947,7 +5685,6 @@ packages: d3-dsv@3.0.1: resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} engines: {node: '>=12'} - hasBin: true d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} @@ -6047,21 +5784,6 @@ packages: dagre-d3-es@7.0.14: resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} - damerau-levenshtein@1.0.8: - resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - dayjs@1.11.21: resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} @@ -6103,18 +5825,10 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} @@ -6136,9 +5850,6 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-node-es@1.1.0: - resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - devframe@0.5.4: resolution: {integrity: sha512-dbHU/LuptR1aMXcizjHUeY3gu7qVaRQoFYqbbyyGuO6Y+avFA4uQtwinHtG1qa7lRel/7b8JrBDm0nbnxU3vqg==} peerDependencies: @@ -6192,10 +5903,6 @@ packages: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - echarts-for-react@3.0.6: resolution: {integrity: sha512-4zqLgTGWS3JvkQDXjzkR1k1CHRdpd6by0988TWMJgnvDytegWLbeP/VNZmMa+0VJx2eD7Y632bi2JquXDgiGJg==} peerDependencies: @@ -6241,9 +5948,6 @@ packages: resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} engines: {node: '>=10.0.0'} - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - empathic@2.0.1: resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} engines: {node: '>=14'} @@ -6291,14 +5995,6 @@ packages: error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - es-abstract-get@1.0.0: - resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} - engines: {node: '>= 0.4'} - - es-abstract@1.24.2: - resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} - engines: {node: '>= 0.4'} - es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -6313,22 +6009,6 @@ packages: es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.4: - resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} - engines: {node: '>= 0.4'} - es-toolkit@1.49.0: resolution: {integrity: sha512-G5iZ6Pc/FNRY/soKZHC+TxGDD83rHUDXxzaWhGCX44vAv/tMs56WMusnm/KMNK+luUPsgA9U28cGr4RDlSzL2g==} @@ -6341,7 +6021,6 @@ packages: esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} - hasBin: true escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} @@ -6551,7 +6230,6 @@ packages: eslint@10.7.0: resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - hasBin: true peerDependencies: jiti: '*' peerDependenciesMeta: @@ -6565,7 +6243,6 @@ packages: esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} - hasBin: true esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} @@ -6631,7 +6308,6 @@ packages: extract-zip@2.0.1: resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} engines: {node: '>= 10.17.0'} - hasBin: true fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6697,10 +6373,6 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -6708,7 +6380,6 @@ packages: formatly@0.3.0: resolution: {integrity: sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w==} engines: {node: '>=18.3.0'} - hasBin: true foxact@0.3.8: resolution: {integrity: sha512-lFakEG8xm6A6wMH4ycWhXvwUybq4ATnMxJfBD4hj6hm9CKo72q2M/YaiEemoWGh9sUjQqX+pPXfq8S/jxedWzA==} @@ -6748,28 +6419,14 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function-timeout@1.0.2: resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} engines: {node: '>=18'} - function.prototype.name@1.2.0: - resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} - engines: {node: '>= 0.4'} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - fuse.js@7.4.2: resolution: {integrity: sha512-LVbzjD4WA6UP5B1UnP8wuaXJiLnqMdM/E4fiJXTJ5haJ5b/MBNsK29h2fm6swEoQaVQjvYFWKLE2RanyZIoRVQ==} engines: {node: '>=10'} - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6782,32 +6439,15 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-nonce@1.0.1: - resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} - engines: {node: '>=6'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-stream@5.2.0: resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} engines: {node: '>=8'} - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} giget@3.3.0: resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} - hasBin: true github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -6839,24 +6479,15 @@ packages: resolution: {integrity: sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==} engines: {node: '>=18'} - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} h3@2.0.1-rc.22: resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} engines: {node: '>=20.11.1'} - hasBin: true peerDependencies: crossws: ^0.4.1 peerDependenciesMeta: @@ -6874,33 +6505,10 @@ packages: resolution: {integrity: sha512-vAyM+6+jAYwSwz0/M0jYKfU9AvAMCz0kH791RsUhvMKGUHXled/3FjcQB3YiQ4Astj5srHdb6B2FHGIfZkOQNg==} engines: {node: '>=18'} - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - - hasown@2.0.4: - resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} - engines: {node: '>= 0.4'} - hast-util-from-dom@5.0.1: resolution: {integrity: sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==} @@ -7021,7 +6629,6 @@ packages: image-size@2.0.2: resolution: {integrity: sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==} engines: {node: '>=16.x'} - hasBin: true immer@11.1.11: resolution: {integrity: sha512-qzXuyXAkPySAGYkfsAwodDPWT8Zm7/Uo5BNt4BjhMhG5WlWyZZ4wQqnWwdS8kjlQ1Cwu6gjw3A6+0gTQwlyYtw==} @@ -7058,10 +6665,6 @@ packages: inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - internmap@1.0.1: resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} @@ -7083,62 +6686,21 @@ packages: is-alphanumerical@2.0.1: resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - is-builtin-module@5.0.0: resolution: {integrity: sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==} engines: {node: '>=18.20'} - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - is-document.all@1.0.0: - resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} - engines: {node: '>= 0.4'} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -7157,7 +6719,6 @@ packages: is-inside-container@1.0.0: resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} engines: {node: '>=14.16'} - hasBin: true is-installed-globally@1.0.0: resolution: {integrity: sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==} @@ -7167,18 +6728,6 @@ packages: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} @@ -7191,57 +6740,18 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - is-stream@4.0.1: resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} engines: {node: '>=18'} - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -7259,7 +6769,6 @@ packages: jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} - hasBin: true jotai-effect@2.3.1: resolution: {integrity: sha512-FBBVXDM2podbbxJsZ19+uDv45LxeXoVA5yh6CfkQ35AVCuRHj7Lanlcjiea3b67Q7+/MMGXWf8+GeB73JcbeMg==} @@ -7322,11 +6831,9 @@ packages: js-yaml@4.3.0: resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} - hasBin: true js-yaml@5.2.1: resolution: {integrity: sha512-zfLtNfQqxVqq3uaTqSkh4x4hZw3KHobGUA0fJUj4wawW8bsQLTVqpHdXSIzidh7o+4lEW36tANuAGdaFx6Zgnw==} - hasBin: true jsdoc-type-pratt-parser@7.1.1: resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} @@ -7339,7 +6846,6 @@ packages: jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} - hasBin: true json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -7353,7 +6859,6 @@ packages: json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} - hasBin: true jsonc-eslint-parser@3.1.0: resolution: {integrity: sha512-75EA7EWZExL/j+MDKQrRbdzcRI2HOkRlmUw8fZJc1ioqFEOvBsq7Rt+A6yCxOt9w/TYNpkt52gC6nm/g5tFIng==} @@ -7368,17 +6873,11 @@ packages: jsonschema@1.5.0: resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - katex@0.16.47: resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} - hasBin: true katex@0.17.0: resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==} - hasBin: true keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7389,7 +6888,6 @@ packages: knip@6.26.0: resolution: {integrity: sha512-e9eELEEpBpGTd4H4HB7818/DYj9dMzMyUqAddfYwUN/EbSkgIjOuWEF96W/xHsmV0SDrsdXjIM+oZ2xpPzPsBA==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true knuth-shuffle-seeded@1.0.6: resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} @@ -7401,13 +6899,6 @@ packages: resolution: {integrity: sha512-/GmXpo9F9W+f8n4Ivr2iH+7h7wL7jLbLKWkMlpflcCRb6kGjBfTlASEXaZ9qUgNTn4VgS0P2pwxxzQ4EM6Ulgg==} engines: {node: '>=22'} - language-subtag-registry@0.3.23: - resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} - - language-tags@1.0.9: - resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} - engines: {node: '>=0.10'} - launch-ide@1.4.5: resolution: {integrity: sha512-W7e4UPmJRwW/YgMn9n3dG5RyzHWGOf31Zb0SmG4pYyH5PiL2KlNZHRo9eUOvozYVY2oqglTfidNKWUiOK6PAJA==} @@ -7547,7 +7038,6 @@ packages: loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true loro-crdt@1.13.6: resolution: {integrity: sha512-OsnKW64J+1XdCMafAqR+aASn/wQEgDzwcDwu6dDOVSxxbLPFgnU6LX8ja+hd5PnEcT+kuz9eXSagvlYXsRbQIw==} @@ -7568,7 +7058,6 @@ packages: lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} - hasBin: true magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -7594,16 +7083,10 @@ packages: marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} - hasBin: true marked@17.0.6: resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==} engines: {node: '>= 20'} - hasBin: true - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} mdast-util-directive@3.1.0: resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} @@ -7802,12 +7285,10 @@ packages: mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} - hasBin: true mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} - hasBin: true mimic-function@5.0.1: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} @@ -7825,9 +7306,6 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -7848,7 +7326,6 @@ packages: mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} - hasBin: true mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} @@ -7889,7 +7366,6 @@ packages: nanoid@3.3.15: resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} @@ -7917,7 +7393,6 @@ packages: next@16.2.10: resolution: {integrity: sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==} engines: {node: '>=20.9.0'} - hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 '@playwright/test': ^1.51.1 @@ -8086,7 +7561,6 @@ packages: os: linux libc: musl version: 22.23.1 - hasBin: true normalize-package-data@8.0.0: resolution: {integrity: sha512-RWk+PI433eESQ7ounYxIp67CYuVsS1uYSonX3kA6ps/3LWfjVQa/ptEg6Y3T6uAMq1mWpX9PQ+qx+QaHpsc7gQ==} @@ -8132,26 +7606,6 @@ packages: object-deep-merge@2.0.1: resolution: {integrity: sha512-aKttDKcU3pyZqKcCkDhsMn70WmZFG2JGDQLP9EcLyTSIFQRCPWLAmBZRLJnrVUrhPG1jETEEbfdgbNtJf1LyMg==} - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - obug@2.1.3: resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} engines: {node: '>=12.20.0'} @@ -8191,10 +7645,6 @@ packages: resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} engines: {node: '>=20'} - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - oxc-parser@0.127.0: resolution: {integrity: sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -8213,7 +7663,6 @@ packages: oxfmt@0.57.0: resolution: {integrity: sha512-ZB7Bi+rGDSqmVIo9jwcLyFgjxXvQhDdU+jx+ZrVy6VRiVXK2+CHc4hO3J4dUQjHe7V0ymHB+MDuv5z+NhK07HA==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true peerDependencies: svelte: ^5.0.0 vite-plus: '*' @@ -8225,12 +7674,10 @@ packages: oxlint-tsgolint@0.24.0: resolution: {integrity: sha512-giCk5sEvG02d5tzPmFMX3hem8ndzEEu1xvGYS5OwNfO2WGl6ZVxt5LjE0yiMDoz94INI7XkXwgFAQiydPvVHDw==} - hasBin: true oxlint@1.72.0: resolution: {integrity: sha512-1rhdZIP/EvoI91ABIwNU5Q8+bWf8mjrS5UzIOZld4d4bXxJvtlUhlQvaoTogIGin/qdErMOrwaIJvCSIAKTLhA==} engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true peerDependencies: oxlint-tsgolint: '>=0.22.1' vite-plus: '*' @@ -8362,12 +7809,10 @@ packages: playwright-core@1.61.1: resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==} engines: {node: '>=18'} - hasBin: true playwright@1.61.1: resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==} engines: {node: '>=18'} - hasBin: true pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} @@ -8390,10 +7835,6 @@ packages: resolution: {integrity: sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==} engines: {node: '>= 10.12'} - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - postcss-selector-parser@6.1.4: resolution: {integrity: sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==} engines: {node: '>=4'} @@ -8413,7 +7854,6 @@ packages: resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. - hasBin: true prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} @@ -8467,7 +7907,6 @@ packages: rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true re-resizable@6.11.2: resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==} @@ -8536,26 +7975,6 @@ packages: react: '>=18.0.0' react-dom: '>=18.0.0' - react-remove-scroll-bar@2.3.8: - resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-remove-scroll@2.7.2: - resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react-rnd@10.5.3: resolution: {integrity: sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==} peerDependencies: @@ -8578,16 +7997,6 @@ packages: react-dom: '>=16.9.0' sortablejs: '1' - react-style-singleton@2.2.3: - resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - react-textarea-autosize@8.5.9: resolution: {integrity: sha512-U1DGlIQN5AwgjTyOEnI1oCcMuEr1pv1qOtklB2l4nyMGbHzWrI0eFsYK0zos2YWqAolJyG0IWJaqWmWj5ETh0A==} engines: {node: '>=10'} @@ -8649,10 +8058,6 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -8671,15 +8076,9 @@ packages: regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} - hasBin: true - - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} regjsparser@0.13.2: resolution: {integrity: sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==} - hasBin: true rehype-harden@1.1.8: resolution: {integrity: sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==} @@ -8743,7 +8142,6 @@ packages: resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} - hasBin: true restore-cursor@5.1.0: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} @@ -8775,24 +8173,12 @@ packages: rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} - safe-array-concat@1.1.4: - resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} - engines: {node: '>=0.4'} - safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} safe-json-stringify@1.2.0: resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - satori@0.16.0: resolution: {integrity: sha512-ZvHN3ygzZ8FuxjSNB+mKBiF/NIoqHzlBGbD0MJiT+MvSsFOvotnWOhdTjxKzhHRT2wPC1QbhLzx2q/Y83VhfYQ==} engines: {node: '>=16'} @@ -8817,38 +8203,22 @@ packages: semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true semver@7.8.1: resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} - hasBin: true semver@7.8.2: resolution: {integrity: sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==} engines: {node: '>=10'} - hasBin: true semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} - hasBin: true server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -8952,7 +8322,6 @@ packages: srvx@0.11.17: resolution: {integrity: sha512-43yM4luKfCJamyCMhrUeHUPOrf8TdZe7kN8s5zayZCH5OeprYqi49Aso5ZvHXR4aB+DHaRNO/diNFgZSMNG8Xw==} engines: {node: '>=20.16.0'} - hasBin: true stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -8974,13 +8343,8 @@ packages: resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} engines: {node: '>=18'} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - storybook@10.5.0: resolution: {integrity: sha512-dRhM/kSSvHQR8DmZO41v5sJuz9U6zDjjR2gRBTgZN2RBSXbmF0Brvgszrvvxyx2VfxuYKzhB+xumKwWkwlBtig==} - hasBin: true peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 prettier: ^2 || ^3 @@ -9013,22 +8377,6 @@ packages: string.prototype.codepointat@0.2.1: resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} - string.prototype.includes@2.0.1: - resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} - engines: {node: '>= 0.4'} - - string.prototype.trim@1.2.11: - resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.10: - resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} @@ -9103,7 +8451,6 @@ packages: svgo@3.3.3: resolution: {integrity: sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==} engines: {node: '>=14.0.0'} - hasBin: true synckit@0.11.13: resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} @@ -9185,7 +8532,6 @@ packages: tldts@7.4.8: resolution: {integrity: sha512-htwgN/8KRB3z3vnC0BOETVh2m499g5GmyTK9Wq5JBLX3FNz6tSBveAd+fQhzy9hkjif8vy2jwDMR1sGhLtZl2A==} - hasBin: true to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -9208,7 +8554,6 @@ packages: tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -9236,7 +8581,6 @@ packages: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} deprecated: unmaintained - hasBin: true peerDependencies: typescript: ^5.0.0 peerDependenciesMeta: @@ -9259,7 +8603,6 @@ packages: tsx@4.23.0: resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==} engines: {node: '>=18.0.0'} - hasBin: true tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -9283,22 +8626,6 @@ packages: resolution: {integrity: sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==} engines: {node: '>=20'} - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.8: - resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} - engines: {node: '>= 0.4'} - typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -9307,7 +8634,6 @@ packages: typescript@7.0.2: resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} engines: {node: '>=16.20.0'} - hasBin: true ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} @@ -9315,16 +8641,11 @@ packages: uglify-js@3.19.3: resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} engines: {node: '>=0.8.0'} - hasBin: true unbash@4.0.2: resolution: {integrity: sha512-8gwNZ29+0/3zmXw7ToIHZtg6wK37xnniRUdBt7B27xZxaxfgR5tGMaGHT0t0dLtBV9fXE7zurh0s6Z1DHVjfWg==} engines: {node: '>=14'} - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -9419,23 +8740,12 @@ packages: update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true peerDependencies: browserslist: '>= 4.21.0' uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-callback-ref@1.3.3: - resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-composed-ref@1.4.0: resolution: {integrity: sha512-djviaxuOOh7wkj0paeO1Q/4wMZ8Zrnag5H6yBvzN7AKKe8beOaED9SF5/ByLqsku8NP4zQqsvM2u3ew/tJK8/w==} peerDependencies: @@ -9469,16 +8779,6 @@ packages: '@types/react': optional: true - use-sidecar@1.1.3: - resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} - engines: {node: '>=10'} - peerDependencies: - '@types/react': '*' - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc - peerDependenciesMeta: - '@types/react': - optional: true - use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -9492,7 +8792,6 @@ packages: uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} - hasBin: true valibot@1.4.2: resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==} @@ -9517,7 +8816,6 @@ packages: vinext@1.0.0-beta.1: resolution: {integrity: sha512-epfX5y/li8dTzBRoVcJK19vWf/kM50bSkYhqF8BePz4/Y3oPClTAndYgkMNmRxPJ5rps2H2ZNcxlnAoLWKuAXw==} engines: {node: '>=22'} - hasBin: true peerDependencies: '@mdx-js/rollup': ^3.0.0 '@vitejs/plugin-react': ^5.1.4 || ^6.0.0 @@ -9560,7 +8858,6 @@ packages: vite-plus@0.2.4: resolution: {integrity: sha512-gaBBjOXIq9lLRU44oAYdIr99p+JBLX1kxs+l/6LqGgSXwcVKAdDa1boSrOTELqYCkQQ0fpppXUGWi9o6JDT5zw==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} - hasBin: true peerDependencies: '@vitest/browser-playwright': 4.1.10 '@vitest/browser-webdriverio': 4.1.10 @@ -9608,7 +8905,6 @@ packages: vitest@4.1.10: resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 @@ -9689,31 +8985,13 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-typed-array@1.1.22: - resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} - engines: {node: '>= 0.4'} - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} - hasBin: true why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} - hasBin: true word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} @@ -9772,7 +9050,6 @@ packages: yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} - hasBin: true yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} @@ -11957,148 +11234,6 @@ snapshots: '@preact/signals-core@1.14.3': {} - '@radix-ui/primitive@1.1.4': {} - - '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-context@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-dialog@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-context': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dismissable-layer': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-focus-scope': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-portal': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-presence': 1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7) - aria-hidden: 1.2.6 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-dismissable-layer@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/primitive': 1.1.4 - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-escape-keydown': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-focus-scope@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-portal@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-presence@1.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-primitive@2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-escape-keydown@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7) - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - - '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)': - dependencies: - react: 19.2.7 - optionalDependencies: - '@types/react': 19.2.17 - '@reactflow/background@11.3.14(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': dependencies: '@reactflow/core': 11.11.4(@types/react@19.2.17)(immer@11.1.11)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -13644,56 +12779,12 @@ snapshots: argparse@2.0.1: {} - aria-hidden@1.2.6: - dependencies: - tslib: 2.8.1 - aria-query@5.3.0: dependencies: dequal: 2.0.3 aria-query@5.3.2: {} - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - assertion-error-formatter@3.0.0: dependencies: diff: 4.0.4 @@ -13702,8 +12793,6 @@ snapshots: assertion-error@2.0.1: {} - ast-types-flow@0.0.8: {} - ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -13716,22 +12805,12 @@ snapshots: astring@1.9.0: {} - async-function@1.0.0: {} - async@3.2.6: {} - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - axe-core@4.12.1: {} - axobject-query@4.1.0: {} - bail@2.0.2: {} - balanced-match@1.0.2: {} - balanced-match@4.0.4: {} base64-arraybuffer@1.0.2: {} @@ -13756,11 +12835,6 @@ snapshots: boolbase@1.0.0: {} - brace-expansion@1.1.16: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 @@ -13833,23 +12907,6 @@ snapshots: cached-factory@0.3.0: {} - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - camelize@1.0.1: {} caniuse-lite@1.0.30001799: {} @@ -13966,18 +13023,6 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): - dependencies: - '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-dialog': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7) - '@radix-ui/react-primitive': 2.1.6(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - transitivePeerDependencies: - - '@types/react' - - '@types/react-dom' - code-inspector-plugin@1.6.6: dependencies: '@code-inspector/core': 1.6.6 @@ -14018,8 +13063,6 @@ snapshots: compare-versions@6.1.1: {} - concat-map@0.0.1: {} - concurrently@10.0.3: dependencies: chalk: 5.6.2 @@ -14291,26 +13334,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.18.1 - damerau-levenshtein@1.0.8: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - dayjs@1.11.21: {} debug@4.4.3(supports-color@10.2.2): @@ -14344,20 +13367,8 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - define-lazy-prop@3.0.0: {} - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - defu@6.1.7: {} delaunator@5.1.0: @@ -14372,8 +13383,6 @@ snapshots: detect-libc@2.1.2: {} - detect-node-es@1.1.0: {} - devframe@0.5.4(@typescript/typescript6@6.0.2)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1): dependencies: '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(@typescript/typescript6@6.0.2)) @@ -14442,12 +13451,6 @@ snapshots: dotenv@17.4.2: {} - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - echarts-for-react@3.0.6(echarts@6.1.0)(react@19.2.7): dependencies: echarts: 6.1.0 @@ -14488,8 +13491,6 @@ snapshots: emoji-regex-xs@2.0.1: {} - emoji-regex@9.2.2: {} - empathic@2.0.1: {} encoding-sniffer@0.2.1: @@ -14539,70 +13540,6 @@ snapshots: dependencies: stackframe: 1.3.4 - es-abstract-get@1.0.0: - dependencies: - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - is-callable: 1.2.7 - object-inspect: 1.13.4 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.4 - function.prototype.name: 1.2.0 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.11 - string.prototype.trimend: 1.0.10 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.8 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.22 - es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -14611,30 +13548,6 @@ snapshots: es-module-lexer@2.1.0: {} - es-object-atoms@1.1.2: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.4 - - es-to-primitive@1.3.4: - dependencies: - es-abstract-get: 1.0.0 - es-define-property: 1.0.1 - es-errors: 1.3.0 - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - es-toolkit@1.49.0: {} esast-util-from-estree@2.0.0: @@ -15182,10 +14095,6 @@ snapshots: flatted@3.4.2: {} - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - format@0.2.2: {} formatly@0.3.0: @@ -15219,64 +14128,20 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - function-timeout@1.0.2: {} - function.prototype.name@1.2.0: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - es-define-property: 1.0.1 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - hasown: 2.0.4 - is-callable: 1.2.7 - is-document.all: 1.0.0 - - functions-have-names@1.2.3: {} - fuse.js@7.4.2: {} - generator-function@2.0.1: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.4 - math-intrinsics: 1.1.0 - - get-nonce@1.0.1: {} - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.2 - get-stream@5.2.0: dependencies: pump: 3.0.4 - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -15310,15 +14175,8 @@ snapshots: globals@17.7.0: {} - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - globrex@0.1.2: {} - gopd@1.2.0: {} - graceful-fs@4.2.11: {} h3@2.0.1-rc.22: @@ -15345,28 +14203,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 - has-bigints@1.1.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - - hasown@2.0.4: - dependencies: - function-bind: 1.1.2 - hast-util-from-dom@5.0.1: dependencies: '@types/hast': 3.0.4 @@ -15602,12 +14440,6 @@ snapshots: inline-style-parser@0.2.7: {} - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.4 - side-channel: '@nolyfill/side-channel@1.0.44' - internmap@1.0.1: {} internmap@2.0.3: {} @@ -15623,68 +14455,16 @@ snapshots: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-builtin-module@5.0.0: dependencies: builtin-modules: 5.3.0 - is-callable@1.2.7: {} - - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-decimal@2.0.1: {} is-docker@3.0.0: {} - is-document.all@1.0.0: - dependencies: - call-bound: 1.0.4 - is-extglob@2.1.1: {} - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -15709,70 +14489,20 @@ snapshots: is-interactive@2.0.0: {} - is-map@2.0.3: {} - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-number@7.0.0: {} is-path-inside@4.0.0: {} is-plain-obj@4.1.0: {} - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.4 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - is-stream@4.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.22 - is-unicode-supported@2.1.0: {} - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 - isarray@2.0.5: {} - isexe@2.0.0: {} istanbul-lib-coverage@3.2.2: {} @@ -15862,13 +14592,6 @@ snapshots: jsonschema@1.5.0: {} - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - katex@0.16.47: dependencies: commander: 8.3.0 @@ -15907,12 +14630,6 @@ snapshots: ky@2.0.2: {} - language-subtag-registry@0.3.23: {} - - language-tags@1.0.9: - dependencies: - language-subtag-registry: 0.3.23 - launch-ide@1.4.5: dependencies: chalk: 4.1.2 @@ -16069,8 +14786,6 @@ snapshots: marked@17.0.6: {} - math-intrinsics@1.1.0: {} - mdast-util-directive@3.1.0: dependencies: '@types/mdast': 4.0.4 @@ -16622,10 +15337,6 @@ snapshots: dependencies: brace-expansion: 5.0.6 - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.16 - minimist@1.2.8: {} minipass@7.1.3: {} @@ -16782,33 +15493,6 @@ snapshots: object-deep-merge@2.0.1: {} - object-inspect@1.13.4: {} - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - obug@2.1.3: {} ohash@2.0.11: {} @@ -16867,12 +15551,6 @@ snapshots: stdin-discarder: 0.3.2 string-width: 8.2.1 - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - oxc-parser@0.127.0: dependencies: '@oxc-project/types': 0.127.0 @@ -16995,6 +15673,31 @@ snapshots: '@oxfmt/binding-win32-x64-msvc': 0.57.0 vite-plus: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + oxfmt@0.57.0(vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.57.0 + '@oxfmt/binding-android-arm64': 0.57.0 + '@oxfmt/binding-darwin-arm64': 0.57.0 + '@oxfmt/binding-darwin-x64': 0.57.0 + '@oxfmt/binding-freebsd-x64': 0.57.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.57.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.57.0 + '@oxfmt/binding-linux-arm64-gnu': 0.57.0 + '@oxfmt/binding-linux-arm64-musl': 0.57.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.57.0 + '@oxfmt/binding-linux-riscv64-musl': 0.57.0 + '@oxfmt/binding-linux-s390x-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-gnu': 0.57.0 + '@oxfmt/binding-linux-x64-musl': 0.57.0 + '@oxfmt/binding-openharmony-arm64': 0.57.0 + '@oxfmt/binding-win32-arm64-msvc': 0.57.0 + '@oxfmt/binding-win32-ia32-msvc': 0.57.0 + '@oxfmt/binding-win32-x64-msvc': 0.57.0 + vite-plus: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + oxfmt@0.57.0(vite-plus@0.2.4(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 @@ -17078,6 +15781,30 @@ snapshots: oxlint-tsgolint: 0.24.0 vite-plus: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): + optionalDependencies: + '@oxlint/binding-android-arm-eabi': 1.72.0 + '@oxlint/binding-android-arm64': 1.72.0 + '@oxlint/binding-darwin-arm64': 1.72.0 + '@oxlint/binding-darwin-x64': 1.72.0 + '@oxlint/binding-freebsd-x64': 1.72.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.72.0 + '@oxlint/binding-linux-arm-musleabihf': 1.72.0 + '@oxlint/binding-linux-arm64-gnu': 1.72.0 + '@oxlint/binding-linux-arm64-musl': 1.72.0 + '@oxlint/binding-linux-ppc64-gnu': 1.72.0 + '@oxlint/binding-linux-riscv64-gnu': 1.72.0 + '@oxlint/binding-linux-riscv64-musl': 1.72.0 + '@oxlint/binding-linux-s390x-gnu': 1.72.0 + '@oxlint/binding-linux-x64-gnu': 1.72.0 + '@oxlint/binding-linux-x64-musl': 1.72.0 + '@oxlint/binding-openharmony-arm64': 1.72.0 + '@oxlint/binding-win32-arm64-msvc': 1.72.0 + '@oxlint/binding-win32-ia32-msvc': 1.72.0 + '@oxlint/binding-win32-x64-msvc': 1.72.0 + oxlint-tsgolint: 0.24.0 + vite-plus: 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.72.0 @@ -17275,8 +16002,6 @@ snapshots: transitivePeerDependencies: - supports-color - possible-typed-array-names@1.1.0: {} - postcss-selector-parser@6.1.4: dependencies: cssesc: 3.0.0 @@ -17435,25 +16160,6 @@ snapshots: react-rnd: 10.5.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) ts-debounce: 4.0.0 - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7) - tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7) - optionalDependencies: - '@types/react': 19.2.17 - react-rnd@10.5.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7): dependencies: re-resizable: 6.11.2(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -17479,14 +16185,6 @@ snapshots: sortablejs: 1.15.7 tiny-invariant: 1.2.0 - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - get-nonce: 1.0.1 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - react-textarea-autosize@8.5.9(@types/react@19.2.17)(react@19.2.7): dependencies: '@babel/runtime': 7.29.7 @@ -17583,17 +16281,6 @@ snapshots: reflect-metadata@0.2.2: {} - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -17615,15 +16302,6 @@ snapshots: regexp-tree@0.1.27: {} - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - regjsparser@0.13.2: dependencies: jsesc: 3.1.0 @@ -17775,30 +16453,11 @@ snapshots: dependencies: tslib: 2.8.1 - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-buffer@5.2.1: optional: true safe-json-stringify@1.2.0: {} - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - satori@0.16.0: dependencies: '@shuding/opentype.js': 1.4.0-beta.0 @@ -17837,28 +16496,6 @@ snapshots: server-only@0.0.1: {} - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.2 - sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -18035,11 +16672,6 @@ snapshots: stdin-discarder@0.3.2: {} - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - storybook@10.5.0(@types/react@19.2.17)(react@19.2.7)(vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)): dependencies: '@storybook/global': 5.0.0 @@ -18128,36 +16760,6 @@ snapshots: string.prototype.codepointat@0.2.1: {} - string.prototype.includes@2.0.1: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - - string.prototype.trim@1.2.11: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.2 - has-property-descriptors: 1.0.2 - safe-regex-test: 1.1.0 - - string.prototype.trimend@1.0.10: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.2 - string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 @@ -18378,39 +16980,6 @@ snapshots: dependencies: tagged-tag: 1.0.0 - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.8: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - typescript@6.0.3: {} typescript@7.0.2: @@ -18442,13 +17011,6 @@ snapshots: unbash@4.0.2: {} - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - undici-types@7.24.6: {} undici-types@8.3.0: {} @@ -18544,13 +17106,6 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - use-composed-ref@1.4.0(@types/react@19.2.17)(react@19.2.7): dependencies: react: 19.2.7 @@ -18575,14 +17130,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.17 - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7): - dependencies: - detect-node-es: 1.1.0 - react: 19.2.7 - tslib: 2.8.1 - optionalDependencies: - '@types/react': 19.2.17 - use-sync-external-store@1.6.0(react@19.2.7): dependencies: react: 19.2.7 @@ -18751,6 +17298,65 @@ snapshots: - vite - yaml + vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0): + dependencies: + '@oxc-project/types': 0.138.0 + '@oxlint/plugins': 1.68.0 + '@vitest/browser': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + '@voidzero-dev/vite-plus-core': 0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0) + oxfmt: 0.57.0(vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.4(@types/node@25.9.5)(@typescript/typescript6@6.0.2)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + oxlint-tsgolint: 0.24.0 + vitest: 4.1.10(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(happy-dom@20.10.6) + optionalDependencies: + '@vitest/browser-playwright': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(playwright@1.61.1)(vitest@4.1.10) + '@voidzero-dev/vite-plus-darwin-arm64': 0.2.4 + '@voidzero-dev/vite-plus-darwin-x64': 0.2.4 + '@voidzero-dev/vite-plus-linux-arm64-gnu': 0.2.4 + '@voidzero-dev/vite-plus-linux-arm64-musl': 0.2.4 + '@voidzero-dev/vite-plus-linux-x64-gnu': 0.2.4 + '@voidzero-dev/vite-plus-linux-x64-musl': 0.2.4 + '@voidzero-dev/vite-plus-win32-arm64-msvc': 0.2.4 + '@voidzero-dev/vite-plus-win32-x64-msvc': 0.2.4 + transitivePeerDependencies: + - '@arethetypeswrong/core' + - '@edge-runtime/vm' + - '@opentelemetry/api' + - '@types/node' + - '@vitejs/devtools' + - '@vitest/coverage-istanbul' + - '@vitest/coverage-v8' + - '@vitest/ui' + - bufferutil + - esbuild + - happy-dom + - jiti + - jsdom + - less + - msw + - publint + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - terser + - tsx + - typescript + - unplugin-unused + - unrun + - utf-8-validate + - vite + - yaml + vite-plus@0.2.4(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@25.9.5)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0))(esbuild@0.28.1)(happy-dom@20.10.6)(jiti@2.7.0)(tsx@4.23.0)(typescript@6.0.3)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.138.0 @@ -18961,6 +17567,37 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.10(@types/node@25.9.5)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(happy-dom@20.10.6): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: '@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0)' + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.9.5 + '@vitest/browser-playwright': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(playwright@1.61.1)(vitest@4.1.10) + '@vitest/browser-preview': 4.1.10(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + happy-dom: 20.10.6 + transitivePeerDependencies: + - msw + vitest@4.1.10(@types/node@26.0.1)(@vitest/browser-playwright@4.1.10)(@vitest/browser-preview@4.1.10)(@vitest/coverage-v8@4.1.10)(@voidzero-dev/vite-plus-core@0.2.4(@types/node@26.0.1)(@typescript/typescript6@6.0.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.0)(yaml@2.9.0))(happy-dom@20.10.6): dependencies: '@vitest/expect': 4.1.10 @@ -19018,47 +17655,6 @@ snapshots: whatwg-mimetype@4.0.0: {} - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.2.0 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.22 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-typed-array@1.1.22: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -19257,7 +17853,6 @@ time: class-variance-authority@0.7.1: '2024-11-26T08:20:34.604Z' cli-table3@0.6.5: '2024-05-12T16:36:50.079Z' clsx@2.1.1: '2024-04-23T05:26:04.645Z' - cmdk@1.1.1: '2025-03-14T19:21:16.194Z' code-inspector-plugin@1.6.6: '2026-07-07T04:00:26.523Z' concurrently@10.0.3: '2026-06-02T04:31:54.180Z' copy-to-clipboard@4.0.2: '2026-04-24T22:15:18.933Z' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2ea335f5f65..ad59bbb6153 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -142,7 +142,6 @@ catalog: class-variance-authority: 0.7.1 cli-table3: 0.6.5 clsx: 2.1.1 - cmdk: 1.1.1 code-inspector-plugin: 1.6.6 concurrently: ^10.0.3 copy-to-clipboard: 4.0.2 diff --git a/web/__tests__/goto-anything/command-selector.test.tsx b/web/__tests__/goto-anything/command-selector.test.tsx deleted file mode 100644 index 2bf1798d8e8..00000000000 --- a/web/__tests__/goto-anything/command-selector.test.tsx +++ /dev/null @@ -1,310 +0,0 @@ -import type { ActionItem } from '../../app/components/goto-anything/actions/types' -import { fireEvent, render, screen } from '@testing-library/react' -import * as React from 'react' -import CommandSelector from '../../app/components/goto-anything/command-selector' - -vi.mock('cmdk', () => ({ - Command: { - Group: ({ children, className }: any) =>
{children}
, - Item: ({ children, onSelect, value, className }: any) => ( -
onSelect?.()} - data-value={value} - data-testid={`command-item-${value}`} - > - {children} -
- ), - }, -})) - -describe('CommandSelector', () => { - const mockActions: Record = { - app: { - key: '@app', - shortcut: '@app', - title: 'Search Applications', - description: 'Search apps', - search: vi.fn(), - }, - knowledge: { - key: '@knowledge', - shortcut: '@kb', - title: 'Search Knowledge', - description: 'Search knowledge bases', - search: vi.fn(), - }, - plugin: { - key: '@plugin', - shortcut: '@plugin', - title: 'Search Plugins', - description: 'Search plugins', - search: vi.fn(), - }, - node: { - key: '@node', - shortcut: '@node', - title: 'Search Nodes', - description: 'Search workflow nodes', - search: vi.fn(), - }, - } - - const mockOnCommandSelect = vi.fn() - const mockOnCommandValueChange = vi.fn() - - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Basic Rendering', () => { - it('should render all actions when no filter is provided', () => { - render() - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@node')).toBeInTheDocument() - }) - - it('should render empty filter as showing all actions', () => { - render( - , - ) - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@node')).toBeInTheDocument() - }) - }) - - describe('Filtering Functionality', () => { - it('should filter actions based on searchFilter - single match', () => { - render( - , - ) - - expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument() - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument() - }) - - it('should filter actions with multiple matches', () => { - render( - , - ) - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument() - expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument() - expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument() - }) - - it('should be case-insensitive when filtering', () => { - render( - , - ) - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument() - }) - - it('should match partial strings', () => { - render( - , - ) - - expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument() - expect(screen.getByTestId('command-item-@node')).toBeInTheDocument() - }) - }) - - describe('Empty State', () => { - it('should show empty state when no matches found', () => { - render( - , - ) - - expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@kb')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@plugin')).not.toBeInTheDocument() - expect(screen.queryByTestId('command-item-@node')).not.toBeInTheDocument() - - expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument() - expect(screen.getByText('app.gotoAnything.tryDifferentSearch')).toBeInTheDocument() - }) - - it('should not show empty state when filter is empty', () => { - render( - , - ) - - expect(screen.queryByText('app.gotoAnything.noMatchingCommands')).not.toBeInTheDocument() - }) - }) - - describe('Selection and Highlight Management', () => { - it('should call onCommandValueChange when filter changes and first item differs', () => { - const { rerender } = render( - , - ) - - rerender( - , - ) - - expect(mockOnCommandValueChange).toHaveBeenCalledWith('@kb') - }) - - it('should not call onCommandValueChange if current value still exists', () => { - const { rerender } = render( - , - ) - - rerender( - , - ) - - expect(mockOnCommandValueChange).not.toHaveBeenCalled() - }) - - it('should handle onCommandSelect callback correctly', () => { - render( - , - ) - - const knowledgeItem = screen.getByTestId('command-item-@kb') - fireEvent.click(knowledgeItem) - - expect(mockOnCommandSelect).toHaveBeenCalledWith('@kb') - }) - }) - - describe('Edge Cases', () => { - it('should handle empty actions object', () => { - render() - - expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument() - }) - - it('should handle special characters in filter', () => { - render( - , - ) - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@node')).toBeInTheDocument() - }) - - it('should handle undefined onCommandValueChange gracefully', () => { - const { rerender } = render( - , - ) - - expect(() => { - rerender( - , - ) - }).not.toThrow() - }) - }) - - describe('Backward Compatibility', () => { - it('should work without searchFilter prop (backward compatible)', () => { - render() - - expect(screen.getByTestId('command-item-@app')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@plugin')).toBeInTheDocument() - expect(screen.getByTestId('command-item-@node')).toBeInTheDocument() - }) - - it('should work without commandValue and onCommandValueChange props', () => { - render( - , - ) - - expect(screen.getByTestId('command-item-@kb')).toBeInTheDocument() - expect(screen.queryByTestId('command-item-@app')).not.toBeInTheDocument() - }) - }) -}) diff --git a/web/__tests__/goto-anything/match-action.test.ts b/web/__tests__/goto-anything/match-action.test.ts deleted file mode 100644 index 297f5d40fda..00000000000 --- a/web/__tests__/goto-anything/match-action.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -import type { Mock } from 'vitest' -import type { ActionItem } from '../../app/components/goto-anything/actions/types' -// Import after mocking to get mocked version -import { matchAction } from '../../app/components/goto-anything/actions' -import { slashCommandRegistry } from '../../app/components/goto-anything/actions/commands/registry' - -// Mock the entire actions module to avoid import issues -vi.mock('../../app/components/goto-anything/actions', () => ({ - matchAction: vi.fn(), -})) - -vi.mock('../../app/components/goto-anything/actions/commands/registry') - -// Implement the actual matchAction logic for testing -const actualMatchAction = (query: string, actions: Record) => { - const result = Object.values(actions).find((action) => { - // Special handling for slash commands - if (action.key === '/') { - // Get all registered commands from the registry - const allCommands = slashCommandRegistry.getAllCommands() - - // Check if query matches any registered command - return allCommands.some((cmd) => { - const cmdPattern = `/${cmd.name}` - - // For direct mode commands, don't match (keep in command selector) - if (cmd.mode === 'direct') return false - - // For submenu mode commands, match when complete command is entered - return query === cmdPattern || query.startsWith(`${cmdPattern} `) - }) - } - - const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`) - return reg.test(query) - }) - return result -} - -// Replace mock with actual implementation -;(matchAction as Mock).mockImplementation(actualMatchAction) - -describe('matchAction Logic', () => { - const mockActions: Record = { - app: { - key: '@app', - shortcut: '@a', - title: 'Search Applications', - description: 'Search apps', - search: vi.fn(), - }, - knowledge: { - key: '@knowledge', - shortcut: '@kb', - title: 'Search Knowledge', - description: 'Search knowledge bases', - search: vi.fn(), - }, - slash: { - key: '/', - shortcut: '/', - title: 'Commands', - description: 'Execute commands', - search: vi.fn(), - }, - } - - beforeEach(() => { - vi.clearAllMocks() - ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([ - { name: 'docs', mode: 'direct' }, - { name: 'community', mode: 'direct' }, - { name: 'feedback', mode: 'direct' }, - { name: 'account', mode: 'direct' }, - { name: 'theme', mode: 'submenu' }, - { name: 'language', mode: 'submenu' }, - ]) - }) - - describe('@ Actions Matching', () => { - it('should match @app with key', () => { - const result = matchAction('@app', mockActions) - expect(result).toBe(mockActions.app) - }) - - it('should match @app with shortcut', () => { - const result = matchAction('@a', mockActions) - expect(result).toBe(mockActions.app) - }) - - it('should match @knowledge with key', () => { - const result = matchAction('@knowledge', mockActions) - expect(result).toBe(mockActions.knowledge) - }) - - it('should match @knowledge with shortcut @kb', () => { - const result = matchAction('@kb', mockActions) - expect(result).toBe(mockActions.knowledge) - }) - - it('should match with text after action', () => { - const result = matchAction('@app search term', mockActions) - expect(result).toBe(mockActions.app) - }) - - it('should not match partial @ actions', () => { - const result = matchAction('@ap', mockActions) - expect(result).toBeUndefined() - }) - }) - - describe('Slash Commands Matching', () => { - describe('Direct Mode Commands', () => { - it('should not match direct mode commands', () => { - const result = matchAction('/docs', mockActions) - expect(result).toBeUndefined() - }) - - it('should not match direct mode with arguments', () => { - const result = matchAction('/docs something', mockActions) - expect(result).toBeUndefined() - }) - - it('should not match any direct mode command', () => { - expect(matchAction('/community', mockActions)).toBeUndefined() - expect(matchAction('/feedback', mockActions)).toBeUndefined() - expect(matchAction('/account', mockActions)).toBeUndefined() - }) - }) - - describe('Submenu Mode Commands', () => { - it('should match submenu mode commands exactly', () => { - const result = matchAction('/theme', mockActions) - expect(result).toBe(mockActions.slash) - }) - - it('should match submenu mode with arguments', () => { - const result = matchAction('/theme dark', mockActions) - expect(result).toBe(mockActions.slash) - }) - - it('should match all submenu commands', () => { - expect(matchAction('/language', mockActions)).toBe(mockActions.slash) - expect(matchAction('/language en', mockActions)).toBe(mockActions.slash) - }) - }) - - describe('Slash Without Command', () => { - it('should not match single slash', () => { - const result = matchAction('/', mockActions) - expect(result).toBeUndefined() - }) - - it('should not match unregistered commands', () => { - const result = matchAction('/unknown', mockActions) - expect(result).toBeUndefined() - }) - }) - }) - - describe('Edge Cases', () => { - it('should handle empty query', () => { - const result = matchAction('', mockActions) - expect(result).toBeUndefined() - }) - - it('should handle whitespace only', () => { - const result = matchAction(' ', mockActions) - expect(result).toBeUndefined() - }) - - it('should handle regular text without actions', () => { - const result = matchAction('search something', mockActions) - expect(result).toBeUndefined() - }) - - it('should handle special characters', () => { - const result = matchAction('#tag', mockActions) - expect(result).toBeUndefined() - }) - - it('should handle multiple @ or /', () => { - expect(matchAction('@@app', mockActions)).toBeUndefined() - expect(matchAction('//theme', mockActions)).toBeUndefined() - }) - }) - - describe('Mode-based Filtering', () => { - it('should filter direct mode commands from matching', () => { - ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([ - { name: 'test', mode: 'direct' }, - ]) - - const result = matchAction('/test', mockActions) - expect(result).toBeUndefined() - }) - - it('should allow submenu mode commands to match', () => { - ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([ - { name: 'test', mode: 'submenu' }, - ]) - - const result = matchAction('/test', mockActions) - expect(result).toBe(mockActions.slash) - }) - - it('should treat undefined mode as submenu', () => { - ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([ - { name: 'test' }, // No mode specified - ]) - - const result = matchAction('/test', mockActions) - expect(result).toBe(mockActions.slash) - }) - }) - - describe('Registry Integration', () => { - it('should call getAllCommands when matching slash', () => { - matchAction('/theme', mockActions) - expect(slashCommandRegistry.getAllCommands).toHaveBeenCalled() - }) - - it('should not call getAllCommands for @ actions', () => { - matchAction('@app', mockActions) - expect(slashCommandRegistry.getAllCommands).not.toHaveBeenCalled() - }) - - it('should handle empty command list', () => { - ;(slashCommandRegistry.getAllCommands as Mock).mockReturnValue([]) - const result = matchAction('/anything', mockActions) - expect(result).toBeUndefined() - }) - }) -}) diff --git a/web/__tests__/goto-anything/search-error-handling.test.ts b/web/__tests__/goto-anything/search-error-handling.test.ts deleted file mode 100644 index 584614c6d69..00000000000 --- a/web/__tests__/goto-anything/search-error-handling.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import type { MockedFunction } from 'vitest' -/** - * Test GotoAnything search error handling mechanisms - * - * Main validations: - * 1. @plugin search error handling when API fails - * 2. Regular search (without @prefix) error handling when API fails - * 3. Verify consistent error handling across different search types - * 4. Ensure errors don't propagate to UI layer causing "search failed" - */ -import { Actions, searchAnything } from '@/app/components/goto-anything/actions' -import { fetchAppList } from '@/service/apps' -import { postMarketplace } from '@/service/base' -import { fetchDatasets } from '@/service/datasets' - -// Mock API functions -vi.mock('@/service/base', () => ({ - postMarketplace: vi.fn(), -})) - -vi.mock('@/service/apps', () => ({ - fetchAppList: vi.fn(), -})) - -vi.mock('@/service/datasets', () => ({ - fetchDatasets: vi.fn(), -})) - -const mockPostMarketplace = postMarketplace as MockedFunction -const mockFetchAppList = fetchAppList as MockedFunction -const mockFetchDatasets = fetchDatasets as MockedFunction - -describe('GotoAnything Search Error Handling', () => { - beforeEach(() => { - vi.clearAllMocks() - // Suppress console.warn for clean test output - vi.spyOn(console, 'warn').mockImplementation(() => { - // Suppress console.warn for clean test output - }) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('@plugin search error handling', () => { - it('should return empty array when API fails instead of throwing error', async () => { - // Mock marketplace API failure (403 permission denied) - mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden')) - - const pluginAction = Actions.plugin - - // Directly call plugin action's search method - const result = await pluginAction.search('@plugin', 'test', 'en') - - // Should return empty array instead of throwing error - expect(result).toEqual([]) - expect(mockPostMarketplace).toHaveBeenCalledWith('/plugins/search/advanced', { - body: { - page: 1, - page_size: 10, - query: 'test', - type: 'plugin', - }, - }) - }) - - it('should return empty array when user has no plugin data', async () => { - // Mock marketplace returning empty data - mockPostMarketplace.mockResolvedValue({ - data: { plugins: [] }, - }) - - const pluginAction = Actions.plugin - const result = await pluginAction.search('@plugin', '', 'en') - - expect(result).toEqual([]) - }) - - it('should return empty array when API returns unexpected data structure', async () => { - // Mock API returning unexpected data structure - mockPostMarketplace.mockResolvedValue({ - data: null, - }) - - const pluginAction = Actions.plugin - const result = await pluginAction.search('@plugin', 'test', 'en') - - expect(result).toEqual([]) - }) - }) - - describe('Other search types error handling', () => { - it('@app search should return empty array when API fails', async () => { - // Mock app API failure - mockFetchAppList.mockRejectedValue(new Error('API Error')) - - const appAction = Actions.app - const result = await appAction.search('@app', 'test', 'en') - - expect(result).toEqual([]) - }) - - it('@knowledge search should return empty array when API fails', async () => { - // Mock knowledge API failure - mockFetchDatasets.mockRejectedValue(new Error('API Error')) - - const knowledgeAction = Actions.knowledge - const result = await knowledgeAction.search('@knowledge', 'test', 'en') - - expect(result).toEqual([]) - }) - }) - - describe('Unified search entry error handling', () => { - it('regular search (without @prefix) should return successful results even when partial APIs fail', async () => { - // Set app and knowledge success, plugin failure - mockFetchAppList.mockResolvedValue({ - data: [], - has_more: false, - limit: 10, - page: 1, - total: 0, - }) - mockFetchDatasets.mockResolvedValue({ - data: [], - has_more: false, - limit: 10, - page: 1, - total: 0, - }) - mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed')) - - const result = await searchAnything('en', 'test') - - // Should return successful results even if plugin search fails - expect(result).toEqual([]) - expect(console.warn).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error)) - }) - - it('@plugin dedicated search should return empty array when API fails', async () => { - // Mock plugin API failure - mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable')) - - const pluginAction = Actions.plugin - const result = await searchAnything('en', '@plugin test', pluginAction) - - // Should return empty array instead of throwing error - expect(result).toEqual([]) - }) - - it('@app dedicated search should return empty array when API fails', async () => { - // Mock app API failure - mockFetchAppList.mockRejectedValue(new Error('App service unavailable')) - - const appAction = Actions.app - const result = await searchAnything('en', '@app test', appAction) - - expect(result).toEqual([]) - }) - }) - - describe('Error handling consistency validation', () => { - it('all search types should return empty array when encountering errors', async () => { - // Mock all APIs to fail - mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed')) - mockFetchAppList.mockRejectedValue(new Error('App API failed')) - mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed')) - - const actions = [ - { name: '@plugin', action: Actions.plugin }, - { name: '@app', action: Actions.app }, - { name: '@knowledge', action: Actions.knowledge }, - ] - - for (const { name, action } of actions) { - const result = await action.search(name, 'test', 'en') - expect(result).toEqual([]) - } - }) - }) - - describe('Edge case testing', () => { - it('empty search term should be handled properly', async () => { - mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } }) - - const result = await searchAnything('en', '@plugin ', Actions.plugin) - expect(result).toEqual([]) - }) - - it('network timeout should be handled correctly', async () => { - const timeoutError = new Error('Network timeout') - timeoutError.name = 'TimeoutError' - - mockPostMarketplace.mockRejectedValue(timeoutError) - - const result = await searchAnything('en', '@plugin test', Actions.plugin) - expect(result).toEqual([]) - }) - - it('JSON parsing errors should be handled correctly', async () => { - const parseError = new SyntaxError('Unexpected token in JSON') - mockPostMarketplace.mockRejectedValue(parseError) - - const result = await searchAnything('en', '@plugin test', Actions.plugin) - expect(result).toEqual([]) - }) - }) -}) diff --git a/web/app/components/app-sidebar/__tests__/app-detail-top.spec.tsx b/web/app/components/app-sidebar/__tests__/app-detail-top.spec.tsx index 24f76bb2962..252bffd591c 100644 --- a/web/app/components/app-sidebar/__tests__/app-detail-top.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/app-detail-top.spec.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' +import { Dialog, DialogPopup, DialogPortal, DialogTitle } from '@langgenius/dify-ui/dialog' import { fireEvent, render, screen } from '@testing-library/react' -import { createStore, Provider as JotaiProvider } from 'jotai' -import { useGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import AppDetailTop from '../app-detail-top' vi.mock('../toggle-button', () => ({ @@ -26,54 +26,55 @@ vi.mock('../toggle-button', () => ({ ), })) -function GotoAnythingOpenProbe() { - const open = useGotoAnythingOpen() - - return
{String(open)}
-} - -const renderWithGotoAnythingStore = (ui: ReactNode) => { - const store = createStore() - - return render({ui}) +function TestGotoAnythingDialog() { + return ( + + + + Goto Anything + + + + ) } describe('AppDetailTop', () => { beforeEach(() => { vi.clearAllMocks() + gotoAnythingDialogHandle.close() }) it('links the combined home control to home', () => { - renderWithGotoAnythingStore() + render() expect(screen.getByRole('link', { name: 'common.mainNav.home' })).toHaveAttribute('href', '/') expect(screen.queryByRole('button', { name: 'common.operation.back' })).not.toBeInTheDocument() }) it('links the Studio breadcrumb to the Studio page', () => { - renderWithGotoAnythingStore() + render() expect(screen.getByRole('link', { name: 'common.menus.apps' })).toHaveAttribute('href', '/apps') }) it('keeps the quick search action', () => { - renderWithGotoAnythingStore( + render( <> - + , ) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('false') + expect(screen.queryByRole('dialog', { name: 'Goto Anything' })).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'app.gotoAnything.searchTitle' })) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('true') + expect(screen.getByRole('dialog', { name: 'Goto Anything' })).toBeInTheDocument() }) it('renders the sidebar toggle action in the top right', () => { const onToggle = vi.fn() - renderWithGotoAnythingStore() + render() fireEvent.click(screen.getByTestId('toggle-button')) expect(screen.getByTestId('toggle-button')).toHaveAttribute('data-expand', 'true') diff --git a/web/app/components/app-sidebar/__tests__/dataset-detail-top.spec.tsx b/web/app/components/app-sidebar/__tests__/dataset-detail-top.spec.tsx index 7b013d54c38..c8d952df1e1 100644 --- a/web/app/components/app-sidebar/__tests__/dataset-detail-top.spec.tsx +++ b/web/app/components/app-sidebar/__tests__/dataset-detail-top.spec.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react' +import { Dialog, DialogPopup, DialogPortal, DialogTitle } from '@langgenius/dify-ui/dialog' import { fireEvent, render, screen } from '@testing-library/react' -import { createStore, Provider as JotaiProvider } from 'jotai' -import { useGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import DatasetDetailTop from '../dataset-detail-top' vi.mock('../toggle-button', () => ({ @@ -26,25 +26,26 @@ vi.mock('../toggle-button', () => ({ ), })) -function GotoAnythingOpenProbe() { - const open = useGotoAnythingOpen() - - return
{String(open)}
-} - -const renderWithGotoAnythingStore = (ui: ReactNode) => { - const store = createStore() - - return render({ui}) +function TestGotoAnythingDialog() { + return ( + + + + Goto Anything + + + + ) } describe('DatasetDetailTop', () => { beforeEach(() => { vi.clearAllMocks() + gotoAnythingDialogHandle.close() }) it('links the combined home control to home and labels the breadcrumb as datasets', () => { - renderWithGotoAnythingStore() + render() expect(screen.getByRole('link', { name: 'common.mainNav.home' })).toHaveAttribute('href', '/') expect(screen.getByRole('link', { name: 'common.menus.datasets' })).toHaveAttribute( @@ -55,23 +56,23 @@ describe('DatasetDetailTop', () => { }) it('keeps the quick search action', () => { - renderWithGotoAnythingStore( + render( <> - + , ) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('false') + expect(screen.queryByRole('dialog', { name: 'Goto Anything' })).not.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'app.gotoAnything.searchTitle' })) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('true') + expect(screen.getByRole('dialog', { name: 'Goto Anything' })).toBeInTheDocument() }) it('renders the sidebar toggle action in the top right', () => { const onToggle = vi.fn() - renderWithGotoAnythingStore() + render() fireEvent.click(screen.getByTestId('toggle-button')) expect(screen.getByTestId('toggle-button')).toHaveAttribute('data-expand', 'false') diff --git a/web/app/components/app-sidebar/app-detail-top.tsx b/web/app/components/app-sidebar/app-detail-top.tsx index 6968c0dc564..b7578a70ba3 100644 --- a/web/app/components/app-sidebar/app-detail-top.tsx +++ b/web/app/components/app-sidebar/app-detail-top.tsx @@ -1,11 +1,12 @@ 'use client' +import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { formatForDisplay } from '@tanstack/react-hotkeys' import { useTranslation } from 'react-i18next' import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon' -import { useSetGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import Link from '@/next/link' import ToggleButton from './toggle-button' @@ -18,7 +19,6 @@ const SEARCH_SHORTCUT = ['Mod', 'K'] const AppDetailTop = ({ expand = true, onToggle }: AppDetailTopProps) => { const { t } = useTranslation() - const setGotoAnythingOpen = useSetGotoAnythingOpen() if (!expand) { return ( @@ -62,14 +62,18 @@ const AppDetailTop = ({ expand = true, onToggle }: AppDetailTopProps) => { $['gotoAnything.searchTitle'], { ns: 'app' })} - className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary" - onClick={() => setGotoAnythingOpen(true)} - > - - + $['gotoAnything.searchTitle'], { ns: 'app' })} + className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + > + + + } + /> } /> { const { t } = useTranslation() - const setGotoAnythingOpen = useSetGotoAnythingOpen() if (!expand) { return ( @@ -62,14 +62,18 @@ const DatasetDetailTop = ({ expand = true, onToggle }: DatasetDetailTopProps) => $['gotoAnything.searchTitle'], { ns: 'app' })} - className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary" - onClick={() => setGotoAnythingOpen(true)} - > - - + $['gotoAnything.searchTitle'], { ns: 'app' })} + className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + > + + + } + /> } /> ({ - usePathname: () => '/app', -})) - -const slashCommandsMock = [ - { - name: 'docs', - description: 'Docs', - mode: 'direct', - isAvailable: () => true, - }, -] - -vi.mock('../actions/commands/registry', () => ({ - slashCommandRegistry: { - getAvailableCommands: () => slashCommandsMock, - }, -})) - -const createActions = (): Record => ({ - app: { - key: '@app', - shortcut: '@app', - title: 'Apps', - search: vi.fn(), - description: '', - } as ActionItem, - plugin: { - key: '@plugin', - shortcut: '@plugin', - title: 'Plugins', - search: vi.fn(), - description: '', - } as ActionItem, -}) - -describe('CommandSelector', () => { - it('should list contextual search actions and notify selection', async () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - const actionButton = screen.getByText('app.gotoAnything.actions.searchApplicationsDesc') - await userEvent.click(actionButton) - - expect(onSelect).toHaveBeenCalledWith('@app') - }) - - it('should render slash commands when query starts with slash', async () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - const slashItem = await screen.findByText('app.gotoAnything.actions.docDesc') - await userEvent.click(slashItem) - - expect(onSelect).toHaveBeenCalledWith('/docs') - }) - - it('should show all slash commands when no filter provided', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('/docs')).toBeInTheDocument() - }) - - it('should exclude slash action when in @ mode', () => { - const actions = { - ...createActions(), - slash: { - key: '/', - shortcut: '/', - title: 'Slash', - search: vi.fn(), - description: '', - } as ActionItem, - } - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('@app')).toBeInTheDocument() - expect(screen.queryByText('/')).not.toBeInTheDocument() - }) - - it('should show all actions when no filter in @ mode', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('@app')).toBeInTheDocument() - expect(screen.getByText('@plugin')).toBeInTheDocument() - }) - - it('should set default command value when items exist but value does not', () => { - const actions = createActions() - const onSelect = vi.fn() - const onCommandValueChange = vi.fn() - - render( - - - , - ) - - expect(onCommandValueChange).toHaveBeenCalledWith('@app') - }) - - it('should NOT set command value when value already exists in items', () => { - const actions = createActions() - const onSelect = vi.fn() - const onCommandValueChange = vi.fn() - - render( - - - , - ) - - expect(onCommandValueChange).not.toHaveBeenCalled() - }) - - it('should show no matching commands message when filter has no results', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument() - expect(screen.getByText('app.gotoAnything.tryDifferentSearch')).toBeInTheDocument() - }) - - it('should show no matching commands for slash mode with no results', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('app.gotoAnything.noMatchingCommands')).toBeInTheDocument() - }) - - it('should render description for @ commands', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('app.gotoAnything.actions.searchApplicationsDesc')).toBeInTheDocument() - expect(screen.getByText('app.gotoAnything.actions.searchPluginsDesc')).toBeInTheDocument() - }) - - it('should render group header for @ mode', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('app.gotoAnything.selectSearchType')).toBeInTheDocument() - }) - - it('should render group header for slash mode', () => { - const actions = createActions() - const onSelect = vi.fn() - - render( - - - , - ) - - expect(screen.getByText('app.gotoAnything.groups.commands')).toBeInTheDocument() - }) -}) diff --git a/web/app/components/goto-anything/__tests__/context.spec.tsx b/web/app/components/goto-anything/__tests__/context.spec.tsx deleted file mode 100644 index 5406c30e718..00000000000 --- a/web/app/components/goto-anything/__tests__/context.spec.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { render, screen, waitFor } from '@testing-library/react' -import * as React from 'react' -import { GotoAnythingProvider, useGotoAnythingContext } from '../context' - -let pathnameMock: string | null | undefined = '/' -vi.mock('@/next/navigation', () => ({ - usePathname: () => pathnameMock, -})) - -let isWorkflowPageMock = false -vi.mock('../../workflow/constants', () => ({ - isInWorkflowPage: () => isWorkflowPageMock, -})) - -const ContextConsumer = () => { - const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() - return ( -
- {String(isWorkflowPage)}|{String(isRagPipelinePage)} -
- ) -} - -describe('GotoAnythingProvider', () => { - beforeEach(() => { - isWorkflowPageMock = false - pathnameMock = '/' - }) - - it('should set workflow page flag when workflow path detected', async () => { - isWorkflowPageMock = true - pathnameMock = '/app/123/workflow' - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('true|false') - }) - }) - - it('should detect RAG pipeline path based on pathname', async () => { - pathnameMock = '/datasets/abc/pipeline' - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('false|true') - }) - }) - - it('should set both flags to false when pathname is null', async () => { - pathnameMock = null - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('false|false') - }) - }) - - it('should set both flags to false when pathname is undefined', async () => { - pathnameMock = undefined - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('false|false') - }) - }) - - it('should set both flags to false for regular paths', async () => { - pathnameMock = '/apps' - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('false|false') - }) - }) - - it('should NOT match non-pipeline dataset paths', async () => { - pathnameMock = '/datasets/abc/documents' - - render( - - - , - ) - - await waitFor(() => { - expect(screen.getByTestId('status')).toHaveTextContent('false|false') - }) - }) -}) - -describe('useGotoAnythingContext', () => { - it('should return default values when used outside provider', () => { - const TestComponent = () => { - const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() - return ( -
- {String(isWorkflowPage)}|{String(isRagPipelinePage)} -
- ) - } - - render() - - expect(screen.getByTestId('context')).toHaveTextContent('false|false') - }) -}) diff --git a/web/app/components/goto-anything/__tests__/index.spec.tsx b/web/app/components/goto-anything/__tests__/index.spec.tsx index 63c86c9e278..55258a96cb5 100644 --- a/web/app/components/goto-anything/__tests__/index.spec.tsx +++ b/web/app/components/goto-anything/__tests__/index.spec.tsx @@ -1,9 +1,11 @@ import type { ReactNode } from 'react' import type { ActionItem, SearchResult } from '../actions/types' -import { act, render, screen, waitFor } from '@testing-library/react' +import { DialogTrigger } from '@langgenius/dify-ui/dialog' +import { detectPlatform } from '@tanstack/react-hotkeys' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import { createStore, Provider } from 'jotai' import * as React from 'react' +import { gotoAnythingDialogHandle } from '../dialog-handle' import { GotoAnything } from '../index' type TestSearchResult = Omit & { @@ -19,57 +21,66 @@ vi.mock('@/next/navigation', () => ({ usePathname: () => '/', })) -type KeyPressEvent = { - preventDefault: () => void - target?: EventTarget -} - -type HotkeyRegistration = { - handler: (event: KeyPressEvent) => void - options?: { enabled?: boolean } -} - -const hotkeyHandlers: Record = {} +let debouncedSearchQuery: string | undefined vi.mock('ahooks', () => ({ - useDebounce: (value: T) => value, + useDebounce: (value: T) => (debouncedSearchQuery ?? value) as T, })) -vi.mock('@tanstack/react-hotkeys', async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - useHotkey: ( - hotkey: string, - handler: (event: KeyPressEvent) => void, - options?: HotkeyRegistration['options'], - ) => { - hotkeyHandlers[hotkey] = { handler, options } - }, - } -}) +const isMac = detectPlatform() === 'mac' -const HOTKEY_ALIAS: Record = { - 'ctrl.k': 'Mod+K', +function triggerSearchShortcut(target: Document | HTMLElement = document) { + fireEvent.keyDown(target, { + key: 'k', + ctrlKey: !isMac, + metaKey: isMac, + }) } -const triggerKeyPress = (combo: string) => { - const hotkey = HOTKEY_ALIAS[combo] ?? combo - const registration = hotkeyHandlers[hotkey] - if (registration && registration.options?.enabled !== false) { - act(() => { - registration.handler({ preventDefault: vi.fn(), target: document.body }) - }) - } +type RemoteQueryState = { + data: TestSearchResult[] + isLoading: boolean + isError: boolean + error: Error | null } -let mockQueryResult = { - data: [] as TestSearchResult[], +const emptyRemoteQueryState = (): RemoteQueryState => ({ + data: [], isLoading: false, isError: false, - error: null as Error | null, + error: null, +}) + +let remoteQueryStates: Record<'app' | 'knowledge' | 'plugin', RemoteQueryState> = { + app: emptyRemoteQueryState(), + knowledge: emptyRemoteQueryState(), + plugin: emptyRemoteQueryState(), } +let enabledRemoteQueryKeys: string[] = [] + +function setRemoteResults(results: TestSearchResult[]) { + results.forEach((result) => { + if (result.type === 'app' || result.type === 'knowledge' || result.type === 'plugin') + remoteQueryStates[result.type].data.push(result) + }) +} + vi.mock('@tanstack/react-query', () => ({ - useQuery: () => mockQueryResult, + useQuery: (options: { queryKey: [key: keyof typeof remoteQueryStates]; enabled?: boolean }) => { + if (options.enabled) enabledRemoteQueryKeys.push(options.queryKey[0]) + return options.enabled ? remoteQueryStates[options.queryKey[0]] : emptyRemoteQueryState() + }, +})) + +vi.mock('../actions/app', () => ({ + appSearchQueryOptions: () => ({ queryKey: ['app'] }), +})) + +vi.mock('../actions/knowledge', () => ({ + knowledgeSearchQueryOptions: () => ({ queryKey: ['knowledge'] }), +})) + +vi.mock('../actions/plugin', () => ({ + pluginSearchQueryOptions: () => ({ queryKey: ['plugin'] }), })) vi.mock( '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission', @@ -81,35 +92,38 @@ vi.mock( }), ) -const contextValue = { isWorkflowPage: false, isRagPipelinePage: false } -vi.mock('../context', () => ({ - useGotoAnythingContext: () => contextValue, - GotoAnythingProvider: ({ children }: { children: React.ReactNode }) => <>{children}, -})) - -const createActionItem = (key: ActionItem['key'], shortcut: string): ActionItem => ({ +const createRemoteAction = (key: ActionItem['key'], shortcut: string): ActionItem => ({ key, shortcut, title: `${key} title`, description: `${key} desc`, - action: vi.fn(), - search: vi.fn(), + source: 'remote', }) const actionsMock = { - slash: createActionItem('/', '/'), - app: createActionItem('@app', '@app'), - plugin: createActionItem('@plugin', '@plugin'), + slash: { + key: '/', + shortcut: '/', + title: '/ title', + description: '/ desc', + source: 'local', + action: vi.fn(), + search: vi.fn(() => []), + } satisfies ActionItem, + app: createRemoteAction('@app', '@app'), + knowledge: createRemoteAction('@knowledge', '@kb'), + plugin: createRemoteAction('@plugin', '@plugin'), } const createActionsMock = vi.fn(() => actionsMock) -const matchActionMock = vi.fn(() => undefined) -const searchAnythingMock = vi.fn(async () => mockQueryResult.data) - +const matchActionMock = vi.fn< + (query: string, actions: Record) => ActionItem | undefined +>(() => undefined) vi.mock('../actions', () => ({ createActions: () => createActionsMock(), - matchAction: () => matchActionMock(), - searchAnything: () => searchAnythingMock(), + getActionSearchTerm: (_query: string, action: ActionItem) => action.key, + matchAction: (query: string, actions: Record) => + matchActionMock(query, actions), })) vi.mock('../actions/commands/slash-provider', () => ({ @@ -123,10 +137,11 @@ type MockSlashCommand = { } | null let mockFindCommand: MockSlashCommand = null +let mockAvailableCommands: Array<{ name: string; description: string }> = [] vi.mock('../actions/commands/registry', () => ({ slashCommandRegistry: { findCommand: () => mockFindCommand, - getAvailableCommands: () => [], + getAvailableCommands: () => mockAvailableCommands, getAllCommands: () => [], }, })) @@ -153,40 +168,102 @@ vi.mock('../../plugins/install-plugin/install-from-marketplace', () => ({ ), })) -const renderGotoAnything = (ui: React.ReactElement) => { - const store = createStore() - - return render({ui}) -} +const renderGotoAnything = (ui: React.ReactElement) => render(ui) describe('GotoAnything', () => { beforeEach(() => { routerPush.mockClear() - Object.keys(hotkeyHandlers).forEach((key) => delete hotkeyHandlers[key]) - mockQueryResult = { data: [], isLoading: false, isError: false, error: null } + gotoAnythingDialogHandle.close() + remoteQueryStates = { + app: emptyRemoteQueryState(), + knowledge: emptyRemoteQueryState(), + plugin: emptyRemoteQueryState(), + } + debouncedSearchQuery = undefined + enabledRemoteQueryKeys = [] matchActionMock.mockReset() - searchAnythingMock.mockClear() mockFindCommand = null + mockAvailableCommands = [] }) describe('modal behavior', () => { it('should open modal via Ctrl+K shortcut', async () => { renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( - screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder'), + screen.getByRole('dialog', { name: 'app.gotoAnything.searchTitle' }), ).toBeInTheDocument() + expect(screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder')).toHaveFocus() }) }) + it('should not open from an unrelated editable field', () => { + renderGotoAnything( + <> + + + , + ) + + triggerSearchShortcut(screen.getByRole('textbox', { name: 'Unrelated field' })) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it.each(['shiftKey', 'altKey'] as const)('should ignore Mod+K with %s', (extraModifier) => { + renderGotoAnything() + + fireEvent.keyDown(document, { + key: 'k', + ctrlKey: !isMac, + metaKey: isMac, + [extraModifier]: true, + }) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('should ignore K with the non-primary platform modifier', () => { + renderGotoAnything() + + fireEvent.keyDown(document, { + key: 'k', + ctrlKey: isMac, + metaKey: !isMac, + }) + + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) + + it('should restore focus to the detached trigger after Escape', async () => { + const user = userEvent.setup() + renderGotoAnything( + <> + Search} + /> + + , + ) + const trigger = screen.getByRole('button', { name: 'Search' }) + + await user.click(trigger) + expect(screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder')).toHaveFocus() + + await user.keyboard('{Escape}') + + await waitFor(() => expect(trigger).toHaveFocus()) + }) + it('should close modal via ESC key', async () => { const user = userEvent.setup() renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder'), @@ -201,47 +278,25 @@ describe('GotoAnything', () => { }) }) - it('should toggle modal when pressing Ctrl+K twice', async () => { + it('should keep the modal open when pressing Ctrl+K again', async () => { + const user = userEvent.setup() renderGotoAnything() - triggerKeyPress('ctrl.k') - await waitFor(() => { - expect( - screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder'), - ).toBeInTheDocument() - }) + triggerSearchShortcut() + const input = await screen.findByPlaceholderText('app.gotoAnything.searchPlaceholder') + await user.type(input, 'workflow') - triggerKeyPress('ctrl.k') - await waitFor(() => { - expect( - screen.queryByPlaceholderText('app.gotoAnything.searchPlaceholder'), - ).not.toBeInTheDocument() - }) - }) + triggerSearchShortcut() - it('should call onHide when modal closes', async () => { - const user = userEvent.setup() - const onHide = vi.fn() - renderGotoAnything() - - triggerKeyPress('ctrl.k') - await waitFor(() => { - expect( - screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder'), - ).toBeInTheDocument() - }) - - await user.keyboard('{Escape}') - await waitFor(() => { - expect(onHide).toHaveBeenCalled() - }) + expect(input).toHaveValue('workflow') + expect(input).toHaveFocus() }) it('should reset search query when modal opens', async () => { const user = userEvent.setup() renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder'), @@ -258,7 +313,7 @@ describe('GotoAnything', () => { ).not.toBeInTheDocument() }) - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { const newInput = screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder') expect(newInput).toHaveValue('') @@ -269,25 +324,20 @@ describe('GotoAnything', () => { describe('search functionality', () => { it('should navigate to selected result', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'app-1', - type: 'app', - title: 'Sample App', - description: 'desc', - path: '/apps/1', - icon:
🧩
, - data: {}, - }, - ], - isLoading: false, - isError: false, - error: null, - } + setRemoteResults([ + { + id: 'app-1', + type: 'app', + title: 'Sample App', + description: 'desc', + path: '/apps/1', + icon:
🧩
, + data: {}, + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -302,12 +352,84 @@ describe('GotoAnything', () => { await user.click(result) expect(routerPush).toHaveBeenCalledWith('/apps/1') + expect(routerPush).toHaveBeenCalledTimes(1) + }) + + it('should navigate the highlighted result with ArrowDown and Enter', async () => { + const user = userEvent.setup() + setRemoteResults([ + { + id: 'app-1', + type: 'app', + title: 'Keyboard App', + path: '/apps/keyboard', + data: {}, + }, + ]) + + renderGotoAnything() + triggerSearchShortcut() + const input = await screen.findByRole('combobox', { + name: 'app.gotoAnything.searchTitle', + }) + + await user.type(input, 'keyboard') + await user.keyboard('{ArrowDown}{Enter}') + + expect(routerPush).toHaveBeenCalledWith('/apps/keyboard') + expect(routerPush).toHaveBeenCalledTimes(1) + }) + + it('should loop from the last command to the first with ArrowDown', async () => { + const user = userEvent.setup() + mockAvailableCommands = [ + { name: 'theme', description: 'Change theme' }, + { name: 'language', description: 'Change language' }, + ] + + renderGotoAnything() + triggerSearchShortcut() + const input = await screen.findByRole('combobox', { + name: 'app.gotoAnything.searchTitle', + }) + + await user.type(input, '/') + const options = screen.getAllByRole('option') + expect(options).toHaveLength(2) + const [firstOption, secondOption] = options + if (!firstOption || !secondOption) throw new Error('Expected two command options') + + await user.keyboard('{ArrowDown}') + expect(input).toHaveAttribute('aria-activedescendant', secondOption.id) + + await user.keyboard('{ArrowDown}') + expect(input).toHaveAttribute('aria-activedescendant', firstOption.id) + }) + + it('should announce the displayed command count', async () => { + const user = userEvent.setup() + mockAvailableCommands = [ + { name: 'theme', description: 'Change theme' }, + { name: 'language', description: 'Change language' }, + ] + + renderGotoAnything() + triggerSearchShortcut() + const input = await screen.findByRole('combobox', { + name: 'app.gotoAnything.searchTitle', + }) + + await user.type(input, '/') + + expect(screen.getByRole('status')).toHaveTextContent( + 'app.gotoAnything.resultCount:{"count":2}', + ) }) it('should clear selection when typing without prefix', async () => { const user = userEvent.setup() renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -320,20 +442,36 @@ describe('GotoAnything', () => { expect(input).toHaveValue('test query') }) + + it('should not search providers with a stale scope prefix', async () => { + const user = userEvent.setup() + matchActionMock.mockImplementation((query: string) => + query.startsWith('@app') ? actionsMock.app : undefined, + ) + + renderGotoAnything() + triggerSearchShortcut() + const input = await screen.findByRole('combobox', { + name: 'app.gotoAnything.searchTitle', + }) + + await user.type(input, '@') + debouncedSearchQuery = '@' + enabledRemoteQueryKeys = [] + await user.click(screen.getByText('@app')) + + expect(input).toHaveValue('@app ') + expect(enabledRemoteQueryKeys).toEqual([]) + }) }) describe('empty states', () => { it('should show loading state', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [], - isLoading: true, - isError: false, - error: null, - } + remoteQueryStates.app.isLoading = true renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -346,20 +484,21 @@ describe('GotoAnything', () => { const searchingTexts = screen.getAllByText('app.gotoAnything.searching') expect(searchingTexts.length).toBeGreaterThanOrEqual(1) + expect(screen.getByRole('status')).toHaveTextContent('app.gotoAnything.searching') + expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument() }) it('should show error state', async () => { const user = userEvent.setup() const testError = new Error('Search failed') - mockQueryResult = { - data: [], - isLoading: false, - isError: true, - error: testError, + remoteQueryStates = { + app: { data: [], isLoading: false, isError: true, error: testError }, + knowledge: { data: [], isLoading: false, isError: true, error: testError }, + plugin: { data: [], isLoading: false, isError: true, error: testError }, } renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -370,12 +509,47 @@ describe('GotoAnything', () => { const input = screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder') await user.type(input, 'search') - expect(screen.getByText('app.gotoAnything.searchFailed')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent('app.gotoAnything.searchFailed') + expect(screen.getAllByText('app.gotoAnything.searchFailed')).toHaveLength(2) + }) + + it('should preserve successful results when one provider fails', async () => { + const user = userEvent.setup() + setRemoteResults([ + { + id: 'app-1', + type: 'app', + title: 'Available App', + path: '/apps/available', + data: {}, + }, + ]) + remoteQueryStates.plugin = { + data: [], + isLoading: false, + isError: true, + error: new Error('Marketplace unavailable'), + } + + renderGotoAnything() + triggerSearchShortcut() + const input = await screen.findByRole('combobox', { + name: 'app.gotoAnything.searchTitle', + }) + + await user.type(input, 'available') + + expect(await screen.findByText('Available App')).toBeInTheDocument() + expect(screen.getByRole('status')).toHaveTextContent( + 'app.gotoAnything.someServicesUnavailable', + ) + expect(screen.getAllByText('app.gotoAnything.someServicesUnavailable')).toHaveLength(2) + expect(screen.queryByText('app.gotoAnything.searchFailed')).not.toBeInTheDocument() }) it('should show default state when no query', async () => { renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -383,20 +557,13 @@ describe('GotoAnything', () => { ).toBeInTheDocument() }) - expect(screen.getByText('app.gotoAnything.searchTitle')).toBeInTheDocument() + expect(screen.getAllByText('app.gotoAnything.searchTitle')).toHaveLength(2) }) it('should show no results state when search returns empty', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [], - isLoading: false, - isError: false, - error: null, - } - renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -407,35 +574,30 @@ describe('GotoAnything', () => { const input = screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder') await user.type(input, 'nonexistent') - expect(screen.getByText('app.gotoAnything.noResults')).toBeInTheDocument() + expect(await screen.findByText('app.gotoAnything.noResults')).toBeInTheDocument() }) }) describe('plugin installation', () => { it('should open plugin installer when selecting plugin result', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'plugin-1', - type: 'plugin', - title: 'Plugin Item', - description: 'desc', - path: '', - icon:
, - data: { - name: 'Plugin Item', - latest_package_identifier: 'pkg', - }, + setRemoteResults([ + { + id: 'plugin-1', + type: 'plugin', + title: 'Plugin Item', + description: 'desc', + path: '', + icon:
, + data: { + name: 'Plugin Item', + latest_package_identifier: 'pkg', }, - ], - isLoading: false, - isError: false, - error: null, - } + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -454,28 +616,23 @@ describe('GotoAnything', () => { it('should close plugin installer via close button', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'plugin-1', - type: 'plugin', - title: 'Plugin Item', - description: 'desc', - path: '', - icon:
, - data: { - name: 'Plugin Item', - latest_package_identifier: 'pkg', - }, + setRemoteResults([ + { + id: 'plugin-1', + type: 'plugin', + title: 'Plugin Item', + description: 'desc', + path: '', + icon:
, + data: { + name: 'Plugin Item', + latest_package_identifier: 'pkg', }, - ], - isLoading: false, - isError: false, - error: null, - } + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -499,28 +656,23 @@ describe('GotoAnything', () => { it('should close plugin installer on success', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'plugin-1', - type: 'plugin', - title: 'Plugin Item', - description: 'desc', - path: '', - icon:
, - data: { - name: 'Plugin Item', - latest_package_identifier: 'pkg', - }, + setRemoteResults([ + { + id: 'plugin-1', + type: 'plugin', + title: 'Plugin Item', + description: 'desc', + path: '', + icon:
, + data: { + name: 'Plugin Item', + latest_package_identifier: 'pkg', }, - ], - isLoading: false, - isError: false, - error: null, - } + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -552,9 +704,10 @@ describe('GotoAnything', () => { execute: executeMock, isAvailable: () => true, } + mockAvailableCommands = [{ name: 'theme', description: 'Change theme' }] renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -566,7 +719,7 @@ describe('GotoAnything', () => { await user.type(input, '/theme') await user.keyboard('{Enter}') - expect(executeMock).toHaveBeenCalled() + expect(executeMock).toHaveBeenCalledTimes(1) }) it('should NOT execute unavailable slash command', async () => { @@ -579,7 +732,7 @@ describe('GotoAnything', () => { } renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -601,9 +754,10 @@ describe('GotoAnything', () => { mode: 'submenu', execute: executeMock, } + mockAvailableCommands = [{ name: 'language', description: 'Change language' }] renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -625,9 +779,10 @@ describe('GotoAnything', () => { execute: vi.fn(), isAvailable: () => true, } + mockAvailableCommands = [{ name: 'theme', description: 'Change theme' }] renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -650,25 +805,20 @@ describe('GotoAnything', () => { describe('result navigation', () => { it('should handle knowledge result navigation', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'kb-1', - type: 'knowledge', - title: 'Knowledge Base', - description: 'desc', - path: '/datasets/kb-1', - icon:
, - data: {}, - }, - ], - isLoading: false, - isError: false, - error: null, - } + setRemoteResults([ + { + id: 'kb-1', + type: 'knowledge', + title: 'Knowledge Base', + description: 'desc', + path: '/datasets/kb-1', + icon:
, + data: {}, + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( @@ -687,25 +837,20 @@ describe('GotoAnything', () => { it('should NOT navigate when result has no path', async () => { const user = userEvent.setup() - mockQueryResult = { - data: [ - { - id: 'item-1', - type: 'app', - title: 'No Path Item', - description: 'desc', - path: '', - icon:
, - data: {}, - }, - ], - isLoading: false, - isError: false, - error: null, - } + setRemoteResults([ + { + id: 'item-1', + type: 'app', + title: 'No Path Item', + description: 'desc', + path: '', + icon:
, + data: {}, + }, + ]) renderGotoAnything() - triggerKeyPress('ctrl.k') + triggerSearchShortcut() await waitFor(() => { expect( diff --git a/web/app/components/goto-anything/actions/__tests__/app.spec.ts b/web/app/components/goto-anything/actions/__tests__/app.spec.ts index eb99f2812fc..f8fe65f1e62 100644 --- a/web/app/components/goto-anything/actions/__tests__/app.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/app.spec.ts @@ -1,159 +1,80 @@ -import type { App } from '@/types/app' -import { appAction } from '../app' +import { appAction, appSearchQueryOptions } from '../app' -vi.mock('@/service/apps', () => ({ - fetchAppList: vi.fn(), +const serviceMocks = vi.hoisted(() => ({ queryOptions: vi.fn((options) => options) })) + +vi.mock('@/service/client', () => ({ + consoleQuery: { apps: { get: { queryOptions: serviceMocks.queryOptions } } }, })) vi.mock('@/utils/app-redirection', () => ({ getRedirectionPath: vi.fn((app: { id: string }) => `/app/${app.id}`), })) -vi.mock('../../../app/type-selector', () => ({ - AppTypeIcon: () => null, -})) +vi.mock('../../../app/type-selector', () => ({ AppTypeIcon: () => null })) -describe('appAction', () => { - beforeEach(() => { - vi.clearAllMocks() +type AppQueryResponse = Parameters< + NonNullable['select']> +>[0] +type AppQueryItem = AppQueryResponse['data'][number] + +function app(overrides: Partial = {}): AppQueryItem { + return { + id: 'app-1', + name: 'My App', + description: 'A great app', + mode: 'chat', + icon: '', + icon_type: 'emoji', + icon_background: '', + icon_url: '', + ...overrides, + } as AppQueryItem +} + +function response(data: AppQueryItem[]): AppQueryResponse { + return { data, has_more: false, limit: 10, page: 1, total: data.length } +} + +describe('app search query', () => { + beforeEach(() => vi.clearAllMocks()) + + it('exposes remote action metadata', () => { + expect(appAction).toMatchObject({ key: '@app', shortcut: '@app', source: 'remote' }) }) - it('has correct metadata', () => { - expect(appAction.key).toBe('@app') - expect(appAction.shortcut).toBe('@app') + it('builds generated query options from the search term', () => { + appSearchQueryOptions('assistant', false) + + expect(serviceMocks.queryOptions).toHaveBeenCalledWith( + expect.objectContaining({ + input: { query: { page: 1, name: 'assistant' } }, + retry: false, + select: expect.any(Function), + }), + ) }) - it('returns parsed app results on success', async () => { - const { fetchAppList } = await import('@/service/apps') - vi.mocked(fetchAppList).mockResolvedValue({ - data: [ - { - id: 'app-1', - name: 'My App', - description: 'A great app', - mode: 'chat', - icon: '', - icon_type: 'emoji', - icon_background: '', - icon_url: '', - } as unknown as App, - ], - has_more: false, - limit: 10, - page: 1, - total: 1, - }) + it('selects plain app results for general search', () => { + const options = appSearchQueryOptions('my app', false) - const results = await appAction.search('@app test', 'test', 'en') + expect(options.select!(response([app(), app({ id: 'app-2', name: 'Other' })]))).toHaveLength(2) + }) - expect(fetchAppList).toHaveBeenCalledWith({ - url: 'apps', - params: { page: 1, name: 'test' }, - }) - expect(results).toHaveLength(5) - expect(results[0]).toMatchObject({ - id: 'app-1', - title: 'My App', - type: 'app', - }) - expect(results.slice(1).map((r) => r.id)).toEqual([ + it('selects app sections only for scoped search', () => { + const chatOptions = appSearchQueryOptions('', true) + const workflowOptions = appSearchQueryOptions('', true) + + expect(chatOptions.select!(response([app()])).map((result) => result.id)).toEqual([ + 'app-1', 'app-1:configuration', 'app-1:overview', 'app-1:logs', 'app-1:develop', ]) - }) - - it('returns workflow sub-sections for workflow-mode apps', async () => { - const { fetchAppList } = await import('@/service/apps') - vi.mocked(fetchAppList).mockResolvedValue({ - data: [ - { - id: 'wf-1', - name: 'Flow', - description: '', - mode: 'workflow', - icon: '', - icon_type: 'emoji', - icon_background: '', - icon_url: '', - } as unknown as App, - ], - has_more: false, - limit: 10, - page: 1, - total: 1, - }) - - const results = await appAction.search('@app', '', 'en') - - expect(results).toHaveLength(4) - expect(results.slice(1).map((r) => r.id)).toEqual([ - 'wf-1:workflow', - 'wf-1:overview', - 'wf-1:logs', - ]) - }) - - it('returns apps without sub-sections for unscoped queries', async () => { - const { fetchAppList } = await import('@/service/apps') - vi.mocked(fetchAppList).mockResolvedValue({ - data: [ - { - id: 'app-1', - name: 'My App', - description: '', - mode: 'chat', - icon: '', - icon_type: 'emoji', - icon_background: '', - icon_url: '', - } as unknown as App, - { - id: 'app-2', - name: 'Other', - description: '', - mode: 'chat', - icon: '', - icon_type: 'emoji', - icon_background: '', - icon_url: '', - } as unknown as App, - ], - has_more: false, - limit: 10, - page: 1, - total: 2, - }) - - const results = await appAction.search('my app', 'my app', 'en') - - expect(results).toHaveLength(2) - expect(results.map((r) => r.id)).toEqual(['app-1', 'app-2']) - }) - - it('returns empty array when response has no data', async () => { - const { fetchAppList } = await import('@/service/apps') - vi.mocked(fetchAppList).mockResolvedValue({ - data: [], - has_more: false, - limit: 10, - page: 1, - total: 0, - }) - - const results = await appAction.search('@app', '', 'en') - expect(results).toEqual([]) - }) - - it('returns empty array on API failure', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { fetchAppList } = await import('@/service/apps') - vi.mocked(fetchAppList).mockRejectedValue(new Error('network error')) - - const results = await appAction.search('@app fail', 'fail', 'en') - expect(results).toEqual([]) - expect(warnSpy).toHaveBeenCalledWith('App search failed:', expect.any(Error)) - warnSpy.mockRestore() + expect( + workflowOptions.select!(response([app({ id: 'wf-1', mode: 'workflow' })])).map( + (result) => result.id, + ), + ).toEqual(['wf-1', 'wf-1:workflow', 'wf-1:overview', 'wf-1:logs']) }) }) diff --git a/web/app/components/goto-anything/actions/__tests__/index.spec.ts b/web/app/components/goto-anything/actions/__tests__/index.spec.ts index 9010ba047ea..f9f57432cf8 100644 --- a/web/app/components/goto-anything/actions/__tests__/index.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/index.spec.ts @@ -1,8 +1,6 @@ -import type { ActionItem, SearchResult } from '../types' -import type { DataSet } from '@/models/datasets' -import type { App } from '@/types/app' +import type { ActionItem } from '../types' import { slashCommandRegistry } from '../commands/registry' -import { createActions, matchAction, searchAnything } from '../index' +import { createActions, getActionSearchTerm, matchAction } from '../index' vi.mock('../app', () => ({ appAction: { @@ -10,7 +8,7 @@ vi.mock('../app', () => ({ shortcut: '@app', title: 'Apps', description: 'Search apps', - search: vi.fn().mockResolvedValue([]), + source: 'remote', } satisfies ActionItem, })) @@ -20,7 +18,7 @@ vi.mock('../knowledge', () => ({ shortcut: '@kb', title: 'Knowledge', description: 'Search knowledge', - search: vi.fn().mockResolvedValue([]), + source: 'remote', } satisfies ActionItem, })) @@ -30,7 +28,7 @@ vi.mock('../plugin', () => ({ shortcut: '@plugin', title: 'Plugins', description: 'Search plugins', - search: vi.fn().mockResolvedValue([]), + source: 'remote', } satisfies ActionItem, })) @@ -40,7 +38,8 @@ vi.mock('../commands/slash', () => ({ shortcut: '/', title: 'Commands', description: 'Slash commands', - search: vi.fn().mockResolvedValue([]), + source: 'local', + search: vi.fn(() => []), } satisfies ActionItem, })) @@ -50,7 +49,8 @@ vi.mock('../workflow-nodes', () => ({ shortcut: '@node', title: 'Workflow Nodes', description: 'Search workflow nodes', - search: vi.fn().mockResolvedValue([]), + source: 'local', + search: vi.fn(() => []), } satisfies ActionItem, })) @@ -60,266 +60,66 @@ vi.mock('../rag-pipeline-nodes', () => ({ shortcut: '@node', title: 'RAG Pipeline Nodes', description: 'Search RAG nodes', - search: vi.fn().mockResolvedValue([]), + source: 'local', + search: vi.fn(() => []), } satisfies ActionItem, })) vi.mock('../commands/registry') describe('createActions', () => { - it('returns base actions when neither workflow nor rag-pipeline page', () => { - const actions = createActions(false, false) - - expect(actions).toHaveProperty('slash') - expect(actions).toHaveProperty('app') - expect(actions).toHaveProperty('knowledge') - expect(actions).toHaveProperty('plugin') - expect(actions).not.toHaveProperty('node') + it('returns only global actions outside graph pages', () => { + expect(createActions(false, false)).toEqual( + expect.objectContaining({ slash: expect.any(Object), app: expect.any(Object) }), + ) + expect(createActions(false, false)).not.toHaveProperty('node') }) - it('includes workflow nodes action on workflow pages', () => { - const actions = createActions(true, false) as Record - - expect(actions).toHaveProperty('node') - expect(actions.node!.title).toBe('Workflow Nodes') + it('uses the workflow-owned node action on workflow pages', () => { + expect((createActions(true, false) as Record).node!.title).toBe( + 'Workflow Nodes', + ) }) - it('includes rag-pipeline nodes action on rag-pipeline pages', () => { - const actions = createActions(false, true) as Record - - expect(actions).toHaveProperty('node') - expect(actions.node!.title).toBe('RAG Pipeline Nodes') - }) - - it('rag-pipeline page takes priority over workflow page', () => { - const actions = createActions(true, true) as Record - - expect(actions.node!.title).toBe('RAG Pipeline Nodes') + it('uses the RAG-owned node action when both graph flags are true', () => { + expect((createActions(true, true) as Record).node!.title).toBe( + 'RAG Pipeline Nodes', + ) }) }) -describe('searchAnything', () => { - beforeEach(() => { - vi.clearAllMocks() - }) +describe('getActionSearchTerm', () => { + it('removes either the action key or shortcut', () => { + const action = createActions(false, false).knowledge - it('delegates to specific action when actionItem is provided', async () => { - const mockResults: SearchResult[] = [ - { id: '1', title: 'App1', type: 'app', data: {} as unknown as App }, - ] - const action: ActionItem = { - key: '@app', - shortcut: '@app', - title: 'Apps', - description: 'Search apps', - search: vi.fn().mockResolvedValue(mockResults), - } - - const results = await searchAnything('en', '@app myquery', action) - - expect(action.search).toHaveBeenCalledWith('@app myquery', 'myquery', 'en') - expect(results).toEqual(mockResults) - }) - - it('strips action prefix from search term', async () => { - const action: ActionItem = { - key: '@knowledge', - shortcut: '@kb', - title: 'KB', - description: 'Search KB', - search: vi.fn().mockResolvedValue([]), - } - - await searchAnything('en', '@kb hello', action) - - expect(action.search).toHaveBeenCalledWith('@kb hello', 'hello', 'en') - }) - - it('returns empty for queries starting with @ without actionItem', async () => { - const results = await searchAnything('en', '@unknown') - expect(results).toEqual([]) - }) - - it('returns empty for queries starting with / without actionItem', async () => { - const results = await searchAnything('en', '/theme') - expect(results).toEqual([]) - }) - - it('handles action search failure gracefully', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const action: ActionItem = { - key: '@app', - shortcut: '@app', - title: 'Apps', - description: 'Search apps', - search: vi.fn().mockRejectedValue(new Error('network error')), - } - - const results = await searchAnything('en', '@app test', action) - expect(results).toEqual([]) - expect(warnSpy).toHaveBeenCalledWith( - expect.stringContaining('Search failed for @app'), - expect.any(Error), - ) - warnSpy.mockRestore() - }) - - it('runs global search across all non-slash actions for plain queries', async () => { - const appResults: SearchResult[] = [ - { id: 'a1', title: 'My App', type: 'app', data: {} as unknown as App }, - ] - const kbResults: SearchResult[] = [ - { id: 'k1', title: 'My KB', type: 'knowledge', data: {} as unknown as DataSet }, - ] - - const dynamicActions: Record = { - slash: { - key: '/', - shortcut: '/', - title: 'Slash', - description: '', - search: vi.fn().mockResolvedValue([]), - }, - app: { - key: '@app', - shortcut: '@app', - title: 'App', - description: '', - search: vi.fn().mockResolvedValue(appResults), - }, - knowledge: { - key: '@knowledge', - shortcut: '@kb', - title: 'KB', - description: '', - search: vi.fn().mockResolvedValue(kbResults), - }, - } - - const results = await searchAnything('en', 'my query', undefined, dynamicActions) - - expect(dynamicActions.slash!.search).not.toHaveBeenCalled() - expect(results).toHaveLength(2) - expect(results).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: 'a1' }), - expect.objectContaining({ id: 'k1' }), - ]), - ) - }) - - it('handles partial search failures in global search gracefully', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const dynamicActions: Record = { - app: { - key: '@app', - shortcut: '@app', - title: 'App', - description: '', - search: vi.fn().mockRejectedValue(new Error('fail')), - }, - knowledge: { - key: '@knowledge', - shortcut: '@kb', - title: 'KB', - description: '', - search: vi - .fn() - .mockResolvedValue([ - { id: 'k1', title: 'KB1', type: 'knowledge', data: {} as unknown as DataSet }, - ]), - }, - } - - const results = await searchAnything('en', 'query', undefined, dynamicActions) - - expect(results).toHaveLength(1) - expect(results[0]!.id).toBe('k1') - expect(warnSpy).toHaveBeenCalled() - warnSpy.mockRestore() + expect(getActionSearchTerm('@knowledge vector store', action)).toBe('vector store') + expect(getActionSearchTerm('@kb vector store', action)).toBe('vector store') }) }) describe('matchAction', () => { - const actions: Record = { - app: { key: '@app', shortcut: '@app', title: 'App', description: '', search: vi.fn() }, - knowledge: { - key: '@knowledge', - shortcut: '@kb', - title: 'KB', - description: '', - search: vi.fn(), - }, - plugin: { - key: '@plugin', - shortcut: '@plugin', - title: 'Plugin', - description: '', - search: vi.fn(), - }, - slash: { key: '/', shortcut: '/', title: 'Slash', description: '', search: vi.fn() }, - } + const actions = createActions(false, false) beforeEach(() => { - vi.clearAllMocks() - }) - - it('matches @app query', () => { - const result = matchAction('@app test', actions) - expect(result?.key).toBe('@app') - }) - - it('matches @kb shortcut', () => { - const result = matchAction('@kb test', actions) - expect(result?.key).toBe('@knowledge') - }) - - it('matches @plugin query', () => { - const result = matchAction('@plugin test', actions) - expect(result?.key).toBe('@plugin') - }) - - it('returns undefined for unmatched query', () => { vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([]) - const result = matchAction('random query', actions) - expect(result).toBeUndefined() }) - describe('slash command matching', () => { - it('matches submenu command with full name', () => { - vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([ - { name: 'theme', mode: 'submenu', description: '', search: vi.fn() }, - ]) + it.each([ + ['@app query', '@app'], + ['@kb query', '@knowledge'], + ['@plugin query', '@plugin'], + ])('matches %s', (query, key) => { + expect(matchAction(query, actions)?.key).toBe(key) + }) - const result = matchAction('/theme', actions) - expect(result?.key).toBe('/') - }) + it('matches complete submenu commands but leaves direct commands in the command picker', () => { + vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([ + { name: 'theme', mode: 'submenu', description: '', search: vi.fn(() => []) }, + { name: 'docs', mode: 'direct', description: '', search: vi.fn(() => []) }, + ]) - it('matches submenu command with args', () => { - vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([ - { name: 'theme', mode: 'submenu', description: '', search: vi.fn() }, - ]) - - const result = matchAction('/theme dark', actions) - expect(result?.key).toBe('/') - }) - - it('does not match direct-mode commands', () => { - vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([ - { name: 'docs', mode: 'direct', description: '', search: vi.fn() }, - ]) - - const result = matchAction('/docs', actions) - expect(result).toBeUndefined() - }) - - it('does not match partial slash command name', () => { - vi.mocked(slashCommandRegistry.getAllCommands).mockReturnValue([ - { name: 'theme', mode: 'submenu', description: '', search: vi.fn() }, - ]) - - const result = matchAction('/the', actions) - expect(result).toBeUndefined() - }) + expect(matchAction('/theme dark', actions)?.key).toBe('/') + expect(matchAction('/docs', actions)).toBeUndefined() + expect(matchAction('/the', actions)).toBeUndefined() }) }) diff --git a/web/app/components/goto-anything/actions/__tests__/knowledge.spec.ts b/web/app/components/goto-anything/actions/__tests__/knowledge.spec.ts index 575e9011521..e5a115a6742 100644 --- a/web/app/components/goto-anything/actions/__tests__/knowledge.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/knowledge.spec.ts @@ -1,110 +1,62 @@ -import type { DataSet } from '@/models/datasets' -import { knowledgeAction } from '../knowledge' +import type { + DatasetListItemResponse, + DatasetListResponse, +} from '@dify/contracts/api/console/datasets/types.gen' +import { knowledgeAction, knowledgeSearchQueryOptions } from '../knowledge' -vi.mock('@/service/datasets', () => ({ - fetchDatasets: vi.fn(), +const serviceMocks = vi.hoisted(() => ({ queryOptions: vi.fn((options) => options) })) + +vi.mock('@/service/client', () => ({ + consoleQuery: { datasets: { get: { queryOptions: serviceMocks.queryOptions } } }, })) -vi.mock('@langgenius/dify-ui/cn', () => ({ - cn: (...args: string[]) => args.filter(Boolean).join(' '), -})) +function dataset(overrides: Partial = {}): DatasetListItemResponse { + return { + id: 'dataset-1', + name: 'Knowledge', + description: 'Description', + provider: 'vendor', + embedding_available: true, + ...overrides, + } as DatasetListItemResponse +} -describe('knowledgeAction', () => { - beforeEach(() => { - vi.clearAllMocks() - }) +function response(data: DatasetListItemResponse[]): DatasetListResponse { + return { data, has_more: false, limit: 10, page: 1, total: data.length } +} - it('has correct metadata', () => { - expect(knowledgeAction.key).toBe('@knowledge') - expect(knowledgeAction.shortcut).toBe('@kb') - }) +describe('knowledge search query', () => { + beforeEach(() => vi.clearAllMocks()) - it('returns parsed dataset results on success', async () => { - const { fetchDatasets } = await import('@/service/datasets') - vi.mocked(fetchDatasets).mockResolvedValue({ - data: [ - { - id: 'ds-1', - name: 'My Knowledge', - description: 'A KB', - provider: 'vendor', - embedding_available: true, - } as unknown as DataSet, - ], - has_more: false, - limit: 10, - page: 1, - total: 1, - }) - - const results = await knowledgeAction.search('@knowledge query', 'query', 'en') - - expect(fetchDatasets).toHaveBeenCalledWith({ - url: '/datasets', - params: { page: 1, limit: 10, keyword: 'query' }, - }) - expect(results).toHaveLength(1) - expect(results[0]).toMatchObject({ - id: 'ds-1', - title: 'My Knowledge', - type: 'knowledge', + it('exposes remote action metadata', () => { + expect(knowledgeAction).toMatchObject({ + key: '@knowledge', + shortcut: '@kb', + source: 'remote', }) }) - it('generates correct path for external provider', async () => { - const { fetchDatasets } = await import('@/service/datasets') - vi.mocked(fetchDatasets).mockResolvedValue({ - data: [ - { - id: 'ds-ext', - name: 'External', - description: '', - provider: 'external', - embedding_available: true, - } as unknown as DataSet, - ], - has_more: false, - limit: 10, - page: 1, - total: 1, - }) + it('builds generated query options from the search term', () => { + knowledgeSearchQueryOptions('vector') - const results = await knowledgeAction.search('@knowledge', '', 'en') - - expect(results[0]!.path).toBe('/datasets/ds-ext/hitTesting') + expect(serviceMocks.queryOptions).toHaveBeenCalledWith( + expect.objectContaining({ + input: { query: { page: 1, limit: 10, keyword: 'vector' } }, + retry: false, + select: expect.any(Function), + }), + ) }) - it('generates correct path for non-external provider', async () => { - const { fetchDatasets } = await import('@/service/datasets') - vi.mocked(fetchDatasets).mockResolvedValue({ - data: [ - { - id: 'ds-2', - name: 'Internal', - description: '', - provider: 'vendor', - embedding_available: true, - } as unknown as DataSet, - ], - has_more: false, - limit: 10, - page: 1, - total: 1, - }) + it('selects paths from the dataset provider contract', () => { + const options = knowledgeSearchQueryOptions('') + const results = options.select!( + response([dataset(), dataset({ id: 'external', provider: 'external' })]), + ) - const results = await knowledgeAction.search('@knowledge', '', 'en') - - expect(results[0]!.path).toBe('/datasets/ds-2/documents') - }) - - it('returns empty array on API failure', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { fetchDatasets } = await import('@/service/datasets') - vi.mocked(fetchDatasets).mockRejectedValue(new Error('fail')) - - const results = await knowledgeAction.search('@knowledge', 'fail', 'en') - expect(results).toEqual([]) - expect(warnSpy).toHaveBeenCalledWith('Knowledge search failed:', expect.any(Error)) - warnSpy.mockRestore() + expect(results.map((result) => result.path)).toEqual([ + '/datasets/dataset-1/documents', + '/datasets/external/hitTesting', + ]) }) }) diff --git a/web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts b/web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts index bbf25f8c111..bbef61ce1a3 100644 --- a/web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts @@ -1,73 +1,43 @@ import type { SearchResult } from '../types' -import { ragPipelineNodesAction } from '../rag-pipeline-nodes' -import { workflowNodesAction } from '../workflow-nodes' +import { + findRagPipelineNodes, + registerRagPipelineNodeSearch, +} from '@/app/components/rag-pipeline/goto-anything-search' +import { + findWorkflowNodes, + registerWorkflowNodeSearch, +} from '@/app/components/workflow/goto-anything-search' -describe('workflowNodesAction', () => { - beforeEach(() => { - vi.clearAllMocks() - workflowNodesAction.searchFn = undefined - }) - - it('should return an empty result when no workflow search function is registered', async () => { - await expect(workflowNodesAction.search('@node llm', 'llm', 'en')).resolves.toEqual([]) - }) - - it('should delegate to the injected workflow search function', async () => { +describe('workflow node search registry', () => { + it('registers, searches, and unregisters workflow nodes', () => { const results: SearchResult[] = [ { id: 'workflow-node-1', title: 'LLM', type: 'workflow-node', data: {} as never }, ] - workflowNodesAction.searchFn = vi.fn().mockReturnValue(results) + const search = vi.fn().mockReturnValue(results) + const unregister = registerWorkflowNodeSearch(search) - await expect(workflowNodesAction.search('@node llm', 'llm', 'en')).resolves.toEqual(results) - expect(workflowNodesAction.searchFn).toHaveBeenCalledWith('llm') - }) + expect(findWorkflowNodes('llm')).toEqual(results) + expect(search).toHaveBeenCalledWith('llm') - it('should warn and return an empty list when workflow node search throws', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - workflowNodesAction.searchFn = vi.fn(() => { - throw new Error('failed') - }) + unregister() - await expect(workflowNodesAction.search('@node llm', 'llm', 'en')).resolves.toEqual([]) - expect(warnSpy).toHaveBeenCalledWith('Workflow nodes search failed:', expect.any(Error)) - - warnSpy.mockRestore() + expect(findWorkflowNodes('llm')).toEqual([]) }) }) -describe('ragPipelineNodesAction', () => { - beforeEach(() => { - vi.clearAllMocks() - ragPipelineNodesAction.searchFn = undefined - }) - - it('should return an empty result when no rag pipeline search function is registered', async () => { - await expect(ragPipelineNodesAction.search('@node embed', 'embed', 'en')).resolves.toEqual([]) - }) - - it('should delegate to the injected rag pipeline search function', async () => { +describe('RAG pipeline node search registry', () => { + it('registers, searches, and unregisters RAG pipeline nodes', () => { const results: SearchResult[] = [ { id: 'rag-node-1', title: 'Retriever', type: 'workflow-node', data: {} as never }, ] - ragPipelineNodesAction.searchFn = vi.fn().mockReturnValue(results) + const search = vi.fn().mockReturnValue(results) + const unregister = registerRagPipelineNodeSearch(search) - await expect( - ragPipelineNodesAction.search('@node retrieve', 'retrieve', 'en'), - ).resolves.toEqual(results) - expect(ragPipelineNodesAction.searchFn).toHaveBeenCalledWith('retrieve') - }) + expect(findRagPipelineNodes('retrieve')).toEqual(results) + expect(search).toHaveBeenCalledWith('retrieve') - it('should warn and return an empty list when rag pipeline node search throws', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - ragPipelineNodesAction.searchFn = vi.fn(() => { - throw new Error('failed') - }) + unregister() - await expect( - ragPipelineNodesAction.search('@node retrieve', 'retrieve', 'en'), - ).resolves.toEqual([]) - expect(warnSpy).toHaveBeenCalledWith('RAG pipeline nodes search failed:', expect.any(Error)) - - warnSpy.mockRestore() + expect(findRagPipelineNodes('retrieve')).toEqual([]) }) }) diff --git a/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts b/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts index 024c162e2a1..06bf9e7526f 100644 --- a/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts +++ b/web/app/components/goto-anything/actions/__tests__/plugin.spec.ts @@ -1,37 +1,47 @@ -import { pluginAction } from '../plugin' +import { pluginAction, pluginSearchQueryOptions } from '../plugin' -vi.mock('@/service/base', () => ({ - postMarketplace: vi.fn(), +const serviceMocks = vi.hoisted(() => ({ queryOptions: vi.fn((options) => options) })) + +vi.mock('@/service/client', () => ({ + marketplaceQuery: { searchAdvanced: { queryOptions: serviceMocks.queryOptions } }, })) vi.mock('@/i18n-config', () => ({ - renderI18nObject: vi.fn((obj: Record | string, locale: string) => { - if (typeof obj === 'string') return obj - return obj[locale] || obj.en_US || '' - }), -})) - -vi.mock('../../../plugins/card/base/card-icon', () => ({ - default: () => null, + renderI18nObject: vi.fn((value: Record | string, locale: string) => + typeof value === 'string' ? value : value[locale] || value.en_US || '', + ), })) +vi.mock('../../../plugins/card/base/card-icon', () => ({ default: () => null })) vi.mock('../../../plugins/marketplace/utils', () => ({ getFormattedPlugin: vi.fn((plugin) => ({ ...plugin, icon: 'icon-url' })), })) -describe('pluginAction', () => { - beforeEach(() => { - vi.clearAllMocks() +describe('plugin search query', () => { + beforeEach(() => vi.clearAllMocks()) + + it('exposes remote action metadata', () => { + expect(pluginAction).toMatchObject({ key: '@plugin', shortcut: '@plugin', source: 'remote' }) }) - it('has correct metadata', () => { - expect(pluginAction.key).toBe('@plugin') - expect(pluginAction.shortcut).toBe('@plugin') + it('builds generated marketplace query options', () => { + pluginSearchQueryOptions('agent', 'en_US') + + expect(serviceMocks.queryOptions).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + params: { kind: 'plugins' }, + body: { page: 1, page_size: 10, query: 'agent' }, + }, + retry: false, + select: expect.any(Function), + }), + ) }) - it('returns parsed plugin results on success', async () => { - const { postMarketplace } = await import('@/service/base') - vi.mocked(postMarketplace).mockResolvedValue({ + it('selects formatted plugin results using the active locale', () => { + const options = pluginSearchQueryOptions('', 'en_US') + const results = options.select!({ data: { plugins: [ { @@ -41,45 +51,15 @@ describe('pluginAction', () => { icon: 'icon.png', }, ], - total: 1, }, - }) + } as never) - const results = await pluginAction.search('@plugin', 'test', 'en_US') - - expect(postMarketplace).toHaveBeenCalledWith('/plugins/search/advanced', { - body: { page: 1, page_size: 10, query: 'test', type: 'plugin' }, - }) - expect(results).toHaveLength(1) - expect(results[0]).toMatchObject({ - id: 'plugin-1', - title: 'My Plugin', - type: 'plugin', - }) + expect(results[0]).toMatchObject({ id: 'plugin-1', title: 'My Plugin', type: 'plugin' }) }) - it('returns empty array when response has unexpected structure', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { postMarketplace } = await import('@/service/base') - vi.mocked(postMarketplace).mockResolvedValue({ data: {} }) + it('selects an empty list when marketplace returns no plugins', () => { + const options = pluginSearchQueryOptions('', 'en_US') - const results = await pluginAction.search('@plugin', 'test', 'en') - expect(results).toEqual([]) - expect(warnSpy).toHaveBeenCalledWith( - 'Plugin search: Unexpected response structure', - expect.anything(), - ) - warnSpy.mockRestore() - }) - - it('returns empty array on API failure', async () => { - const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - const { postMarketplace } = await import('@/service/base') - vi.mocked(postMarketplace).mockRejectedValue(new Error('fail')) - - const results = await pluginAction.search('@plugin', 'fail', 'en') - expect(results).toEqual([]) - expect(warnSpy).toHaveBeenCalledWith('Plugin search failed:', expect.any(Error)) - warnSpy.mockRestore() + expect(options.select!({ data: {} } as never)).toEqual([]) }) }) diff --git a/web/app/components/goto-anything/actions/app.tsx b/web/app/components/goto-anything/actions/app.tsx index cea05e5d6c0..d4953e652b0 100644 --- a/web/app/components/goto-anything/actions/app.tsx +++ b/web/app/components/goto-anything/actions/app.tsx @@ -1,30 +1,56 @@ +import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' import type { ActionItem, AppSearchResult, SearchResult } from './types' -import type { App } from '@/types/app' -import { - RiFileListLine, - RiLayoutLine, - RiLineChartLine, - RiNodeTree, - RiTerminalBoxLine, -} from '@remixicon/react' -import * as React from 'react' -import { fetchAppList } from '@/service/apps' +import type { AppIconType, AppModeEnum as AppMode } from '@/types/app' +import { consoleQuery } from '@/service/client' import { AppModeEnum } from '@/types/app' import { getRedirectionPath } from '@/utils/app-redirection' import { AppTypeIcon } from '../../app/type-selector' import AppIcon from '../../base/app-icon' const WORKFLOW_MODES = new Set([AppModeEnum.WORKFLOW, AppModeEnum.ADVANCED_CHAT]) +const APP_MODES = new Set(Object.values(AppModeEnum)) +const APP_ICON_TYPES = new Set(['emoji', 'image', 'link']) -type AppSection = { id: string; label: string; path: string; icon: React.ElementType } +type AppSection = { id: string; label: string; path: string; iconClassName: string } -const getAppSections = (app: App): AppSection[] => { +function getAppMode(app: AppPartial): AppMode { + return APP_MODES.has(app.mode) ? (app.mode as AppMode) : AppModeEnum.CHAT +} + +function getAppIconType(app: AppPartial): AppIconType | null { + return app.icon_type && APP_ICON_TYPES.has(app.icon_type) ? (app.icon_type as AppIconType) : null +} + +function getAppPath(app: AppPartial) { + return getRedirectionPath({ + id: app.id, + mode: getAppMode(app), + permission_keys: app.permission_keys, + }) +} + +const getAppSections = (app: AppPartial): AppSection[] => { const base = `/app/${app.id}` - if (WORKFLOW_MODES.has(app.mode)) { + if (WORKFLOW_MODES.has(getAppMode(app))) { return [ - { id: 'workflow', label: 'Workflow', path: `${base}/workflow`, icon: RiNodeTree }, - { id: 'overview', label: 'Overview', path: `${base}/overview`, icon: RiLineChartLine }, - { id: 'logs', label: 'Logs', path: `${base}/logs`, icon: RiFileListLine }, + { + id: 'workflow', + label: 'Workflow', + path: `${base}/workflow`, + iconClassName: 'i-ri-node-tree size-4 text-text-tertiary', + }, + { + id: 'overview', + label: 'Overview', + path: `${base}/overview`, + iconClassName: 'i-ri-line-chart-line size-4 text-text-tertiary', + }, + { + id: 'logs', + label: 'Logs', + path: `${base}/logs`, + iconClassName: 'i-ri-file-list-line size-4 text-text-tertiary', + }, ] } return [ @@ -32,53 +58,68 @@ const getAppSections = (app: App): AppSection[] => { id: 'configuration', label: 'Configuration', path: `${base}/configuration`, - icon: RiLayoutLine, + iconClassName: 'i-ri-layout-line size-4 text-text-tertiary', + }, + { + id: 'overview', + label: 'Overview', + path: `${base}/overview`, + iconClassName: 'i-ri-line-chart-line size-4 text-text-tertiary', + }, + { + id: 'logs', + label: 'Logs', + path: `${base}/logs`, + iconClassName: 'i-ri-file-list-line size-4 text-text-tertiary', + }, + { + id: 'develop', + label: 'Develop', + path: `${base}/develop`, + iconClassName: 'i-ri-terminal-box-line size-4 text-text-tertiary', }, - { id: 'overview', label: 'Overview', path: `${base}/overview`, icon: RiLineChartLine }, - { id: 'logs', label: 'Logs', path: `${base}/logs`, icon: RiFileListLine }, - { id: 'develop', label: 'Develop', path: `${base}/develop`, icon: RiTerminalBoxLine }, ] } -const appIcon = (app: App) => ( +const appIcon = (app: AppPartial) => (
) -const parser = (apps: App[]): AppSearchResult[] => { +function getAppResults(apps: AppPartial[]): AppSearchResult[] { return apps.map((app) => ({ id: app.id, title: app.name, - description: app.description, + description: app.description ?? undefined, type: 'app' as const, - path: getRedirectionPath(app), + path: getAppPath(app), icon: appIcon(app), data: app, })) } // Generate sub-section results for matched apps when in scoped @app search -const parserWithSections = (apps: App[]): SearchResult[] => { +function getScopedAppResults(apps: AppPartial[]): SearchResult[] { const results: SearchResult[] = [] for (const app of apps) { results.push({ id: app.id, title: app.name, - description: app.description, + description: app.description ?? undefined, type: 'app' as const, - path: getRedirectionPath(app), + path: getAppPath(app), icon: appIcon(app), data: app, }) @@ -91,7 +132,7 @@ const parserWithSections = (apps: App[]): SearchResult[] => { path: section.path, icon: (
- +
), data: app, @@ -106,21 +147,19 @@ export const appAction: ActionItem = { shortcut: '@app', title: 'Search Applications', description: 'Search and navigate to your applications', - search: async (query, searchTerm = '', _locale) => { - const isScoped = query.trimStart().startsWith('@app') || query.trimStart().startsWith('@App') - try { - const response = await fetchAppList({ - url: 'apps', - params: { - page: 1, - name: searchTerm, - }, - }) - const apps = response?.data || [] - return isScoped ? parserWithSections(apps) : parser(apps) - } catch (error) { - console.warn('App search failed:', error) - return [] - } - }, + source: 'remote', +} + +export function appSearchQueryOptions(searchTerm: string, scoped: boolean) { + return consoleQuery.apps.get.queryOptions({ + input: { + query: { + page: 1, + name: searchTerm, + }, + }, + retry: false, + select: (response) => + scoped ? getScopedAppResults(response.data) : getAppResults(response.data), + }) } diff --git a/web/app/components/goto-anything/actions/commands/__tests__/create.spec.tsx b/web/app/components/goto-anything/actions/commands/__tests__/create.spec.tsx index c9aefa77889..9de17e5e51e 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/create.spec.tsx +++ b/web/app/components/goto-anything/actions/commands/__tests__/create.spec.tsx @@ -1,12 +1,5 @@ import { executeCommand } from '../command-bus' import { createCommand } from '../create' - -// Stub the icon imports — these are React components we don't render here. -vi.mock('@remixicon/react', () => ({ - RiChat3Line: () => null, - RiNodeTree: () => null, - RiSparkling2Line: () => null, -})) // We spy on the store at module scope so the `create.open` handler that // register() pushes into the command bus can be observed by the tests. const mockOpenGenerator = vi.fn() diff --git a/web/app/components/goto-anything/actions/commands/__tests__/refine.spec.tsx b/web/app/components/goto-anything/actions/commands/__tests__/refine.spec.tsx index 4fb958eff6a..b3d59487abd 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/refine.spec.tsx +++ b/web/app/components/goto-anything/actions/commands/__tests__/refine.spec.tsx @@ -1,10 +1,5 @@ import { executeCommand } from '../command-bus' import { refineCommand } from '../refine' - -// Stub the icon import — it's a React component we don't render here. -vi.mock('@remixicon/react', () => ({ - RiSparkling2Line: () => null, -})) // Spy on the generator store so we can observe what /refine opens it with. const mockOpenGenerator = vi.fn() vi.mock('@/app/components/workflow/workflow-generator/store', () => ({ diff --git a/web/app/components/goto-anything/actions/commands/__tests__/registry.spec.ts b/web/app/components/goto-anything/actions/commands/__tests__/registry.spec.ts index ee4936f4f5f..690b01d1953 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/registry.spec.ts +++ b/web/app/components/goto-anything/actions/commands/__tests__/registry.spec.ts @@ -225,7 +225,9 @@ describe('SlashCommandRegistry', () => { const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) const handler = createHandler({ name: 'broken', - search: vi.fn().mockRejectedValue(new Error('fail')), + search: vi.fn(() => { + throw new Error('fail') + }), }) registry.register(handler) diff --git a/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx b/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx index f13ff88aba9..b2f94af91c4 100644 --- a/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx +++ b/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx @@ -91,6 +91,8 @@ describe('slashAction', () => { { id: 'theme', title: '/theme', type: 'command', data: { command: 'theme' } }, ]) + expect(slashAction.source).toBe('local') + if (slashAction.source !== 'local') throw new Error('Expected a local slash action') const results = await slashAction.search('/theme dark', 'dark') expect(mockSearch).toHaveBeenCalledWith('/theme dark', 'ja') diff --git a/web/app/components/goto-anything/actions/commands/account.tsx b/web/app/components/goto-anything/actions/commands/account.tsx index 396aaf45c42..0966f909879 100644 --- a/web/app/components/goto-anything/actions/commands/account.tsx +++ b/web/app/components/goto-anything/actions/commands/account.tsx @@ -1,6 +1,4 @@ import type { SlashCommandHandler } from './types' -import { RiUser3Line } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { registerCommands, unregisterCommands } from './command-bus' @@ -20,7 +18,7 @@ export const accountCommand: SlashCommandHandler = { window.location.href = '/account' }, - async search(args: string, locale: string = 'en') { + search(args: string, locale: string = 'en') { const i18n = getI18n() return [ { @@ -33,7 +31,7 @@ export const accountCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'navigation.account', args: {} }, diff --git a/web/app/components/goto-anything/actions/commands/community.tsx b/web/app/components/goto-anything/actions/commands/community.tsx index 34f6ae06be0..1993ccf1210 100644 --- a/web/app/components/goto-anything/actions/commands/community.tsx +++ b/web/app/components/goto-anything/actions/commands/community.tsx @@ -1,6 +1,4 @@ import type { SlashCommandHandler } from './types' -import { RiDiscordLine } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { registerCommands, unregisterCommands } from './command-bus' @@ -21,7 +19,7 @@ export const communityCommand: SlashCommandHandler = { window.open(url, '_blank', 'noopener,noreferrer') }, - async search(args: string, locale: string = 'en') { + search(args: string, locale: string = 'en') { const i18n = getI18n() return [ { @@ -33,7 +31,7 @@ export const communityCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'navigation.community', args: { url: 'https://discord.gg/5AEfbxcd9k' } }, diff --git a/web/app/components/goto-anything/actions/commands/create.tsx b/web/app/components/goto-anything/actions/commands/create.tsx index 1b4ee601ca6..435394415c3 100644 --- a/web/app/components/goto-anything/actions/commands/create.tsx +++ b/web/app/components/goto-anything/actions/commands/create.tsx @@ -1,7 +1,5 @@ import type { SlashCommandHandler } from './types' import type { WorkflowGeneratorMode } from '@/app/components/workflow/workflow-generator/types' -import { RiChat3Line, RiNodeTree, RiSparkling2Line } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { useWorkflowGeneratorStore } from '@/app/components/workflow/workflow-generator/store' @@ -18,7 +16,7 @@ type CreateOption = { mode: WorkflowGeneratorMode /** When set, the modal opens in auto-mode and the planner picks the app type. */ auto?: boolean - icon: React.ComponentType<{ className?: string }> + iconClassName: string } // `as const` keeps titleKey/descKey as literal types so the typed `i18n.t` @@ -30,7 +28,7 @@ const OPTIONS = [ descKey: 'gotoAnything.actions.createAutoDesc', mode: 'advanced-chat', auto: true, - icon: RiSparkling2Line, + iconClassName: 'i-ri-sparkling-2-line', }, { id: 'workflow', @@ -38,7 +36,7 @@ const OPTIONS = [ descKey: 'gotoAnything.actions.createWorkflowDesc', mode: 'workflow', auto: false, - icon: RiNodeTree, + iconClassName: 'i-ri-node-tree', }, { id: 'chatflow', @@ -46,7 +44,7 @@ const OPTIONS = [ descKey: 'gotoAnything.actions.createChatflowDesc', mode: 'advanced-chat', auto: false, - icon: RiChat3Line, + iconClassName: 'i-ri-chat-3-line', }, ] as const satisfies readonly CreateOption[] @@ -71,19 +69,17 @@ const OPTIONS = [ export const createCommand: SlashCommandHandler = { name: 'create', aliases: ['new', 'generate'], - // Fallback only — the palette localises the root row via the slashKeyMap in - // command-selector.tsx (gotoAnything.actions.createCategoryDesc). description: getI18n().t(($) => $['gotoAnything.actions.createCategoryDesc'], { ns: 'app' }), mode: 'submenu', - async search(args: string, locale?: string) { + search(args: string, locale?: string) { const i18n = getI18n() const tr = (key: (typeof OPTIONS)[number]['titleKey' | 'descKey']) => i18n.t(($) => $[key], { ns: 'app', lng: locale }) - const renderIcon = (Icon: CreateOption['icon']) => ( + const renderIcon = (iconClassName: string) => (
- +
) @@ -94,7 +90,7 @@ export const createCommand: SlashCommandHandler = { // back to the option's static description otherwise. description: instruction || tr(opt.descKey), type: 'command' as const, - icon: renderIcon(opt.icon), + icon: renderIcon(opt.iconClassName), data: { command: 'create.open', args: { mode: opt.mode, auto: !!opt.auto, instruction } }, }) diff --git a/web/app/components/goto-anything/actions/commands/docs.tsx b/web/app/components/goto-anything/actions/commands/docs.tsx index 12bcdf1b4e5..b234af8a7ca 100644 --- a/web/app/components/goto-anything/actions/commands/docs.tsx +++ b/web/app/components/goto-anything/actions/commands/docs.tsx @@ -1,6 +1,4 @@ import type { SlashCommandHandler } from './types' -import { RiBookOpenLine } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { defaultDocBaseUrl, getDocHomePath } from '@/context/i18n' import { getDocLanguage } from '@/i18n-config/language' @@ -29,7 +27,7 @@ export const docsCommand: SlashCommandHandler = { window.open(getDocsHomeUrl(), '_blank', 'noopener,noreferrer') }, - async search(args: string, locale: string = 'en') { + search(args: string, locale: string = 'en') { const i18n = getI18n() return [ { @@ -41,7 +39,7 @@ export const docsCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'navigation.doc', args: {} }, diff --git a/web/app/components/goto-anything/actions/commands/forum.tsx b/web/app/components/goto-anything/actions/commands/forum.tsx index 861a67c3701..00a37006e22 100644 --- a/web/app/components/goto-anything/actions/commands/forum.tsx +++ b/web/app/components/goto-anything/actions/commands/forum.tsx @@ -1,6 +1,4 @@ import type { SlashCommandHandler } from './types' -import { RiFeedbackLine } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { registerCommands, unregisterCommands } from './command-bus' @@ -21,7 +19,7 @@ export const forumCommand: SlashCommandHandler = { window.open(url, '_blank', 'noopener,noreferrer') }, - async search(args: string, locale: string = 'en') { + search(args: string, locale: string = 'en') { const i18n = getI18n() return [ { @@ -33,7 +31,7 @@ export const forumCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'navigation.forum', args: { url: 'https://forum.dify.ai' } }, diff --git a/web/app/components/goto-anything/actions/commands/go.tsx b/web/app/components/goto-anything/actions/commands/go.tsx index ce035daf212..a048e860c53 100644 --- a/web/app/components/goto-anything/actions/commands/go.tsx +++ b/web/app/components/goto-anything/actions/commands/go.tsx @@ -1,22 +1,13 @@ import type { SlashCommandHandler } from './types' -import { - RiApps2Line, - RiBookOpenLine, - RiCompassLine, - RiPlugLine, - RiToolsLine, - RiUserLine, -} from '@remixicon/react' -import * as React from 'react' import { registerCommands, unregisterCommands } from './command-bus' const NAV_ITEMS = [ - { id: 'apps', label: 'Apps', path: '/apps', icon: RiApps2Line }, - { id: 'datasets', label: 'Knowledge', path: '/datasets', icon: RiBookOpenLine }, - { id: 'plugins', label: 'Plugins', path: '/plugins', icon: RiPlugLine }, - { id: 'tools', label: 'Tools', path: '/tools', icon: RiToolsLine }, - { id: 'explore', label: 'Explore', path: '/explore', icon: RiCompassLine }, - { id: 'account', label: 'Account', path: '/account', icon: RiUserLine }, + { id: 'apps', label: 'Apps', path: '/apps', iconClassName: 'i-ri-apps-2-line' }, + { id: 'datasets', label: 'Knowledge', path: '/datasets', iconClassName: 'i-ri-book-open-line' }, + { id: 'plugins', label: 'Plugins', path: '/plugins', iconClassName: 'i-ri-plug-line' }, + { id: 'tools', label: 'Tools', path: '/tools', iconClassName: 'i-ri-tools-line' }, + { id: 'explore', label: 'Explore', path: '/explore', iconClassName: 'i-ri-compass-line' }, + { id: 'account', label: 'Account', path: '/account', iconClassName: 'i-ri-user-line' }, ] /** @@ -28,7 +19,7 @@ export const goCommand: SlashCommandHandler = { description: 'Navigate to a section', mode: 'submenu', - async search(args: string, _locale: string = 'en') { + search(args: string, _locale: string = 'en') { const query = args.trim().toLowerCase() const items = NAV_ITEMS.filter( (item) => !query || item.id.includes(query) || item.label.toLowerCase().includes(query), @@ -40,7 +31,7 @@ export const goCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'navigation.go', args: { path: item.path } }, diff --git a/web/app/components/goto-anything/actions/commands/language.tsx b/web/app/components/goto-anything/actions/commands/language.tsx index d180c10b92d..0439124f4ea 100644 --- a/web/app/components/goto-anything/actions/commands/language.tsx +++ b/web/app/components/goto-anything/actions/commands/language.tsx @@ -36,7 +36,7 @@ export const languageCommand: SlashCommandHandler = { description: 'Switch between different languages', mode: 'submenu', // Explicitly set submenu mode - async search(args: string, _locale: string = 'en') { + search(args: string, _locale: string = 'en') { // Return language options directly, regardless of parameters return buildLanguageCommands(args) }, diff --git a/web/app/components/goto-anything/actions/commands/refine.tsx b/web/app/components/goto-anything/actions/commands/refine.tsx index 9be329353ef..9c4e68a6741 100644 --- a/web/app/components/goto-anything/actions/commands/refine.tsx +++ b/web/app/components/goto-anything/actions/commands/refine.tsx @@ -1,7 +1,5 @@ import type { SlashCommandHandler } from './types' import type { WorkflowGeneratorMode } from '@/app/components/workflow/workflow-generator/types' -import { RiSparkling2Line } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { useWorkflowGeneratorStore } from '@/app/components/workflow/workflow-generator/store' @@ -49,8 +47,6 @@ const openRefineGenerator = () => { export const refineCommand: SlashCommandHandler = { name: 'refine', aliases: ['improve'], - // Fallback only — the palette localises the root row via the slashKeyMap in - // command-selector.tsx (gotoAnything.actions.refineCategoryDesc). description: getI18n().t(($) => $['gotoAnything.actions.refineCategoryDesc'], { ns: 'app' }), mode: 'direct', @@ -60,7 +56,7 @@ export const refineCommand: SlashCommandHandler = { execute: openRefineGenerator, - async search(_args: string, locale?: string) { + search(_args: string, locale?: string) { const i18n = getI18n() return [ { @@ -73,7 +69,7 @@ export const refineCommand: SlashCommandHandler = { type: 'command' as const, icon: (
- +
), data: { command: 'refine.open', args: {} }, diff --git a/web/app/components/goto-anything/actions/commands/registry.ts b/web/app/components/goto-anything/actions/commands/registry.ts index bbc43a8c21b..f9d2550fc84 100644 --- a/web/app/components/goto-anything/actions/commands/registry.ts +++ b/web/app/components/goto-anything/actions/commands/registry.ts @@ -1,18 +1,18 @@ import type { CommandSearchResult } from '../types' -import type { SlashCommandHandler } from './types' +import type { SlashCommand, SlashCommandHandler } from './types' /** * Slash Command Registry System * Responsible for managing registration, lookup, and search of all slash commands */ export class SlashCommandRegistry { - private commands = new Map() - private commandDeps = new Map() + private commands = new Map() + private commandDeps = new Map() /** * Register command handler */ - register(handler: SlashCommandHandler, deps?: TDeps) { + register(handler: SlashCommandHandler, deps?: TDeps) { // Register main command name this.commands.set(handler.name, handler) @@ -57,7 +57,7 @@ export class SlashCommandRegistry { /** * Find command handler */ - findCommand(commandName: string): SlashCommandHandler | undefined { + findCommand(commandName: string): SlashCommand | undefined { return this.commands.get(commandName) } @@ -65,7 +65,7 @@ export class SlashCommandRegistry { * Smart partial command matching * Prioritize alias matching, then match command name prefix */ - private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined { + private findBestPartialMatch(partialName: string): SlashCommand | undefined { const lowerPartial = partialName.toLowerCase() // First check if any alias starts with this @@ -80,7 +80,7 @@ export class SlashCommandRegistry { /** * Find handler by alias prefix */ - private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined { + private findHandlerByAliasPrefix(prefix: string): SlashCommand | undefined { for (const handler of this.getAllCommands()) { if (handler.aliases?.some((alias) => alias.toLowerCase().startsWith(prefix))) return handler } @@ -90,15 +90,15 @@ export class SlashCommandRegistry { /** * Find handler by name prefix */ - private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined { + private findHandlerByNamePrefix(prefix: string): SlashCommand | undefined { return this.getAllCommands().find((handler) => handler.name.toLowerCase().startsWith(prefix)) } /** * Get all registered commands (deduplicated) */ - getAllCommands(): SlashCommandHandler[] { - const uniqueCommands = new Map() + getAllCommands(): SlashCommand[] { + const uniqueCommands = new Map() this.commands.forEach((handler) => { uniqueCommands.set(handler.name, handler) }) @@ -109,7 +109,7 @@ export class SlashCommandRegistry { * Get all available commands in current context (deduplicated and filtered) * Commands without isAvailable method are considered always available */ - getAvailableCommands(): SlashCommandHandler[] { + getAvailableCommands(): SlashCommand[] { return this.getAllCommands().filter((handler) => this.isCommandAvailable(handler)) } @@ -118,11 +118,11 @@ export class SlashCommandRegistry { * @param query Full query (e.g., "/theme dark" or "/lang en") * @param locale Current language */ - async search(query: string, locale: string = 'en'): Promise { + search(query: string, locale: string = 'en'): CommandSearchResult[] { const trimmed = query.trim() // Handle root level search "/" - if (trimmed === '/' || !trimmed.replace('/', '').trim()) return await this.getRootCommands() + if (trimmed === '/' || !trimmed.replace('/', '').trim()) return this.getRootCommands() // Parse command and arguments const afterSlash = trimmed.substring(1).trim() @@ -134,7 +134,7 @@ export class SlashCommandRegistry { let handler = this.findCommand(commandName) if (handler && this.isCommandAvailable(handler)) { try { - return await handler.search(args, locale) + return handler.search(args, locale) } catch (error) { console.warn(`Command search failed for ${commandName}:`, error) return [] @@ -145,7 +145,7 @@ export class SlashCommandRegistry { handler = this.findBestPartialMatch(commandName) if (handler && this.isCommandAvailable(handler)) { try { - return await handler.search(args, locale) + return handler.search(args, locale) } catch (error) { console.warn(`Command search failed for ${handler.name}:`, error) return [] @@ -160,7 +160,7 @@ export class SlashCommandRegistry { * Get root level command list * Only shows commands that are available in current context */ - private async getRootCommands(): Promise { + private getRootCommands(): CommandSearchResult[] { return this.getAvailableCommands().map((handler) => ({ id: `root-${handler.name}`, title: `/${handler.name}`, @@ -221,7 +221,7 @@ export class SlashCommandRegistry { /** * Get command dependencies */ - getCommandDependencies(commandName: string): any { + getCommandDependencies(commandName: string): unknown { return this.commandDeps.get(commandName) } @@ -229,7 +229,7 @@ export class SlashCommandRegistry { * Determine if a command is available in the current context. * Defaults to true when a handler does not implement the guard. */ - private isCommandAvailable(handler: SlashCommandHandler) { + private isCommandAvailable(handler: SlashCommand) { return handler.isAvailable?.() ?? true } } diff --git a/web/app/components/goto-anything/actions/commands/slash.tsx b/web/app/components/goto-anything/actions/commands/slash.tsx index 5fb2ed678a2..005a841da67 100644 --- a/web/app/components/goto-anything/actions/commands/slash.tsx +++ b/web/app/components/goto-anything/actions/commands/slash.tsx @@ -14,12 +14,13 @@ export const slashAction: ActionItem = { const i18n = getI18n() return i18n.t(($) => $['gotoAnything.actions.slashDesc'], { ns: 'app' }) }, + source: 'local', action: (result) => { if (result.type !== 'command') return const { command, args } = result.data executeCommand(command, args) }, - search: async (query, _searchTerm = '') => { + search: (query, _searchTerm = '') => { const i18n = getI18n() // Delegate all search logic to the command registry system return slashCommandRegistry.search(query, i18n.language) diff --git a/web/app/components/goto-anything/actions/commands/theme.tsx b/web/app/components/goto-anything/actions/commands/theme.tsx index 63b74c0a19e..be38b31c060 100644 --- a/web/app/components/goto-anything/actions/commands/theme.tsx +++ b/web/app/components/goto-anything/actions/commands/theme.tsx @@ -1,7 +1,5 @@ import type { CommandSearchResult } from '../types' import type { SlashCommandHandler } from './types' -import { RiComputerLine, RiMoonLine, RiSunLine } from '@remixicon/react' -import * as React from 'react' import { getI18n } from 'react-i18next' import { registerCommands, unregisterCommands } from './command-bus' @@ -15,19 +13,19 @@ const THEME_ITEMS = [ id: 'system', titleKey: 'gotoAnything.actions.themeSystem', descKey: 'gotoAnything.actions.themeSystemDesc', - icon: , + iconClassName: 'i-ri-computer-line', }, { id: 'light', titleKey: 'gotoAnything.actions.themeLight', descKey: 'gotoAnything.actions.themeLightDesc', - icon: , + iconClassName: 'i-ri-sun-line', }, { id: 'dark', titleKey: 'gotoAnything.actions.themeDark', descKey: 'gotoAnything.actions.themeDarkDesc', - icon: , + iconClassName: 'i-ri-moon-line', }, ] as const @@ -50,7 +48,7 @@ const buildThemeCommands = (query: string, locale?: string): CommandSearchResult type: 'command' as const, icon: (
- {item.icon} +
), data: { command: 'theme.set', args: { value: item.id } }, @@ -66,7 +64,7 @@ export const themeCommand: SlashCommandHandler = { description: 'Switch between light and dark themes', mode: 'submenu', // Explicitly set submenu mode - async search(args: string, locale: string = 'en') { + search(args: string, locale: string = 'en') { // Return theme options directly, regardless of parameters return buildThemeCommands(args, locale) }, diff --git a/web/app/components/goto-anything/actions/commands/types.ts b/web/app/components/goto-anything/actions/commands/types.ts index 528883c25f1..f44df073468 100644 --- a/web/app/components/goto-anything/actions/commands/types.ts +++ b/web/app/components/goto-anything/actions/commands/types.ts @@ -4,7 +4,7 @@ import type { CommandSearchResult } from '../types' * Slash command handler interface * Each slash command should implement this interface */ -export type SlashCommandHandler = { +export type SlashCommandHandler = { /** Command name (e.g., 'theme', 'language') */ name: string @@ -39,7 +39,7 @@ export type SlashCommandHandler = { * @param args Command arguments (part after removing command name) * @param locale Current language */ - search: (args: string, locale?: string) => Promise + search: (args: string, locale?: string) => CommandSearchResult[] /** * Called when registering command, passing external dependencies @@ -51,3 +51,5 @@ export type SlashCommandHandler = { */ unregister?: () => void } + +export type SlashCommand = Omit diff --git a/web/app/components/goto-anything/actions/index.ts b/web/app/components/goto-anything/actions/index.ts index 4a5a198c1df..939f3a929bb 100644 --- a/web/app/components/goto-anything/actions/index.ts +++ b/web/app/components/goto-anything/actions/index.ts @@ -1,169 +1,4 @@ -/** - * Goto Anything - Action System - * - * This file defines the action registry for the goto-anything search system. - * Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands. - * - * ## How to Add a New Slash Command - * - * 1. **Create Command Handler File** (in `./commands/` directory): - * ```typescript - * // commands/my-command.ts - * import type { SlashCommandHandler } from './types' - * import type { CommandSearchResult } from '../types' - * import { registerCommands, unregisterCommands } from './command-bus' - * - * interface MyCommandDeps { - * myService?: (data: any) => Promise - * } - * - * export const myCommand: SlashCommandHandler = { - * name: 'mycommand', - * aliases: ['mc'], // Optional aliases - * description: 'My custom command description', - * - * async search(args: string, locale: string = 'en') { - * // Return search results based on args - * return [{ - * id: 'my-result', - * title: 'My Command Result', - * description: 'Description of the result', - * type: 'command' as const, - * data: { command: 'my.action', args: { value: args } } - * }] - * }, - * - * register(deps: MyCommandDeps) { - * registerCommands({ - * 'my.action': async (args) => { - * await deps.myService?.(args?.value) - * } - * }) - * }, - * - * unregister() { - * unregisterCommands(['my.action']) - * } - * } - * ``` - * - * **Example for Self-Contained Command (no external dependencies):** - * ```typescript - * // commands/calculator-command.ts - * export const calculatorCommand: SlashCommandHandler = { - * name: 'calc', - * aliases: ['calculator'], - * description: 'Simple calculator', - * - * async search(args: string) { - * if (!args.trim()) return [] - * try { - * // Safe math evaluation (implement proper parser in real use) - * const result = Function('"use strict"; return (' + args + ')')() - * return [{ - * id: 'calc-result', - * title: `${args} = ${result}`, - * description: 'Calculator result', - * type: 'command' as const, - * data: { command: 'calc.copy', args: { result: result.toString() } } - * }] - * } catch { - * return [{ - * id: 'calc-error', - * title: 'Invalid expression', - * description: 'Please enter a valid math expression', - * type: 'command' as const, - * data: { command: 'calc.noop', args: {} } - * }] - * } - * }, - * - * register() { - * registerCommands({ - * 'calc.copy': (args) => navigator.clipboard.writeText(args.result), - * 'calc.noop': () => {} // No operation - * }) - * }, - * - * unregister() { - * unregisterCommands(['calc.copy', 'calc.noop']) - * } - * } - * ``` - * - * 2. **Register Command** (in `./commands/slash.tsx`): - * ```typescript - * import { myCommand } from './my-command' - * import { calculatorCommand } from './calculator-command' // For self-contained commands - * - * export const registerSlashCommands = (deps: Record) => { - * slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) - * slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) - * slashCommandRegistry.register(myCommand, { myService: deps.myService }) // With dependencies - * slashCommandRegistry.register(calculatorCommand) // Self-contained, no dependencies - * } - * - * export const unregisterSlashCommands = () => { - * slashCommandRegistry.unregister('theme') - * slashCommandRegistry.unregister('language') - * slashCommandRegistry.unregister('mycommand') - * slashCommandRegistry.unregister('calc') // Add this line - * } - * ``` - * - * - * 3. **Update SlashCommandProvider** (in `./commands/slash.tsx`): - * ```typescript - * export const SlashCommandProvider = () => { - * const theme = useTheme() - * const myService = useMyService() // Add external dependency if needed - * - * useEffect(() => { - * registerSlashCommands({ - * setTheme: theme.setTheme, // Required for theme command - * setLocale: setLocaleOnClient, // Required for language command - * myService: myService, // Required for your custom command - * // Note: calculatorCommand doesn't need dependencies, so not listed here - * }) - * return () => unregisterSlashCommands() - * }, [theme.setTheme, myService]) // Update dependency array for all dynamic deps - * - * return null - * } - * ``` - * - * **Note:** Self-contained commands (like calculator) don't require dependencies but are - * still registered through the same system for consistent lifecycle management. - * - * 4. **Usage**: Users can now type `/mycommand` or `/mc` to use your command - * - * ## Command System Architecture - * - Commands are registered via `SlashCommandRegistry` - * - Each command is self-contained with its own dependencies - * - Commands support aliases for easier access - * - Command execution is handled by the command bus system - * - All commands should be registered through `SlashCommandProvider` for consistent lifecycle management - * - * ## Command Types - * **Commands with External Dependencies:** - * - Require external services, APIs, or React hooks - * - Must provide dependencies in `SlashCommandProvider` - * - Example: theme commands (needs useTheme), API commands (needs service) - * - * **Self-Contained Commands:** - * - Pure logic operations, no external dependencies - * - Still recommended to register through `SlashCommandProvider` for consistency - * - Example: calculator, text manipulation commands - * - * ## Available Actions - * - `@app` - Search applications - * - `@knowledge` / `@kb` - Search knowledge bases - * - `@plugin` - Search plugins - * - `@node` - Search workflow nodes (workflow pages only) - * - `/` - Execute slash commands (theme, language, etc.) - */ - -import type { ActionItem, SearchResult } from './types' +import type { ActionItem } from './types' import { appAction } from './app' import { slashCommandRegistry } from './commands/registry' import { slashAction } from './commands/slash' @@ -172,120 +7,38 @@ import { pluginAction } from './plugin' import { ragPipelineNodesAction } from './rag-pipeline-nodes' import { workflowNodesAction } from './workflow-nodes' -// Create dynamic Actions based on context -export const createActions = (isWorkflowPage: boolean, isRagPipelinePage: boolean) => { - const baseActions = { - slash: slashAction, - app: appAction, - knowledge: knowledgeAction, - plugin: pluginAction, - } - - // Add appropriate node search based on context - if (isRagPipelinePage) { - return { - ...baseActions, - node: ragPipelineNodesAction, - } - } else if (isWorkflowPage) { - return { - ...baseActions, - node: workflowNodesAction, - } - } - - // Default actions without node search - return baseActions -} - -// Legacy export for backward compatibility -export const Actions = { +const defaultActions = { slash: slashAction, app: appAction, knowledge: knowledgeAction, plugin: pluginAction, - node: workflowNodesAction, +} satisfies Record + +export function createActions(isWorkflowPage: boolean, isRagPipelinePage: boolean) { + if (isRagPipelinePage) return { ...defaultActions, node: ragPipelineNodesAction } + if (isWorkflowPage) return { ...defaultActions, node: workflowNodesAction } + return defaultActions } -export const searchAnything = async ( - locale: string, - query: string, - actionItem?: ActionItem, - dynamicActions?: Record, -): Promise => { - const trimmedQuery = query.trim() - - if (actionItem) { - const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - const prefixPattern = new RegExp( - `^(${escapeRegExp(actionItem.key)}|${escapeRegExp(actionItem.shortcut)})\\s*`, - ) - const searchTerm = trimmedQuery.replace(prefixPattern, '').trim() - try { - return await actionItem.search(query, searchTerm, locale) - } catch (error) { - console.warn(`Search failed for ${actionItem.key}:`, error) - return [] - } - } - - if (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/')) return [] - - const globalSearchActions = Object.values(dynamicActions || Actions) - // Exclude slash commands from general search results - .filter((action) => action.key !== '/') - - // Use Promise.allSettled to handle partial failures gracefully - const searchPromises = globalSearchActions.map(async (action) => { - try { - const results = await action.search(query, query, locale) - return { success: true, data: results, actionType: action.key } - } catch (error) { - console.warn(`Search failed for ${action.key}:`, error) - return { success: false, data: [], actionType: action.key, error } - } - }) - - const settledResults = await Promise.allSettled(searchPromises) - - const allResults: SearchResult[] = [] - const failedActions: string[] = [] - - settledResults.forEach((result, index) => { - if (result.status === 'fulfilled' && result.value.success) { - allResults.push(...result.value.data) - } else { - const actionKey = globalSearchActions[index]?.key || 'unknown' - failedActions.push(actionKey) - } - }) - - if (failedActions.length > 0) - console.warn(`Some search actions failed: ${failedActions.join(', ')}`) - - return allResults +export function getActionSearchTerm(query: string, action: ActionItem) { + const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const prefixPattern = new RegExp( + `^(${escapeRegExp(action.key)}|${escapeRegExp(action.shortcut)})\\s*`, + ) + return query.trim().replace(prefixPattern, '').trim() } -export const matchAction = (query: string, actions: Record) => { +export function matchAction(query: string, actions: Record) { return Object.values(actions).find((action) => { - // Special handling for slash commands if (action.key === '/') { - // Get all registered commands from the registry - const allCommands = slashCommandRegistry.getAllCommands() + return slashCommandRegistry.getAllCommands().some((command) => { + if (command.mode === 'direct') return false - // Check if query matches any registered command - return allCommands.some((cmd) => { - const cmdPattern = `/${cmd.name}` - - // For direct mode commands, don't match (keep in command selector) - if (cmd.mode === 'direct') return false - - // For submenu mode commands, match when complete command is entered - return query === cmdPattern || query.startsWith(`${cmdPattern} `) + const commandPattern = `/${command.name}` + return query === commandPattern || query.startsWith(`${commandPattern} `) }) } - const reg = new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`) - return reg.test(query) + return new RegExp(`^(${action.key}|${action.shortcut})(?:\\s|$)`).test(query) }) } diff --git a/web/app/components/goto-anything/actions/knowledge.tsx b/web/app/components/goto-anything/actions/knowledge.tsx index ff5770ab44d..69eb4b326a7 100644 --- a/web/app/components/goto-anything/actions/knowledge.tsx +++ b/web/app/components/goto-anything/actions/knowledge.tsx @@ -1,13 +1,13 @@ +import type { DatasetListItemResponse } from '@dify/contracts/api/console/datasets/types.gen' import type { ActionItem, KnowledgeSearchResult } from './types' -import type { DataSet } from '@/models/datasets' import { cn } from '@langgenius/dify-ui/cn' -import { fetchDatasets } from '@/service/datasets' +import { consoleQuery } from '@/service/client' import { Folder } from '../../base/icons/src/vender/solid/files' const EXTERNAL_PROVIDER = 'external' as const const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER -const parser = (datasets: DataSet[]): KnowledgeSearchResult[] => { +function getKnowledgeResults(datasets: DatasetListItemResponse[]): KnowledgeSearchResult[] { return datasets.map((dataset) => { const path = isExternalProvider(dataset.provider) ? `/datasets/${dataset.id}/hitTesting` @@ -15,7 +15,7 @@ const parser = (datasets: DataSet[]): KnowledgeSearchResult[] => { return { id: dataset.id, title: dataset.name, - description: dataset.description, + description: dataset.description ?? undefined, type: 'knowledge' as const, path, icon: ( @@ -38,22 +38,19 @@ export const knowledgeAction: ActionItem = { shortcut: '@kb', title: 'Search Knowledge Bases', description: 'Search and navigate to your knowledge bases', - // action, - search: async (_, searchTerm = '', _locale) => { - try { - const response = await fetchDatasets({ - url: '/datasets', - params: { - page: 1, - limit: 10, - keyword: searchTerm, - }, - }) - const datasets = response?.data || [] - return parser(datasets) - } catch (error) { - console.warn('Knowledge search failed:', error) - return [] - } - }, + source: 'remote', +} + +export function knowledgeSearchQueryOptions(searchTerm: string) { + return consoleQuery.datasets.get.queryOptions({ + input: { + query: { + page: 1, + limit: 10, + keyword: searchTerm, + }, + }, + retry: false, + select: (response) => getKnowledgeResults(response.data), + }) } diff --git a/web/app/components/goto-anything/actions/plugin.tsx b/web/app/components/goto-anything/actions/plugin.tsx index 9038d318190..b25a138231c 100644 --- a/web/app/components/goto-anything/actions/plugin.tsx +++ b/web/app/components/goto-anything/actions/plugin.tsx @@ -1,8 +1,7 @@ -import type { PluginsFromMarketplaceResponse } from '@dify/contracts/marketplace' import type { Plugin } from '../../plugins/types' import type { ActionItem, PluginSearchResult } from './types' import { renderI18nObject } from '@/i18n-config' -import { postMarketplace } from '@/service/base' +import { marketplaceQuery } from '@/service/client' import Icon from '../../plugins/card/base/card-icon' import { getFormattedPlugin } from '../../plugins/marketplace/utils' @@ -24,30 +23,26 @@ export const pluginAction: ActionItem = { shortcut: '@plugin', title: 'Search Plugins', description: 'Search and navigate to your plugins', - search: async (_, searchTerm = '', locale) => { - try { - const response = await postMarketplace<{ data: PluginsFromMarketplaceResponse }>( - '/plugins/search/advanced', - { - body: { - page: 1, - page_size: 10, - query: searchTerm, - type: 'plugin', - }, - }, - ) - - if (!response?.data?.plugins) { - console.warn('Plugin search: Unexpected response structure', response) - return [] - } - - const list = response.data.plugins.map((plugin) => getFormattedPlugin(plugin)) - return parser(list, locale!) - } catch (error) { - console.warn('Plugin search failed:', error) - return [] - } - }, + source: 'remote', +} + +export function pluginSearchQueryOptions(searchTerm: string, locale: string) { + return marketplaceQuery.searchAdvanced.queryOptions({ + input: { + params: { kind: 'plugins' }, + body: { + page: 1, + page_size: 10, + query: searchTerm, + }, + }, + retry: false, + select: (response) => { + const plugins = response.data?.plugins ?? [] + return parser( + plugins.map((plugin) => getFormattedPlugin(plugin)), + locale, + ) + }, + }) } diff --git a/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx b/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx index d9c03c5824c..138fb65635d 100644 --- a/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx +++ b/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx @@ -1,19 +1,15 @@ import type { ActionItem } from './types' +import { findRagPipelineNodes } from '@/app/components/rag-pipeline/goto-anything-search' -// Create the RAG pipeline nodes action export const ragPipelineNodesAction: ActionItem = { key: '@node', shortcut: '@node', title: 'Search RAG Pipeline Nodes', description: 'Find and jump to nodes in the current RAG pipeline by name or type', - searchFn: undefined, // Will be set by useRagPipelineSearch hook - search: async (_, searchTerm = '', _locale) => { + source: 'local', + search: (_, searchTerm = '', _locale) => { try { - // Use the searchFn if available (set by useRagPipelineSearch hook) - if (ragPipelineNodesAction.searchFn) return ragPipelineNodesAction.searchFn(searchTerm) - - // If not in RAG pipeline context, return empty array - return [] + return findRagPipelineNodes(searchTerm) } catch (error) { console.warn('RAG pipeline nodes search failed:', error) return [] diff --git a/web/app/components/goto-anything/actions/types.ts b/web/app/components/goto-anything/actions/types.ts index b03b2e2773b..248369af0df 100644 --- a/web/app/components/goto-anything/actions/types.ts +++ b/web/app/components/goto-anything/actions/types.ts @@ -1,13 +1,13 @@ +import type { AppPartial } from '@dify/contracts/api/console/apps/types.gen' +import type { DatasetListItemResponse } from '@dify/contracts/api/console/datasets/types.gen' import type { ReactNode } from 'react' import type { TypeWithI18N } from '../../base/form/types' import type { Plugin } from '../../plugins/types' import type { CommonNodeType } from '../../workflow/types' -import type { DataSet } from '@/models/datasets' -import type { App } from '@/types/app' type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' | 'recent' -type BaseSearchResult = { +type BaseSearchResult = { id: string title: string description?: string @@ -19,7 +19,7 @@ type BaseSearchResult = { export type AppSearchResult = { type: 'app' -} & BaseSearchResult +} & BaseSearchResult export type PluginSearchResult = { type: 'plugin' @@ -27,7 +27,7 @@ export type PluginSearchResult = { export type KnowledgeSearchResult = { type: 'knowledge' -} & BaseSearchResult +} & BaseSearchResult type WorkflowNodeSearchResult = { type: 'workflow-node' @@ -39,7 +39,7 @@ type WorkflowNodeSearchResult = { export type CommandSearchResult = { type: 'command' -} & BaseSearchResult<{ command: string; args?: Record }> +} & BaseSearchResult<{ command: string; args?: Record }> export type RecentSearchResult = { type: 'recent' @@ -54,16 +54,21 @@ export type SearchResult = | CommandSearchResult | RecentSearchResult -export type ActionItem = { +type ActionItemBase = { key: '@app' | '@knowledge' | '@plugin' | '@node' | '/' shortcut: string title: string | TypeWithI18N description: string action?: (data: SearchResult) => void - searchFn?: (searchTerm: string) => SearchResult[] - search: ( - query: string, - searchTerm: string, - locale?: string, - ) => Promise | SearchResult[] } + +type RemoteActionItem = ActionItemBase & { + source: 'remote' +} + +type LocalActionItem = ActionItemBase & { + source: 'local' + search: (query: string, searchTerm: string, locale?: string) => SearchResult[] +} + +export type ActionItem = RemoteActionItem | LocalActionItem diff --git a/web/app/components/goto-anything/actions/workflow-nodes.tsx b/web/app/components/goto-anything/actions/workflow-nodes.tsx index a641b721a9d..9da25f409c6 100644 --- a/web/app/components/goto-anything/actions/workflow-nodes.tsx +++ b/web/app/components/goto-anything/actions/workflow-nodes.tsx @@ -1,19 +1,15 @@ import type { ActionItem } from './types' +import { findWorkflowNodes } from '@/app/components/workflow/goto-anything-search' -// Create the workflow nodes action export const workflowNodesAction: ActionItem = { key: '@node', shortcut: '@node', title: 'Search Workflow Nodes', description: 'Find and jump to nodes in the current workflow by name or type', - searchFn: undefined, // Will be set by useWorkflowSearch hook - search: async (_, searchTerm = '', _locale) => { + source: 'local', + search: (_, searchTerm = '', _locale) => { try { - // Use the searchFn if available (set by useWorkflowSearch hook) - if (workflowNodesAction.searchFn) return workflowNodesAction.searchFn(searchTerm) - - // If not in workflow context, return empty array - return [] + return findWorkflowNodes(searchTerm) } catch (error) { console.warn('Workflow nodes search failed:', error) return [] diff --git a/web/app/components/goto-anything/atoms.ts b/web/app/components/goto-anything/atoms.ts deleted file mode 100644 index 50ea4c6ffbe..00000000000 --- a/web/app/components/goto-anything/atoms.ts +++ /dev/null @@ -1,13 +0,0 @@ -'use client' - -import { atom, useAtomValue, useSetAtom } from 'jotai' - -const gotoAnythingOpenAtom = atom(false) - -export function useGotoAnythingOpen() { - return useAtomValue(gotoAnythingOpenAtom) -} - -export function useSetGotoAnythingOpen() { - return useSetAtom(gotoAnythingOpenAtom) -} diff --git a/web/app/components/goto-anything/command-selector.tsx b/web/app/components/goto-anything/command-selector.tsx deleted file mode 100644 index 30b7584bd92..00000000000 --- a/web/app/components/goto-anything/command-selector.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import type { FC } from 'react' -import type { ActionItem } from './actions/types' -import { Command } from 'cmdk' -import { useEffect, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { slashCommandRegistry } from './actions/commands/registry' - -type Props = Readonly<{ - actions: Record - onCommandSelect: (commandKey: string) => void - searchFilter?: string - commandValue?: string - onCommandValueChange?: (value: string) => void - originalQuery?: string -}> - -const slashCommandDescriptionKeys = { - '/create': 'gotoAnything.actions.createCategoryDesc', - '/refine': 'gotoAnything.actions.refineCategoryDesc', - '/theme': 'gotoAnything.actions.themeCategoryDesc', - '/language': 'gotoAnything.actions.languageChangeDesc', - '/account': 'gotoAnything.actions.accountDesc', - '/feedback': 'gotoAnything.actions.feedbackDesc', - '/docs': 'gotoAnything.actions.docDesc', - '/community': 'gotoAnything.actions.communityDesc', -} as const - -const actionDescriptionKeys = { - '@app': 'gotoAnything.actions.searchApplicationsDesc', - '@plugin': 'gotoAnything.actions.searchPluginsDesc', - '@knowledge': 'gotoAnything.actions.searchKnowledgeBasesDesc', - '@node': 'gotoAnything.actions.searchWorkflowNodesDesc', -} as const - -const CommandSelector: FC = ({ - actions, - onCommandSelect, - searchFilter, - commandValue, - onCommandValueChange, - originalQuery, -}) => { - const { t } = useTranslation() - - // Check if we're in slash command mode - const isSlashMode = originalQuery?.trim().startsWith('/') || false - - // Get slash commands from registry - const slashCommands = useMemo(() => { - if (!isSlashMode) return [] - - const availableCommands = slashCommandRegistry.getAvailableCommands() - const filter = searchFilter?.toLowerCase() || '' // searchFilter already has '/' removed - - return availableCommands - .filter((cmd) => { - if (!filter) return true - return cmd.name.toLowerCase().includes(filter) - }) - .map((cmd) => ({ - key: `/${cmd.name}`, - shortcut: `/${cmd.name}`, - title: cmd.name, - description: cmd.description, - })) - }, [isSlashMode, searchFilter]) - - const filteredActions = useMemo(() => { - if (isSlashMode) return [] - - return Object.values(actions).filter((action) => { - // Exclude slash action when in @ mode - if (action.key === '/') return false - if (!searchFilter) return true - const filterLower = searchFilter.toLowerCase() - return action.shortcut.toLowerCase().includes(filterLower) - }) - }, [actions, searchFilter, isSlashMode]) - - const allItems = isSlashMode ? slashCommands : filteredActions - - useEffect(() => { - if (allItems.length > 0 && onCommandValueChange) { - const currentValueExists = allItems.some((item) => item.shortcut === commandValue) - if (!currentValueExists) onCommandValueChange(allItems[0]!.shortcut) - } - }, [allItems, commandValue, onCommandValueChange]) - - if (allItems.length === 0) { - return ( -
-
-
-
- {t(($) => $['gotoAnything.noMatchingCommands'], { ns: 'app' })} -
-
- {t(($) => $['gotoAnything.tryDifferentSearch'], { ns: 'app' })} -
-
-
-
- ) - } - - return ( -
-
- {isSlashMode - ? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' }) - : t(($) => $['gotoAnything.selectSearchType'], { ns: 'app' })} -
- - {allItems.map((item) => ( - onCommandSelect(item.shortcut)} - > - - {item.shortcut} - - - {isSlashMode - ? t( - ($) => - $[ - slashCommandDescriptionKeys[ - item.key as keyof typeof slashCommandDescriptionKeys - ] || item.description - ], - { ns: 'app' }, - ) - : t( - ($) => $[actionDescriptionKeys[item.key as keyof typeof actionDescriptionKeys]], - { ns: 'app' }, - )} - - - ))} - -
- ) -} - -export default CommandSelector diff --git a/web/app/components/goto-anything/components/__tests__/empty-state.spec.tsx b/web/app/components/goto-anything/components/__tests__/empty-state.spec.tsx index aab213dde55..de4cc97be4d 100644 --- a/web/app/components/goto-anything/components/__tests__/empty-state.spec.tsx +++ b/web/app/components/goto-anything/components/__tests__/empty-state.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import EmptyState from '../empty-state' +import { EmptyState } from '../empty-state' describe('EmptyState', () => { describe('loading variant', () => { @@ -73,11 +73,11 @@ describe('EmptyState', () => { }) it('should show specific search hint with shortcuts', () => { - const Actions = { + const actions = { app: { key: '@app', shortcut: '@app' }, plugin: { key: '@plugin', shortcut: '@plugin' }, } as unknown as Record - render() + render() expect( screen.getByText( @@ -145,7 +145,7 @@ describe('EmptyState', () => { expect(screen.getByText('app.gotoAnything.noResults')).toBeInTheDocument() }) - it('should use empty object as default Actions', () => { + it('should use empty object as default actions', () => { render() expect( diff --git a/web/app/components/goto-anything/components/__tests__/footer.spec.tsx b/web/app/components/goto-anything/components/__tests__/footer.spec.tsx index 93239079de3..1763404a13c 100644 --- a/web/app/components/goto-anything/components/__tests__/footer.spec.tsx +++ b/web/app/components/goto-anything/components/__tests__/footer.spec.tsx @@ -1,261 +1,53 @@ import { render, screen } from '@testing-library/react' -import Footer from '../footer' +import { Footer } from '../footer' + +const defaultProps = { + resultCount: 0, + searchMode: 'general', + isLoading: false, + hasUnavailableServices: false, + isCommandsMode: false, + hasQuery: false, +} describe('Footer', () => { - describe('left content', () => { - describe('when there are results', () => { - it('should show result count', () => { - render( -
, - ) + it('shows the result count and active scope', () => { + render(
) - expect(screen.getByText('app.gotoAnything.resultCount:{"count":5}')).toBeInTheDocument() - }) - - it('should show scope when not in general mode', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.inScope:{"scope":"app"}')).toBeInTheDocument() - }) - - it('should NOT show scope when in general mode', () => { - render( -
, - ) - - expect(screen.queryByText(/inScope/)).not.toBeInTheDocument() - }) - }) - - describe('when there is an error', () => { - it('should show error message', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.someServicesUnavailable')).toBeInTheDocument() - }) - - it('should have red text styling', () => { - const { container } = render( -
, - ) - - const errorText = container.querySelector('.text-red-500') - expect(errorText).toBeInTheDocument() - }) - - it('should show error even with results', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.someServicesUnavailable')).toBeInTheDocument() - }) - }) - - describe('when no results and no error', () => { - it('should show select to navigate in commands mode', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.selectToNavigate')).toBeInTheDocument() - }) - - it('should show searching when has query', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.searching')).toBeInTheDocument() - }) - - it('should show start typing when no query', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.startTyping')).toBeInTheDocument() - }) - }) + expect(screen.getByText('app.gotoAnything.resultCount:{"count":3}')).toBeInTheDocument() + expect(screen.getByText('app.gotoAnything.inScope:{"scope":"app"}')).toBeInTheDocument() + expect(screen.getByText('app.gotoAnything.clearToSearchAll')).toBeInTheDocument() }) - describe('right content', () => { - describe('when there are results or error', () => { - it('should show clear to search all when in specific mode', () => { - render( -
, - ) + it('reports partial provider failure even when results remain available', () => { + render(
) - expect(screen.getByText('app.gotoAnything.clearToSearchAll')).toBeInTheDocument() - }) - - it('should show use @ for specific when in general mode', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.useAtForSpecific')).toBeInTheDocument() - }) - - it('should show same hint when error', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.useAtForSpecific')).toBeInTheDocument() - }) - }) - - describe('when no results and no error', () => { - it('should show tips when has query', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.tips')).toBeInTheDocument() - }) - - it('should show tips when in commands mode', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.tips')).toBeInTheDocument() - }) - - it('should show press ESC to close when no query and not in commands mode', () => { - render( -
, - ) - - expect(screen.getByText('app.gotoAnything.pressEscToClose')).toBeInTheDocument() - }) - }) + expect(screen.getByText('app.gotoAnything.someServicesUnavailable')).toHaveClass('text-red-500') + expect(screen.getByText('app.gotoAnything.useAtForSpecific')).toBeInTheDocument() }) - describe('styling', () => { - it('should have border and background classes', () => { - const { container } = render( -
, - ) + it('reports pending remote search', () => { + render(
) - const footer = container.firstChild - expect(footer).toHaveClass('border-t', 'border-divider-subtle', 'bg-components-panel-bg-blur') - }) + expect(screen.getByText('app.gotoAnything.searching')).toBeInTheDocument() + expect(screen.getByText('app.gotoAnything.tips')).toBeInTheDocument() + }) - it('should have flex layout for content', () => { - const { container } = render( -
, - ) + it('reports command selection mode', () => { + render(
) - const flexContainer = container.querySelector('.flex.items-center.justify-between') - expect(flexContainer).toBeInTheDocument() - }) + expect(screen.getByText('app.gotoAnything.selectToNavigate')).toBeInTheDocument() + }) + + it('shows the idle shortcut hint', () => { + const { container } = render(
) + + expect(screen.getByText('app.gotoAnything.startTyping')).toBeInTheDocument() + expect(screen.getByText('app.gotoAnything.pressEscToClose')).toBeInTheDocument() + expect(container.firstChild).toHaveClass( + 'border-t', + 'border-divider-subtle', + 'bg-components-panel-bg-blur', + ) }) }) diff --git a/web/app/components/goto-anything/components/__tests__/result-item.spec.tsx b/web/app/components/goto-anything/components/__tests__/result-item.spec.tsx deleted file mode 100644 index d3f42069e95..00000000000 --- a/web/app/components/goto-anything/components/__tests__/result-item.spec.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import type { SearchResult } from '../../actions/types' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { Command } from 'cmdk' -import ResultItem from '../result-item' - -function renderInCommandRoot(ui: React.ReactElement) { - return render({ui}) -} - -function createResult(overrides: Partial = {}): SearchResult { - return { - id: 'test-1', - title: 'Test Result', - type: 'app', - data: {}, - ...overrides, - } as SearchResult -} - -describe('ResultItem', () => { - it('renders title', () => { - renderInCommandRoot( - , - ) - - expect(screen.getByText('My App')).toBeInTheDocument() - }) - - it('renders description when provided', () => { - renderInCommandRoot( - , - ) - - expect(screen.getByText('A great app')).toBeInTheDocument() - }) - - it('does not render description when absent', () => { - const result = createResult() - delete (result as Record).description - - renderInCommandRoot() - - expect(screen.getByText('Test Result')).toBeInTheDocument() - expect(screen.getByText('app')).toBeInTheDocument() - }) - - it('renders result type label', () => { - renderInCommandRoot() - - expect(screen.getByText('plugin')).toBeInTheDocument() - }) - - it('renders icon when provided', () => { - const icon = icon - renderInCommandRoot() - - expect(screen.getByTestId('custom-icon')).toBeInTheDocument() - }) - - it('calls onSelect when clicked', async () => { - const user = userEvent.setup() - const onSelect = vi.fn() - - renderInCommandRoot() - - await user.click(screen.getByText('Test Result')) - - expect(onSelect).toHaveBeenCalled() - }) -}) diff --git a/web/app/components/goto-anything/components/__tests__/result-list.spec.tsx b/web/app/components/goto-anything/components/__tests__/result-list.spec.tsx deleted file mode 100644 index 1405244036f..00000000000 --- a/web/app/components/goto-anything/components/__tests__/result-list.spec.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { SearchResult } from '../../actions/types' -import { render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { Command } from 'cmdk' -import ResultList from '../result-list' - -function renderInCommandRoot(ui: React.ReactElement) { - return render({ui}) -} - -function createResult(overrides: Partial = {}): SearchResult { - return { - id: 'test-1', - title: 'Result 1', - type: 'app', - data: {}, - ...overrides, - } as SearchResult -} - -describe('ResultList', () => { - it('renders grouped results with headings', () => { - const grouped: Record = { - app: [createResult({ id: 'a1', title: 'App One', type: 'app' })], - plugin: [createResult({ id: 'p1', title: 'Plugin One', type: 'plugin' })], - } - - renderInCommandRoot() - - expect(screen.getByText('App One')).toBeInTheDocument() - expect(screen.getByText('Plugin One')).toBeInTheDocument() - }) - - it('renders multiple results in the same group', () => { - const grouped: Record = { - app: [ - createResult({ id: 'a1', title: 'App One', type: 'app' }), - createResult({ id: 'a2', title: 'App Two', type: 'app' }), - ], - } - - renderInCommandRoot() - - expect(screen.getByText('App One')).toBeInTheDocument() - expect(screen.getByText('App Two')).toBeInTheDocument() - }) - - it('calls onSelect with the correct result when clicked', async () => { - const user = userEvent.setup() - const onSelect = vi.fn() - const result = createResult({ id: 'a1', title: 'Click Me', type: 'app' }) - - renderInCommandRoot() - - await user.click(screen.getByText('Click Me')) - - expect(onSelect).toHaveBeenCalledWith(result) - }) - - it('renders empty when no grouped results provided', () => { - const { container } = renderInCommandRoot() - - const groups = container.querySelectorAll('[cmdk-group]') - expect(groups).toHaveLength(0) - }) - - it('uses i18n keys for known group types', () => { - const grouped: Record = { - command: [createResult({ id: 'c1', title: 'Cmd', type: 'command' })], - } - - renderInCommandRoot() - - expect(screen.getByText('app.gotoAnything.groups.commands')).toBeInTheDocument() - }) -}) diff --git a/web/app/components/goto-anything/components/__tests__/search-input.spec.tsx b/web/app/components/goto-anything/components/__tests__/search-input.spec.tsx deleted file mode 100644 index e337ae2518b..00000000000 --- a/web/app/components/goto-anything/components/__tests__/search-input.spec.tsx +++ /dev/null @@ -1,191 +0,0 @@ -import type { ChangeEvent, KeyboardEvent, RefObject } from 'react' -import { fireEvent, render, screen } from '@testing-library/react' -import SearchInput from '../search-input' - -vi.mock('@remixicon/react', () => ({ - RiSearchLine: ({ className }: { className?: string }) => ( - - ), -})) - -vi.mock('@/app/components/base/input', async () => { - const { forwardRef } = await import('react') - - type MockInputProps = { - value?: string - placeholder?: string - onChange?: (e: ChangeEvent) => void - onKeyDown?: (e: KeyboardEvent) => void - className?: string - wrapperClassName?: string - autoFocus?: boolean - } - - const MockInput = forwardRef( - ({ value, placeholder, onChange, onKeyDown, className, wrapperClassName, autoFocus }, ref) => ( - - ), - ) - MockInput.displayName = 'MockInput' - - return { default: MockInput } -}) - -describe('SearchInput', () => { - const defaultProps = { - inputRef: { current: null } as RefObject, - value: '', - onChange: vi.fn(), - searchMode: 'general', - } - - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('rendering', () => { - it('should render search icon', () => { - render() - - expect(screen.getByTestId('search-icon')).toBeInTheDocument() - }) - - it('should render input field', () => { - render() - - expect(screen.getByTestId('search-input')).toBeInTheDocument() - }) - - it('should render shortcut keycaps', () => { - const { container } = render() - - const keycaps = container.querySelectorAll('kbd') - expect(keycaps).toHaveLength(2) - expect(keycaps[1]).toHaveTextContent('K') - }) - - it('should use provided placeholder', () => { - render() - - expect(screen.getByPlaceholderText('Custom placeholder')).toBeInTheDocument() - }) - - it('should use default placeholder from translation', () => { - render() - - expect(screen.getByPlaceholderText('app.gotoAnything.searchPlaceholder')).toBeInTheDocument() - }) - }) - - describe('mode label', () => { - it('should NOT show mode badge in general mode', () => { - render() - - expect(screen.queryByText('GENERAL')).not.toBeInTheDocument() - }) - - it('should show SCOPES label in scopes mode', () => { - render() - - expect(screen.getByText('SCOPES')).toBeInTheDocument() - }) - - it('should show COMMANDS label in commands mode', () => { - render() - - expect(screen.getByText('COMMANDS')).toBeInTheDocument() - }) - - it('should show APP label in @app mode', () => { - render() - - expect(screen.getByText('APP')).toBeInTheDocument() - }) - - it('should show PLUGIN label in @plugin mode', () => { - render() - - expect(screen.getByText('PLUGIN')).toBeInTheDocument() - }) - - it('should show KNOWLEDGE label in @knowledge mode', () => { - render() - - expect(screen.getByText('KNOWLEDGE')).toBeInTheDocument() - }) - - it('should show NODE label in @node mode', () => { - render() - - expect(screen.getByText('NODE')).toBeInTheDocument() - }) - - it('should uppercase custom mode label', () => { - render() - - expect(screen.getByText('CUSTOM')).toBeInTheDocument() - }) - }) - - describe('input interactions', () => { - it('should call onChange when typing', () => { - const onChange = vi.fn() - render() - - const input = screen.getByTestId('search-input') - fireEvent.change(input, { target: { value: 'test query' } }) - - expect(onChange).toHaveBeenCalledWith('test query') - }) - - it('should call onKeyDown when pressing keys', () => { - const onKeyDown = vi.fn() - render() - - const input = screen.getByTestId('search-input') - fireEvent.keyDown(input, { key: 'Enter' }) - - expect(onKeyDown).toHaveBeenCalled() - }) - - it('should render with provided value', () => { - render() - - expect(screen.getByDisplayValue('existing query')).toBeInTheDocument() - }) - - it('should NOT throw when onKeyDown is undefined', () => { - render() - - const input = screen.getByTestId('search-input') - expect(() => fireEvent.keyDown(input, { key: 'Enter' })).not.toThrow() - }) - }) - - describe('styling', () => { - it('should have search icon styling', () => { - render() - - const icon = screen.getByTestId('search-icon') - expect(icon).toHaveClass('size-4', 'text-text-quaternary') - }) - - it('should have mode badge styling when visible', () => { - const { container } = render() - - const badge = container.querySelector('.bg-gray-100') - expect(badge).toBeInTheDocument() - expect(badge).toHaveClass('rounded-sm', 'px-2', 'text-xs', 'font-medium') - }) - }) -}) diff --git a/web/app/components/goto-anything/components/empty-state.tsx b/web/app/components/goto-anything/components/empty-state.tsx index ad70c5cb605..b7c1ff2825a 100644 --- a/web/app/components/goto-anything/components/empty-state.tsx +++ b/web/app/components/goto-anything/components/empty-state.tsx @@ -1,6 +1,5 @@ 'use client' -import type { FC } from 'react' import type { ActionItem } from '../actions/types' import { useTranslation } from 'react-i18next' @@ -10,15 +9,15 @@ type EmptyStateProps = { variant: EmptyStateVariant searchMode?: string error?: Error | null - Actions?: Record + actions?: Record } -const EmptyState: FC = ({ +export function EmptyState({ variant, searchMode = 'general', error, - Actions = {}, -}) => { + actions = {}, +}: EmptyStateProps) { const { t } = useTranslation() if (variant === 'loading') { @@ -67,7 +66,6 @@ const EmptyState: FC = ({ ) } - // variant === 'no-results' const isCommandSearch = searchMode !== 'general' const commandType = isCommandSearch ? searchMode.replace('@', '') : '' @@ -93,7 +91,7 @@ const EmptyState: FC = ({ return t(($) => $['gotoAnything.emptyState.tryDifferentTerm'], { ns: 'app' }) } - const shortcuts = Object.values(Actions) + const shortcuts = Object.values(actions) .map((action) => action.shortcut) .join(', ') return t(($) => $['gotoAnything.emptyState.trySpecificSearch'], { ns: 'app', shortcuts }) @@ -108,5 +106,3 @@ const EmptyState: FC = ({
) } - -export default EmptyState diff --git a/web/app/components/goto-anything/components/footer.tsx b/web/app/components/goto-anything/components/footer.tsx index 0eaf005e7de..7de6a01672c 100644 --- a/web/app/components/goto-anything/components/footer.tsx +++ b/web/app/components/goto-anything/components/footer.tsx @@ -1,35 +1,36 @@ 'use client' -import type { FC } from 'react' import { useTranslation } from 'react-i18next' type FooterProps = { resultCount: number searchMode: string - isError: boolean + isLoading: boolean + hasUnavailableServices: boolean isCommandsMode: boolean hasQuery: boolean } -const Footer: FC = ({ +export function Footer({ resultCount, searchMode, - isError, + isLoading, + hasUnavailableServices, isCommandsMode, hasQuery, -}) => { +}: FooterProps) { const { t } = useTranslation() const renderLeftContent = () => { - if (resultCount > 0 || isError) { - if (isError) { - return ( - - {t(($) => $['gotoAnything.someServicesUnavailable'], { ns: 'app' })} - - ) - } + if (hasUnavailableServices) { + return ( + + {t(($) => $['gotoAnything.someServicesUnavailable'], { ns: 'app' })} + + ) + } + if (resultCount > 0) { return ( <> {t(($) => $['gotoAnything.resultCount'], { ns: 'app', count: resultCount })} @@ -50,7 +51,7 @@ const Footer: FC = ({ {(() => { if (isCommandsMode) return t(($) => $['gotoAnything.selectToNavigate'], { ns: 'app' }) - if (hasQuery) return t(($) => $['gotoAnything.searching'], { ns: 'app' }) + if (isLoading) return t(($) => $['gotoAnything.searching'], { ns: 'app' }) return t(($) => $['gotoAnything.startTyping'], { ns: 'app' }) })()} @@ -59,7 +60,7 @@ const Footer: FC = ({ } const renderRightContent = () => { - if (resultCount > 0 || isError) { + if (resultCount > 0 || hasUnavailableServices) { return ( {searchMode !== 'general' @@ -87,5 +88,3 @@ const Footer: FC = ({
) } - -export default Footer diff --git a/web/app/components/goto-anything/components/index.ts b/web/app/components/goto-anything/components/index.ts deleted file mode 100644 index d430a156967..00000000000 --- a/web/app/components/goto-anything/components/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { default as EmptyState } from './empty-state' - -export { default as Footer } from './footer' - -export { default as ResultList } from './result-list' - -export { default as SearchInput } from './search-input' diff --git a/web/app/components/goto-anything/components/result-item.tsx b/web/app/components/goto-anything/components/result-item.tsx deleted file mode 100644 index 413382c2691..00000000000 --- a/web/app/components/goto-anything/components/result-item.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { SearchResult } from '../actions/types' -import { Command } from 'cmdk' - -type ResultItemProps = { - result: SearchResult - onSelect: () => void -} - -const ResultItem: FC = ({ result, onSelect }) => { - return ( - - {result.icon} -
-
{result.title}
- {result.description && ( -
{result.description}
- )} -
-
{result.type}
-
- ) -} - -export default ResultItem diff --git a/web/app/components/goto-anything/components/result-list.tsx b/web/app/components/goto-anything/components/result-list.tsx deleted file mode 100644 index ca113e1b5db..00000000000 --- a/web/app/components/goto-anything/components/result-list.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client' - -import type { FC } from 'react' -import type { SearchResult } from '../actions/types' -import { Command } from 'cmdk' -import { useTranslation } from 'react-i18next' -import ResultItem from './result-item' - -type ResultListProps = { - groupedResults: Record - onSelect: (result: SearchResult) => void -} - -const ResultList: FC = ({ groupedResults, onSelect }) => { - const { t } = useTranslation() - - const getGroupHeading = (type: string) => { - const typeMap = { - app: 'gotoAnything.groups.apps', - plugin: 'gotoAnything.groups.plugins', - knowledge: 'gotoAnything.groups.knowledgeBases', - 'workflow-node': 'gotoAnything.groups.workflowNodes', - command: 'gotoAnything.groups.commands', - recent: 'gotoAnything.groups.recent', - } as const - return t(($) => $[typeMap[type as keyof typeof typeMap] || `${type}s`], { ns: 'app' }) - } - - return ( - <> - {Object.entries(groupedResults).map(([type, results]) => ( - - {results.map((result) => ( - onSelect(result)} - /> - ))} - - ))} - - ) -} - -export default ResultList diff --git a/web/app/components/goto-anything/components/search-input.tsx b/web/app/components/goto-anything/components/search-input.tsx deleted file mode 100644 index e1ac951ce31..00000000000 --- a/web/app/components/goto-anything/components/search-input.tsx +++ /dev/null @@ -1,64 +0,0 @@ -'use client' - -import type { FC, KeyboardEvent, RefObject } from 'react' -import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' -import { RiSearchLine } from '@remixicon/react' -import { formatForDisplay } from '@tanstack/react-hotkeys' -import { useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' - -type SearchInputProps = { - inputRef: RefObject - value: string - onChange: (value: string) => void - onKeyDown?: (e: KeyboardEvent) => void - searchMode: string - placeholder?: string -} - -const SearchInput: FC = ({ - inputRef, - value, - onChange, - onKeyDown, - searchMode, - placeholder, -}) => { - const { t } = useTranslation() - - const getModeLabel = () => { - if (searchMode === 'scopes') return 'SCOPES' - else if (searchMode === 'commands') return 'COMMANDS' - else return searchMode.replace('@', '').toUpperCase() - } - - return ( -
- -
- $['gotoAnything.searchPlaceholder'], { ns: 'app' })} - onChange={(e) => onChange(e.target.value)} - onKeyDown={onKeyDown} - className="flex-1 border-0! bg-transparent! shadow-none!" - wrapperClassName="flex-1 border-0! bg-transparent!" - autoFocus - /> - {searchMode !== 'general' && ( -
- {getModeLabel()} -
- )} -
- - {['Mod', 'K'].map((key) => ( - {formatForDisplay(key)} - ))} - -
- ) -} - -export default SearchInput diff --git a/web/app/components/goto-anything/context.tsx b/web/app/components/goto-anything/context.tsx deleted file mode 100644 index 53c3ab1e1b2..00000000000 --- a/web/app/components/goto-anything/context.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client' - -import type { ReactNode } from 'react' -import * as React from 'react' -import { createContext, use, useEffect, useState } from 'react' -import { usePathname } from '@/next/navigation' -import { isInWorkflowPage } from '../workflow/constants' - -/** - * Interface for the GotoAnything context - */ -type GotoAnythingContextType = { - /** - * Whether the current page is a workflow page - */ - isWorkflowPage: boolean - /** - * Whether the current page is a RAG pipeline page - */ - isRagPipelinePage: boolean -} - -// Create context with default values -const GotoAnythingContext = createContext({ - isWorkflowPage: false, - isRagPipelinePage: false, -}) - -/** - * Hook to use the GotoAnything context - */ -export const useGotoAnythingContext = () => use(GotoAnythingContext) - -type GotoAnythingProviderProps = { - children: ReactNode -} - -/** - * Provider component for GotoAnything context - */ -export const GotoAnythingProvider: React.FC = ({ children }) => { - const [isWorkflowPage, setIsWorkflowPage] = useState(false) - const [isRagPipelinePage, setIsRagPipelinePage] = useState(false) - const pathname = usePathname() - - // Update context based on current pathname using more robust route matching - useEffect(() => { - if (!pathname) { - setIsWorkflowPage(false) - setIsRagPipelinePage(false) - return - } - - // Workflow pages: /app/[appId]/workflow or /workflow/[token] (shared) - const isWorkflow = isInWorkflowPage() - // RAG Pipeline pages: /datasets/[datasetId]/pipeline - const isRagPipeline = /^\/datasets\/[^/]+\/pipeline$/.test(pathname) - - setIsWorkflowPage(isWorkflow) - setIsRagPipelinePage(isRagPipeline) - }, [pathname]) - - return ( - - {children} - - ) -} diff --git a/web/app/components/goto-anything/dialog-handle.ts b/web/app/components/goto-anything/dialog-handle.ts new file mode 100644 index 00000000000..2fccb17ec53 --- /dev/null +++ b/web/app/components/goto-anything/dialog-handle.ts @@ -0,0 +1,5 @@ +'use client' + +import { createDialogHandle } from '@langgenius/dify-ui/dialog' + +export const gotoAnythingDialogHandle = createDialogHandle() diff --git a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts b/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts deleted file mode 100644 index 0f6219d45f9..00000000000 --- a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-modal.spec.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { ReactNode } from 'react' -import { act, renderHook } from '@testing-library/react' -import { createStore, Provider } from 'jotai' -import { createElement } from 'react' -import { useGotoAnythingModal } from '../use-goto-anything-modal' - -type KeyPressEvent = { - preventDefault: () => void - target?: EventTarget -} - -type HotkeyRegistration = { - handler: (event: KeyPressEvent) => void - options?: { enabled?: boolean; ignoreInputs?: boolean } -} - -const hotkeyHandlers: Record = {} - -vi.mock('@tanstack/react-hotkeys', () => ({ - useHotkey: ( - hotkey: string, - handler: (event: KeyPressEvent) => void, - options?: HotkeyRegistration['options'], - ) => { - hotkeyHandlers[hotkey] = { handler, options } - }, -})) - -const triggerHotkey = (hotkey: string, event: KeyPressEvent) => { - const registration = hotkeyHandlers[hotkey] - if (registration?.options?.enabled === false) return - - registration?.handler(event) -} - -const renderGotoAnythingModalHook = () => { - const store = createStore() - const wrapper = ({ children }: { children: ReactNode }) => - createElement(Provider, { store }, children) - - return renderHook(() => useGotoAnythingModal(), { wrapper }) -} - -describe('useGotoAnythingModal', () => { - beforeEach(() => { - Object.keys(hotkeyHandlers).forEach((key) => delete hotkeyHandlers[key]) - vi.useFakeTimers() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - describe('initialization', () => { - it('should initialize with open=false', () => { - const { result } = renderGotoAnythingModalHook() - expect(result.current.open).toBe(false) - }) - - it('should provide inputRef initialized to null', () => { - const { result } = renderGotoAnythingModalHook() - expect(result.current.inputRef).toBeDefined() - expect(result.current.inputRef.current).toBe(null) - }) - - it('should provide onOpenChange function', () => { - const { result } = renderGotoAnythingModalHook() - expect(typeof result.current.onOpenChange).toBe('function') - }) - }) - - describe('keyboard shortcuts', () => { - it('should toggle open state when Mod+K is triggered', () => { - const { result } = renderGotoAnythingModalHook() - - expect(result.current.open).toBe(false) - - act(() => { - triggerHotkey('Mod+K', { preventDefault: vi.fn(), target: document.body }) - }) - - expect(result.current.open).toBe(true) - }) - - it('should toggle back to closed when Mod+K is triggered twice', () => { - const { result } = renderGotoAnythingModalHook() - - act(() => { - triggerHotkey('Mod+K', { preventDefault: vi.fn(), target: document.body }) - }) - expect(result.current.open).toBe(true) - - act(() => { - triggerHotkey('Mod+K', { preventDefault: vi.fn(), target: document.body }) - }) - expect(result.current.open).toBe(false) - }) - - it('should let the hotkey library ignore inputs when the modal is closed', () => { - renderGotoAnythingModalHook() - - expect(hotkeyHandlers['Mod+K']?.options?.ignoreInputs).toBe(true) - }) - - it('should not register a separate escape hotkey', () => { - renderGotoAnythingModalHook() - - expect(hotkeyHandlers.Escape).toBeUndefined() - }) - - it('should call preventDefault when Mod+K is triggered', () => { - renderGotoAnythingModalHook() - - const preventDefaultMock = vi.fn() - act(() => { - triggerHotkey('Mod+K', { preventDefault: preventDefaultMock, target: document.body }) - }) - - expect(preventDefaultMock).toHaveBeenCalled() - }) - }) - - describe('onOpenChange', () => { - it('should accept boolean value', () => { - const { result } = renderGotoAnythingModalHook() - - act(() => { - result.current.onOpenChange(true) - }) - expect(result.current.open).toBe(true) - - act(() => { - result.current.onOpenChange(false) - }) - expect(result.current.open).toBe(false) - }) - }) - - describe('focus management', () => { - it('should call requestAnimationFrame when modal opens', () => { - const rafSpy = vi.spyOn(window, 'requestAnimationFrame') - const { result } = renderGotoAnythingModalHook() - - act(() => { - result.current.onOpenChange(true) - }) - - expect(rafSpy).toHaveBeenCalled() - rafSpy.mockRestore() - }) - - it('should not call requestAnimationFrame when modal closes', () => { - const { result } = renderGotoAnythingModalHook() - - act(() => { - result.current.onOpenChange(true) - }) - - const rafSpy = vi.spyOn(window, 'requestAnimationFrame') - - act(() => { - result.current.onOpenChange(false) - }) - - expect(rafSpy).not.toHaveBeenCalled() - rafSpy.mockRestore() - }) - - it('should focus input when modal opens and inputRef.current exists', () => { - const originalRAF = window.requestAnimationFrame - window.requestAnimationFrame = (callback: FrameRequestCallback) => { - callback(0) - return 0 - } - - const { result } = renderGotoAnythingModalHook() - - const mockFocus = vi.fn() - const mockInput = { focus: mockFocus } as unknown as HTMLInputElement - - Object.defineProperty(result.current.inputRef, 'current', { - value: mockInput, - writable: true, - }) - - act(() => { - result.current.onOpenChange(true) - }) - - expect(mockFocus).toHaveBeenCalled() - - window.requestAnimationFrame = originalRAF - }) - - it('should not throw when inputRef.current is null when modal opens', () => { - const originalRAF = window.requestAnimationFrame - window.requestAnimationFrame = (callback: FrameRequestCallback) => { - callback(0) - return 0 - } - - const { result } = renderGotoAnythingModalHook() - - act(() => { - result.current.onOpenChange(true) - }) - - expect(result.current.open).toBe(true) - - window.requestAnimationFrame = originalRAF - }) - }) -}) diff --git a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-navigation.spec.ts b/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-navigation.spec.ts deleted file mode 100644 index d26071baf2f..00000000000 --- a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-navigation.spec.ts +++ /dev/null @@ -1,496 +0,0 @@ -import type * as React from 'react' -import type { Plugin } from '../../../plugins/types' -import type { CommonNodeType } from '../../../workflow/types' -import type { DataSet } from '@/models/datasets' -import type { App } from '@/types/app' -import { act, renderHook } from '@testing-library/react' -import { useGotoAnythingNavigation } from '../use-goto-anything-navigation' - -const mockRouterPush = vi.fn() -const mockSelectWorkflowNode = vi.fn() -const mockAddRecentItem = vi.fn() - -type MockCommandResult = { - mode: string - execute?: () => void -} | null - -let mockFindCommandResult: MockCommandResult = null - -vi.mock('@/next/navigation', () => ({ - useRouter: () => ({ - push: mockRouterPush, - }), -})) - -vi.mock('@/app/components/workflow/utils/node-navigation', () => ({ - selectWorkflowNode: (...args: unknown[]) => mockSelectWorkflowNode(...args), -})) - -vi.mock('../../actions/commands/registry', () => ({ - slashCommandRegistry: { - findCommand: () => mockFindCommandResult, - }, -})) - -vi.mock('../../actions/recent-store', () => ({ - addRecentItem: (...args: unknown[]) => mockAddRecentItem(...args), -})) - -const createMockActionItem = ( - key: '@app' | '@knowledge' | '@plugin' | '@node' | '/', - extra: Record = {}, -) => ({ - key, - shortcut: key, - title: `${key} title`, - description: `${key} description`, - search: vi.fn().mockResolvedValue([]), - ...extra, -}) - -const createMockOptions = (overrides = {}) => ({ - Actions: { - slash: createMockActionItem('/', { action: vi.fn() }), - app: createMockActionItem('@app'), - }, - setSearchQuery: vi.fn(), - clearSelection: vi.fn(), - inputRef: { current: { focus: vi.fn() } } as unknown as React.RefObject, - onClose: vi.fn(), - ...overrides, -}) - -describe('useGotoAnythingNavigation', () => { - beforeEach(() => { - vi.clearAllMocks() - mockFindCommandResult = null - vi.useFakeTimers() - }) - - afterEach(() => { - vi.useRealTimers() - }) - - describe('initialization', () => { - it('should return handleCommandSelect function', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - expect(typeof result.current.handleCommandSelect).toBe('function') - }) - - it('should return handleNavigate function', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - expect(typeof result.current.handleNavigate).toBe('function') - }) - - it('should initialize activePlugin as undefined', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - expect(result.current.activePlugin).toBeUndefined() - }) - - it('should return setActivePlugin function', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - expect(typeof result.current.setActivePlugin).toBe('function') - }) - }) - - describe('handleCommandSelect', () => { - it('should execute direct mode slash command immediately', () => { - const execute = vi.fn() - mockFindCommandResult = { mode: 'direct', execute } - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('/theme') - }) - - expect(execute).toHaveBeenCalled() - expect(options.onClose).toHaveBeenCalled() - expect(options.setSearchQuery).toHaveBeenCalledWith('') - }) - - it('should NOT execute when handler has no execute function', () => { - mockFindCommandResult = { mode: 'direct', execute: undefined } - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('/theme') - }) - - expect(options.onClose).not.toHaveBeenCalled() - expect(options.setSearchQuery).toHaveBeenCalledWith('/theme ') - }) - - it('should proceed with submenu mode for non-direct commands', () => { - mockFindCommandResult = { mode: 'submenu' } - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('/language') - }) - - expect(options.setSearchQuery).toHaveBeenCalledWith('/language ') - expect(options.clearSelection).toHaveBeenCalled() - }) - - it('should handle @ commands (scopes)', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('@app') - }) - - expect(options.setSearchQuery).toHaveBeenCalledWith('@app ') - expect(options.clearSelection).toHaveBeenCalled() - }) - - it('should focus input after setting search query', () => { - const focusMock = vi.fn() - const options = createMockOptions({ - inputRef: { current: { focus: focusMock } }, - }) - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('@app') - }) - - act(() => { - vi.runAllTimers() - }) - - expect(focusMock).toHaveBeenCalled() - }) - - it('should handle null handler from registry', () => { - mockFindCommandResult = null - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('/unknown') - }) - - expect(options.setSearchQuery).toHaveBeenCalledWith('/unknown ') - }) - }) - - describe('handleNavigate', () => { - it('should navigate to path for default result types', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: '1', - type: 'app' as const, - title: 'My App', - path: '/apps/1', - data: { id: '1', name: 'My App' } as unknown as App, - }) - }) - - expect(options.onClose).toHaveBeenCalled() - expect(options.setSearchQuery).toHaveBeenCalledWith('') - expect(mockRouterPush).toHaveBeenCalledWith('/apps/1') - }) - - it('should NOT call router.push when path is empty', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: '1', - type: 'app' as const, - title: 'My App', - path: '', - data: { id: '1', name: 'My App' } as unknown as App, - }) - }) - - expect(mockRouterPush).not.toHaveBeenCalled() - }) - - it('should execute slash command action for command type', () => { - const actionMock = vi.fn() - const options = createMockOptions({ - Actions: { - slash: { key: '/', shortcut: '/', action: actionMock }, - }, - }) - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - const commandResult = { - id: 'cmd-1', - type: 'command' as const, - title: 'Theme Dark', - data: { command: 'theme.set', args: { theme: 'dark' } }, - } - - act(() => { - result.current.handleNavigate(commandResult) - }) - - expect(actionMock).toHaveBeenCalledWith(commandResult) - }) - - it('should set activePlugin for plugin type', () => { - const options = createMockOptions() - const pluginData = { - name: 'My Plugin', - latest_package_identifier: 'pkg', - } as unknown as Plugin - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'plugin-1', - type: 'plugin' as const, - title: 'My Plugin', - data: pluginData, - }) - }) - - expect(result.current.activePlugin).toEqual(pluginData) - }) - - it('should select workflow node for workflow-node type', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'node-1', - type: 'workflow-node' as const, - title: 'Start Node', - metadata: { nodeId: 'node-123', nodeData: {} as CommonNodeType }, - data: { id: 'node-1' } as unknown as CommonNodeType, - }) - }) - - expect(mockSelectWorkflowNode).toHaveBeenCalledWith('node-123', true) - }) - - it('should NOT select workflow node when metadata.nodeId is missing', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'node-1', - type: 'workflow-node' as const, - title: 'Start Node', - metadata: undefined, - data: { id: 'node-1' } as unknown as CommonNodeType, - }) - }) - - expect(mockSelectWorkflowNode).not.toHaveBeenCalled() - }) - - it('should handle knowledge type (default case with path)', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'kb-1', - type: 'knowledge' as const, - title: 'My Knowledge Base', - path: '/datasets/kb-1', - data: { id: 'kb-1', name: 'My Knowledge Base' } as unknown as DataSet, - }) - }) - - expect(mockRouterPush).toHaveBeenCalledWith('/datasets/kb-1') - }) - - it('should record app navigation to recent history', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'app-1', - type: 'app' as const, - title: 'My App', - description: 'Desc', - path: '/app/app-1', - data: { id: 'app-1', name: 'My App' } as unknown as App, - }) - }) - - expect(mockAddRecentItem).toHaveBeenCalledWith({ - id: 'app-1', - title: 'My App', - description: 'Desc', - path: '/app/app-1', - originalType: 'app', - }) - }) - - it('should record knowledge navigation to recent history', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'kb-1', - type: 'knowledge' as const, - title: 'My KB', - path: '/datasets/kb-1', - data: { id: 'kb-1', name: 'My KB' } as unknown as DataSet, - }) - }) - - expect(mockAddRecentItem).toHaveBeenCalledWith( - expect.objectContaining({ id: 'kb-1', originalType: 'knowledge' }), - ) - }) - - it('should NOT record to recent history when path is missing', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'app-1', - type: 'app' as const, - title: 'My App', - path: '', - data: { id: 'app-1', name: 'My App' } as unknown as App, - }) - }) - - expect(mockAddRecentItem).not.toHaveBeenCalled() - }) - - it('should navigate for recent type without recording again', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'recent-app-1', - type: 'recent' as const, - originalType: 'app', - title: 'My App', - path: '/app/app-1', - data: { path: '/app/app-1' }, - }) - }) - - expect(mockRouterPush).toHaveBeenCalledWith('/app/app-1') - expect(mockAddRecentItem).not.toHaveBeenCalled() - }) - - it('should NOT call router.push for recent type when path is missing', () => { - const options = createMockOptions() - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'recent-app-1', - type: 'recent' as const, - originalType: 'app', - title: 'My App', - data: { path: '' }, - }) - }) - - expect(mockRouterPush).not.toHaveBeenCalled() - }) - }) - - describe('setActivePlugin', () => { - it('should update activePlugin state', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - - const plugin = { - name: 'Test Plugin', - latest_package_identifier: 'test-pkg', - } as unknown as Plugin - act(() => { - result.current.setActivePlugin(plugin) - }) - - expect(result.current.activePlugin).toEqual(plugin) - }) - - it('should clear activePlugin when set to undefined', () => { - const { result } = renderHook(() => useGotoAnythingNavigation(createMockOptions())) - - act(() => { - result.current.setActivePlugin({ - name: 'Plugin', - latest_package_identifier: 'pkg', - } as unknown as Plugin) - }) - expect(result.current.activePlugin).toBeDefined() - - act(() => { - result.current.setActivePlugin(undefined) - }) - - expect(result.current.activePlugin).toBeUndefined() - }) - }) - - describe('edge cases', () => { - it('should handle undefined inputRef.current', () => { - const options = createMockOptions({ - inputRef: { current: null }, - }) - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleCommandSelect('@app') - }) - - act(() => { - vi.runAllTimers() - }) - }) - - it('should handle missing slash action', () => { - const options = createMockOptions({ - Actions: {}, - }) - - const { result } = renderHook(() => useGotoAnythingNavigation(options)) - - act(() => { - result.current.handleNavigate({ - id: 'cmd-1', - type: 'command' as const, - title: 'Command', - data: { command: 'test-command' }, - }) - }) - }) - }) -}) diff --git a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-results.spec.ts b/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-results.spec.ts deleted file mode 100644 index 1451fde2229..00000000000 --- a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-results.spec.ts +++ /dev/null @@ -1,453 +0,0 @@ -import type { SearchResult } from '../../actions/types' -import { renderHook } from '@testing-library/react' -import { useGotoAnythingResults } from '../use-goto-anything-results' - -type MockQueryResult = { - data: Array<{ id: string; type: string; title: string }> | undefined - isLoading: boolean - isError: boolean - error: Error | null -} - -type UseQueryOptions = { - queryFn: () => Promise -} - -let mockQueryResult: MockQueryResult = { data: [], isLoading: false, isError: false, error: null } -let capturedQueryFn: (() => Promise) | null = null - -vi.mock('@tanstack/react-query', () => ({ - useQuery: (options: UseQueryOptions) => { - capturedQueryFn = options.queryFn - return mockQueryResult - }, -})) -const mockMatchAction = vi.fn() -const mockSearchAnything = vi.fn() -const mockGetRecentItems = vi.fn(() => [] as Array>) - -vi.mock('../../actions', () => ({ - matchAction: (...args: unknown[]) => mockMatchAction(...args), - searchAnything: (...args: unknown[]) => mockSearchAnything(...args), -})) - -vi.mock('../../actions/recent-store', () => ({ - getRecentItems: () => mockGetRecentItems(), -})) - -const createMockActionItem = (key: '@app' | '@knowledge' | '@plugin' | '@node' | '/') => ({ - key, - shortcut: key, - title: `${key} title`, - description: `${key} description`, - search: vi.fn().mockResolvedValue([]), -}) - -const createMockOptions = (overrides = {}) => ({ - searchQueryDebouncedValue: '', - searchMode: 'general', - isCommandsMode: false, - Actions: { app: createMockActionItem('@app') }, - isWorkflowPage: false, - isRagPipelinePage: false, - cmdVal: '_', - setCmdVal: vi.fn(), - ...overrides, -}) - -describe('useGotoAnythingResults', () => { - beforeEach(() => { - mockQueryResult = { data: [], isLoading: false, isError: false, error: null } - capturedQueryFn = null - mockMatchAction.mockReset() - mockSearchAnything.mockReset() - mockGetRecentItems.mockReturnValue([]) - }) - - describe('initialization', () => { - it('should return empty arrays when no results', () => { - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.searchResults).toEqual([]) - expect(result.current.dedupedResults).toEqual([]) - expect(result.current.groupedResults).toEqual({}) - }) - - it('should return loading state', () => { - mockQueryResult = { data: [], isLoading: true, isError: false, error: null } - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.isLoading).toBe(true) - }) - - it('should return error state', () => { - const error = new Error('Test error') - mockQueryResult = { data: [], isLoading: false, isError: true, error } - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.isError).toBe(true) - expect(result.current.error).toBe(error) - }) - }) - - describe('dedupedResults', () => { - it('should remove duplicate results', () => { - mockQueryResult = { - data: [ - { id: '1', type: 'app', title: 'App 1' }, - { id: '1', type: 'app', title: 'App 1 Duplicate' }, - { id: '2', type: 'app', title: 'App 2' }, - ], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.dedupedResults).toHaveLength(2) - expect(result.current.dedupedResults[0]!.id).toBe('1') - expect(result.current.dedupedResults[1]!.id).toBe('2') - }) - - it('should keep first occurrence when duplicates exist', () => { - mockQueryResult = { - data: [ - { id: '1', type: 'app', title: 'First' }, - { id: '1', type: 'app', title: 'Second' }, - ], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.dedupedResults).toHaveLength(1) - expect(result.current.dedupedResults[0]!.title).toBe('First') - }) - - it('should handle different types with same id', () => { - mockQueryResult = { - data: [ - { id: '1', type: 'app', title: 'App' }, - { id: '1', type: 'plugin', title: 'Plugin' }, - ], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.dedupedResults).toHaveLength(2) - }) - }) - - describe('groupedResults', () => { - it('should group results by type', () => { - mockQueryResult = { - data: [ - { id: '1', type: 'app', title: 'App 1' }, - { id: '2', type: 'app', title: 'App 2' }, - { id: '3', type: 'plugin', title: 'Plugin 1' }, - ], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.groupedResults.app).toHaveLength(2) - expect(result.current.groupedResults.plugin).toHaveLength(1) - }) - - it('should handle single type', () => { - mockQueryResult = { - data: [ - { id: '1', type: 'knowledge', title: 'KB 1' }, - { id: '2', type: 'knowledge', title: 'KB 2' }, - ], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(Object.keys(result.current.groupedResults)).toEqual(['knowledge']) - expect(result.current.groupedResults.knowledge).toHaveLength(2) - }) - - it('should return empty object when no results', () => { - mockQueryResult = { data: [], isLoading: false, isError: false, error: null } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.groupedResults).toEqual({}) - }) - }) - - describe('auto-select first result', () => { - it('should call setCmdVal when results change and current value does not exist', () => { - const setCmdVal = vi.fn() - mockQueryResult = { - data: [{ id: '1', type: 'app', title: 'App 1' }], - isLoading: false, - isError: false, - error: null, - } - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - cmdVal: 'non-existent', - setCmdVal, - }), - ), - ) - - expect(setCmdVal).toHaveBeenCalledWith('app-1') - }) - - it('should NOT call setCmdVal when in commands mode', () => { - const setCmdVal = vi.fn() - mockQueryResult = { - data: [{ id: '1', type: 'app', title: 'App 1' }], - isLoading: false, - isError: false, - error: null, - } - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - isCommandsMode: true, - setCmdVal, - }), - ), - ) - - expect(setCmdVal).not.toHaveBeenCalled() - }) - - it('should NOT call setCmdVal when results are empty', () => { - const setCmdVal = vi.fn() - mockQueryResult = { data: [], isLoading: false, isError: false, error: null } - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - setCmdVal, - }), - ), - ) - - expect(setCmdVal).not.toHaveBeenCalled() - }) - - it('should NOT call setCmdVal when current value exists in results', () => { - const setCmdVal = vi.fn() - mockQueryResult = { - data: [ - { id: '1', type: 'app', title: 'App 1' }, - { id: '2', type: 'app', title: 'App 2' }, - ], - isLoading: false, - isError: false, - error: null, - } - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - cmdVal: 'app-2', - setCmdVal, - }), - ), - ) - - expect(setCmdVal).not.toHaveBeenCalled() - }) - }) - - describe('error handling', () => { - it('should return error as Error | null', () => { - const error = new Error('Search failed') - mockQueryResult = { data: [], isLoading: false, isError: true, error } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.error).toBeInstanceOf(Error) - expect(result.current.error?.message).toBe('Search failed') - }) - - it('should return null error when no error', () => { - mockQueryResult = { data: [], isLoading: false, isError: false, error: null } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.error).toBeNull() - }) - }) - - describe('searchResults', () => { - it('should return raw search results', () => { - const mockData = [ - { id: '1', type: 'app', title: 'App 1' }, - { id: '2', type: 'plugin', title: 'Plugin 1' }, - ] - mockQueryResult = { data: mockData, isLoading: false, isError: false, error: null } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.searchResults).toEqual(mockData) - }) - - it('should default to empty array when data is undefined', () => { - mockQueryResult = { data: undefined, isLoading: false, isError: false, error: null } - - const { result } = renderHook(() => useGotoAnythingResults(createMockOptions())) - - expect(result.current.searchResults).toEqual([]) - }) - }) - - describe('recent results', () => { - it('surfaces recent items when the search query is empty', () => { - mockGetRecentItems.mockReturnValue([ - { - id: 'app-1', - title: 'My App', - description: 'Desc', - path: '/app/app-1', - originalType: 'app', - }, - { id: 'kb-1', title: 'My KB', path: '/datasets/kb-1', originalType: 'knowledge' }, - ]) - - const { result } = renderHook(() => - useGotoAnythingResults( - createMockOptions({ - searchQueryDebouncedValue: '', - }), - ), - ) - - expect(result.current.dedupedResults).toHaveLength(2) - expect(result.current.dedupedResults[0]).toMatchObject({ - id: 'recent-app-1', - type: 'recent', - originalType: 'app', - path: '/app/app-1', - data: { path: '/app/app-1' }, - }) - expect(result.current.groupedResults.recent).toHaveLength(2) - }) - - it('does not surface recent items when a query is active', () => { - mockGetRecentItems.mockReturnValue([ - { id: 'app-1', title: 'My App', path: '/app/app-1', originalType: 'app' }, - ]) - mockQueryResult = { - data: [{ id: 's1', type: 'app', title: 'Searched' }], - isLoading: false, - isError: false, - error: null, - } - - const { result } = renderHook(() => - useGotoAnythingResults( - createMockOptions({ - searchQueryDebouncedValue: 'foo', - }), - ), - ) - - expect(result.current.dedupedResults.map((r) => r.id)).toEqual(['s1']) - }) - - it('does not surface recent items in commands mode', () => { - mockGetRecentItems.mockReturnValue([ - { id: 'app-1', title: 'My App', path: '/app/app-1', originalType: 'app' }, - ]) - - const { result } = renderHook(() => - useGotoAnythingResults( - createMockOptions({ - isCommandsMode: true, - }), - ), - ) - - expect(result.current.dedupedResults).toEqual([]) - }) - }) - - describe('queryFn execution', () => { - it('should call matchAction with lowercased query', async () => { - const mockActions = { app: createMockActionItem('@app') } - mockMatchAction.mockReturnValue({ key: '@app' }) - mockSearchAnything.mockResolvedValue([]) - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - searchQueryDebouncedValue: 'TEST QUERY', - Actions: mockActions, - }), - ), - ) - - expect(capturedQueryFn).toBeDefined() - await capturedQueryFn!() - - expect(mockMatchAction).toHaveBeenCalledWith('test query', mockActions) - }) - - it('should call searchAnything with correct parameters', async () => { - const mockActions = { app: createMockActionItem('@app') } - const mockAction = { key: '@app' } - mockMatchAction.mockReturnValue(mockAction) - mockSearchAnything.mockResolvedValue([{ id: '1', type: 'app', title: 'Result' }]) - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - searchQueryDebouncedValue: 'My Query', - Actions: mockActions, - }), - ), - ) - - expect(capturedQueryFn).toBeDefined() - const result = await capturedQueryFn!() - - expect(mockSearchAnything).toHaveBeenCalledWith('en_US', 'my query', mockAction, mockActions) - expect(result).toEqual([{ id: '1', type: 'app', title: 'Result' }]) - }) - - it('should handle searchAnything returning results', async () => { - const expectedResults = [ - { id: '1', type: 'app', title: 'App 1' }, - { id: '2', type: 'plugin', title: 'Plugin 1' }, - ] - mockMatchAction.mockReturnValue(null) - mockSearchAnything.mockResolvedValue(expectedResults) - - renderHook(() => - useGotoAnythingResults( - createMockOptions({ - searchQueryDebouncedValue: 'search term', - }), - ), - ) - - expect(capturedQueryFn).toBeDefined() - const result = await capturedQueryFn!() - - expect(result).toEqual(expectedResults) - }) - }) -}) diff --git a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-search.spec.ts b/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-search.spec.ts deleted file mode 100644 index f13fb21704d..00000000000 --- a/web/app/components/goto-anything/hooks/__tests__/use-goto-anything-search.spec.ts +++ /dev/null @@ -1,298 +0,0 @@ -import type { ActionItem } from '../../actions/types' -import { act, renderHook } from '@testing-library/react' -import { useGotoAnythingSearch } from '../use-goto-anything-search' - -let mockContextValue = { isWorkflowPage: false, isRagPipelinePage: false } -let mockMatchActionResult: Partial | undefined - -vi.mock('ahooks', () => ({ - useDebounce: (value: T) => value, -})) - -vi.mock('../../context', () => ({ - useGotoAnythingContext: () => mockContextValue, -})) - -vi.mock('../../actions', () => ({ - createActions: (isWorkflowPage: boolean, isRagPipelinePage: boolean) => { - const base = { - slash: { key: '/', shortcut: '/' }, - app: { key: '@app', shortcut: '@app' }, - knowledge: { key: '@knowledge', shortcut: '@kb' }, - } - if (isWorkflowPage) { - return { ...base, node: { key: '@node', shortcut: '@node' } } - } - if (isRagPipelinePage) { - return { ...base, ragNode: { key: '@node', shortcut: '@node' } } - } - return base - }, - matchAction: () => mockMatchActionResult, -})) - -describe('useGotoAnythingSearch', () => { - beforeEach(() => { - mockContextValue = { isWorkflowPage: false, isRagPipelinePage: false } - mockMatchActionResult = undefined - }) - - describe('initialization', () => { - it('should initialize with empty search query', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.searchQuery).toBe('') - }) - - it('should initialize cmdVal with "_"', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.cmdVal).toBe('_') - }) - - it('should initialize searchMode as "general"', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.searchMode).toBe('general') - }) - - it('should initialize isCommandsMode as false', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.isCommandsMode).toBe(false) - }) - - it('should provide setSearchQuery function', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(typeof result.current.setSearchQuery).toBe('function') - }) - - it('should provide setCmdVal function', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(typeof result.current.setCmdVal).toBe('function') - }) - - it('should provide clearSelection function', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(typeof result.current.clearSelection).toBe('function') - }) - }) - - describe('Actions', () => { - it('should provide Actions based on context', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.Actions).toBeDefined() - expect(typeof result.current.Actions).toBe('object') - }) - - it('should include node action when on workflow page', () => { - mockContextValue = { isWorkflowPage: true, isRagPipelinePage: false } - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.Actions.node).toBeDefined() - }) - - it('should include ragNode action when on RAG pipeline page', () => { - mockContextValue = { isWorkflowPage: false, isRagPipelinePage: true } - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.Actions.ragNode).toBeDefined() - }) - - it('should not include node actions when on regular page', () => { - mockContextValue = { isWorkflowPage: false, isRagPipelinePage: false } - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.Actions.node).toBeUndefined() - expect(result.current.Actions.ragNode).toBeUndefined() - }) - }) - - describe('isCommandsMode', () => { - it('should return true when query is exactly "@"', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('@') - }) - - expect(result.current.isCommandsMode).toBe(true) - }) - - it('should return true when query is exactly "/"', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('/') - }) - - expect(result.current.isCommandsMode).toBe(true) - }) - - it('should return true when query starts with "@" and no action matches', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('@unknown') - }) - - expect(result.current.isCommandsMode).toBe(true) - }) - - it('should return true when query starts with "/" and no action matches', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('/unknown') - }) - - expect(result.current.isCommandsMode).toBe(true) - }) - - it('should return false when query starts with "@" and action matches', () => { - mockMatchActionResult = { key: '@app', shortcut: '@app' } - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('@app test') - }) - - expect(result.current.isCommandsMode).toBe(false) - }) - - it('should return false for regular search query', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('hello world') - }) - - expect(result.current.isCommandsMode).toBe(false) - }) - }) - - describe('searchMode', () => { - it('should return "general" when query is empty', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - expect(result.current.searchMode).toBe('general') - }) - - it('should return "scopes" when in commands mode and query starts with "@"', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('@') - }) - - expect(result.current.searchMode).toBe('scopes') - }) - - it('should return "commands" when in commands mode and query starts with "/"', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('/') - }) - - expect(result.current.searchMode).toBe('commands') - }) - - it('should return "general" when no action matches', () => { - mockMatchActionResult = undefined - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('hello') - }) - - expect(result.current.searchMode).toBe('general') - }) - - it('should return action key when action matches', () => { - mockMatchActionResult = { key: '@app', shortcut: '@app' } - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('@app test') - }) - - expect(result.current.searchMode).toBe('@app') - }) - - it('should return "@command" when action key is "/"', () => { - mockMatchActionResult = { key: '/', shortcut: '/' } - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('/theme dark') - }) - - expect(result.current.searchMode).toBe('@command') - }) - }) - - describe('clearSelection', () => { - it('should reset cmdVal to "_"', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setCmdVal('app-1') - }) - expect(result.current.cmdVal).toBe('app-1') - - act(() => { - result.current.clearSelection() - }) - - expect(result.current.cmdVal).toBe('_') - }) - }) - - describe('setSearchQuery', () => { - it('should update search query', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('test query') - }) - - expect(result.current.searchQuery).toBe('test query') - }) - - it('should handle empty string', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery('test') - }) - expect(result.current.searchQuery).toBe('test') - - act(() => { - result.current.setSearchQuery('') - }) - expect(result.current.searchQuery).toBe('') - }) - }) - - describe('setCmdVal', () => { - it('should update cmdVal', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setCmdVal('plugin-2') - }) - - expect(result.current.cmdVal).toBe('plugin-2') - }) - }) - - describe('searchQueryDebouncedValue', () => { - it('should return trimmed debounced value', () => { - const { result } = renderHook(() => useGotoAnythingSearch()) - - act(() => { - result.current.setSearchQuery(' test ') - }) - - expect(result.current.searchQueryDebouncedValue).toBe('test') - }) - }) -}) diff --git a/web/app/components/goto-anything/hooks/use-goto-anything-modal.ts b/web/app/components/goto-anything/hooks/use-goto-anything-modal.ts deleted file mode 100644 index 50f1cb5abf9..00000000000 --- a/web/app/components/goto-anything/hooks/use-goto-anything-modal.ts +++ /dev/null @@ -1,43 +0,0 @@ -'use client' - -import type { RefObject } from 'react' -import { useHotkey } from '@tanstack/react-hotkeys' -import { useEffect, useRef } from 'react' -import { useGotoAnythingOpen, useSetGotoAnythingOpen } from '../atoms' - -type UseGotoAnythingModalReturn = { - open: boolean - onOpenChange: (open: boolean) => void - inputRef: RefObject -} - -export const useGotoAnythingModal = (): UseGotoAnythingModalReturn => { - const open = useGotoAnythingOpen() - const setOpen = useSetGotoAnythingOpen() - const inputRef = useRef(null) - - useHotkey( - 'Mod+K', - (e) => { - e.preventDefault() - setOpen((prev) => !prev) - }, - { - ignoreInputs: !open, - }, - ) - - useEffect(() => { - if (open) { - requestAnimationFrame(() => { - inputRef.current?.focus() - }) - } - }, [open]) - - return { - open, - onOpenChange: setOpen, - inputRef, - } -} diff --git a/web/app/components/goto-anything/hooks/use-goto-anything-navigation.ts b/web/app/components/goto-anything/hooks/use-goto-anything-navigation.ts deleted file mode 100644 index aece64f5e31..00000000000 --- a/web/app/components/goto-anything/hooks/use-goto-anything-navigation.ts +++ /dev/null @@ -1,109 +0,0 @@ -'use client' - -import type { RefObject } from 'react' -import type { Plugin } from '../../plugins/types' -import type { ActionItem, SearchResult } from '../actions/types' -import { useCallback, useState } from 'react' -import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' -import { useRouter } from '@/next/navigation' -import { slashCommandRegistry } from '../actions/commands/registry' -import { addRecentItem } from '../actions/recent-store' - -type UseGotoAnythingNavigationReturn = { - handleCommandSelect: (commandKey: string) => void - handleNavigate: (result: SearchResult) => void - activePlugin: Plugin | undefined - setActivePlugin: (plugin: Plugin | undefined) => void -} - -type UseGotoAnythingNavigationOptions = { - Actions: Record - setSearchQuery: (query: string) => void - clearSelection: () => void - inputRef: RefObject - onClose: () => void -} - -export const useGotoAnythingNavigation = ( - options: UseGotoAnythingNavigationOptions, -): UseGotoAnythingNavigationReturn => { - const { Actions, setSearchQuery, clearSelection, inputRef, onClose } = options - - const router = useRouter() - const [activePlugin, setActivePlugin] = useState() - - const handleCommandSelect = useCallback( - (commandKey: string) => { - // Check if it's a slash command - if (commandKey.startsWith('/')) { - const commandName = commandKey.substring(1) - const handler = slashCommandRegistry.findCommand(commandName) - - // If it's a direct mode command, execute immediately - if (handler?.mode === 'direct' && handler.execute) { - handler.execute() - onClose() - setSearchQuery('') - return - } - } - - // Otherwise, proceed with the normal flow (submenu mode) - setSearchQuery(`${commandKey} `) - clearSelection() - setTimeout(() => { - inputRef.current?.focus() - }, 0) - }, - [onClose, setSearchQuery, clearSelection, inputRef], - ) - - // Handle navigation to selected result - const handleNavigate = useCallback( - (result: SearchResult) => { - onClose() - setSearchQuery('') - - switch (result.type) { - case 'command': { - // Execute slash commands - const action = Actions.slash - action?.action?.(result) - break - } - case 'plugin': - setActivePlugin(result.data) - break - case 'workflow-node': - // Handle workflow node selection and navigation - if (result.metadata?.nodeId) selectWorkflowNode(result.metadata.nodeId, true) - - break - case 'recent': - if (result.path) router.push(result.path) - - break - default: - // Record to recent history for app and knowledge results - if ((result.type === 'app' || result.type === 'knowledge') && result.path) { - addRecentItem({ - id: result.id, - title: result.title, - description: result.description, - path: result.path, - originalType: result.type, - }) - } - if (result.path) router.push(result.path) - } - }, - [router, Actions, onClose, setSearchQuery], - ) - - return { - handleCommandSelect, - handleNavigate, - activePlugin, - setActivePlugin, - } -} diff --git a/web/app/components/goto-anything/hooks/use-goto-anything-results.ts b/web/app/components/goto-anything/hooks/use-goto-anything-results.ts deleted file mode 100644 index c12b90382c5..00000000000 --- a/web/app/components/goto-anything/hooks/use-goto-anything-results.ts +++ /dev/null @@ -1,147 +0,0 @@ -'use client' - -import type { ActionItem, RecentSearchResult, SearchResult } from '../actions/types' -import { RiTimeLine } from '@remixicon/react' -import { useQuery } from '@tanstack/react-query' -import * as React from 'react' -import { useEffect, useMemo } from 'react' -import { useGetLanguage } from '@/context/i18n' -import { matchAction, searchAnything } from '../actions' -import { getRecentItems } from '../actions/recent-store' - -type UseGotoAnythingResultsReturn = { - searchResults: SearchResult[] - dedupedResults: SearchResult[] - groupedResults: Record - isLoading: boolean - isError: boolean - error: Error | null -} - -type UseGotoAnythingResultsOptions = { - searchQueryDebouncedValue: string - searchMode: string - isCommandsMode: boolean - Actions: Record - isWorkflowPage: boolean - isRagPipelinePage: boolean - cmdVal: string - setCmdVal: (val: string) => void -} - -export const useGotoAnythingResults = ( - options: UseGotoAnythingResultsOptions, -): UseGotoAnythingResultsReturn => { - const { - searchQueryDebouncedValue, - searchMode, - isCommandsMode, - Actions, - isWorkflowPage, - isRagPipelinePage, - cmdVal, - setCmdVal, - } = options - - const defaultLocale = useGetLanguage() - - // Use action keys as stable cache key instead of the full Actions object - // (Actions contains functions which are not serializable) - const actionKeys = useMemo(() => Object.keys(Actions).sort(), [Actions]) - - const { - data: searchResults = [], - isLoading, - isError, - error, - } = useQuery({ - queryKey: [ - 'goto-anything', - 'search-result', - searchQueryDebouncedValue, - searchMode, - isWorkflowPage, - isRagPipelinePage, - defaultLocale, - actionKeys, - ], - queryFn: async () => { - const query = searchQueryDebouncedValue.toLowerCase() - const action = matchAction(query, Actions) - return await searchAnything(defaultLocale, query, action, Actions) - }, - enabled: !!searchQueryDebouncedValue && !isCommandsMode, - staleTime: 30000, - gcTime: 300000, - }) - - // Build recent items to show when search is empty - const recentResults = useMemo((): RecentSearchResult[] => { - if (searchQueryDebouncedValue || isCommandsMode) return [] - return getRecentItems().map((item) => ({ - id: `recent-${item.id}`, - title: item.title, - description: item.description, - type: 'recent' as const, - originalType: item.originalType, - path: item.path, - icon: React.createElement( - 'div', - { - className: - 'flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg', - }, - React.createElement(RiTimeLine, { className: 'h-4 w-4 text-text-tertiary' }), - ), - data: { path: item.path }, - })) - }, [searchQueryDebouncedValue, isCommandsMode]) - - const dedupedResults = useMemo(() => { - const allResults = recentResults.length ? recentResults : searchResults - const seen = new Set() - return allResults.filter((result) => { - const key = `${result.type}-${result.id}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - }, [searchResults, recentResults]) - - // Group results by type - const groupedResults = useMemo( - () => - dedupedResults.reduce( - (acc, result) => { - if (!acc[result.type]) acc[result.type] = [] - - acc[result.type]!.push(result) - return acc - }, - {} as Record, - ), - [dedupedResults], - ) - - // Auto-select first result when results change - useEffect(() => { - if (isCommandsMode) return - - if (!dedupedResults.length) return - - const currentValueExists = dedupedResults.some( - (result) => `${result.type}-${result.id}` === cmdVal, - ) - - if (!currentValueExists) setCmdVal(`${dedupedResults[0]!.type}-${dedupedResults[0]!.id}`) - }, [isCommandsMode, dedupedResults, cmdVal, setCmdVal]) - - return { - searchResults, - dedupedResults, - groupedResults, - isLoading, - isError, - error: error as Error | null, - } -} diff --git a/web/app/components/goto-anything/hooks/use-goto-anything-search.ts b/web/app/components/goto-anything/hooks/use-goto-anything-search.ts deleted file mode 100644 index 2c0a5a3d461..00000000000 --- a/web/app/components/goto-anything/hooks/use-goto-anything-search.ts +++ /dev/null @@ -1,77 +0,0 @@ -'use client' - -import type { ActionItem } from '../actions/types' -import { useDebounce } from 'ahooks' -import { useCallback, useMemo, useState } from 'react' -import { createActions, matchAction } from '../actions' -import { useGotoAnythingContext } from '../context' - -type UseGotoAnythingSearchReturn = { - searchQuery: string - setSearchQuery: (query: string) => void - searchQueryDebouncedValue: string - searchMode: string - isCommandsMode: boolean - cmdVal: string - setCmdVal: (val: string) => void - clearSelection: () => void - Actions: Record -} - -export const useGotoAnythingSearch = (): UseGotoAnythingSearchReturn => { - const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() - const [searchQuery, setSearchQuery] = useState('') - const [cmdVal, setCmdVal] = useState('_') - - // Filter actions based on context - const Actions = useMemo(() => { - return createActions(isWorkflowPage, isRagPipelinePage) - }, [isWorkflowPage, isRagPipelinePage]) - - const searchQueryDebouncedValue = useDebounce(searchQuery.trim(), { - wait: 300, - }) - - const isCommandsMode = useMemo(() => { - const trimmed = searchQuery.trim() - return ( - trimmed === '@' || - trimmed === '/' || - (trimmed.startsWith('@') && !matchAction(trimmed, Actions)) || - (trimmed.startsWith('/') && !matchAction(trimmed, Actions)) - ) - }, [searchQuery, Actions]) - - const searchMode = useMemo(() => { - if (isCommandsMode) { - // Distinguish between @ (scopes) and / (commands) mode - if (searchQuery.trim().startsWith('@')) return 'scopes' - else if (searchQuery.trim().startsWith('/')) return 'commands' - return 'commands' // default fallback - } - - const query = searchQueryDebouncedValue.toLowerCase() - const action = matchAction(query, Actions) - - if (!action) return 'general' - - return action.key === '/' ? '@command' : action.key - }, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery]) - - // Prevent automatic selection of the first option when cmdVal is not set - const clearSelection = useCallback(() => { - setCmdVal('_') - }, []) - - return { - searchQuery, - setSearchQuery, - searchQueryDebouncedValue, - searchMode, - isCommandsMode, - cmdVal, - setCmdVal, - clearSelection, - Actions, - } -} diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 5e8e1442d7c..147cc98e57e 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -1,191 +1,599 @@ 'use client' -import type { FC, KeyboardEvent } from 'react' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { Command } from 'cmdk' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import type { AutocompleteChangeEventDetails } from '@langgenius/dify-ui/autocomplete' +import type { Plugin } from '../plugins/types' +import type { ActionItem, RecentSearchResult, SearchResult } from './actions/types' +import { + Autocomplete, + AutocompleteCollection, + AutocompleteGroup, + AutocompleteGroupLabel, + AutocompleteInput, + AutocompleteInputGroup, + AutocompleteItem, + AutocompleteList, + AutocompleteStatus, +} from '@langgenius/dify-ui/autocomplete' +import { + Dialog, + DialogBackdrop, + DialogCloseButton, + DialogPopup, + DialogPortal, + DialogTitle, +} from '@langgenius/dify-ui/dialog' +import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' +import { + ScrollAreaContent, + ScrollAreaRoot, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, +} from '@langgenius/dify-ui/scroll-area' +import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' +import { useQuery } from '@tanstack/react-query' +import { useDebounce } from 'ahooks' +import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation' +import { useGetLanguage } from '@/context/i18n' +import { usePathname, useRouter } from '@/next/navigation' import { PluginInstallPermissionProvider } from '../plugins/install-plugin/components/plugin-install-permission-provider' import useWorkspacePluginInstallPermission from '../plugins/install-plugin/hooks/use-workspace-plugin-install-permission' import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace' +import { createActions, getActionSearchTerm, matchAction } from './actions' +import { appSearchQueryOptions } from './actions/app' import { slashCommandRegistry } from './actions/commands/registry' import { SlashCommandProvider } from './actions/commands/slash-provider' -import CommandSelector from './command-selector' -import { EmptyState, Footer, ResultList, SearchInput } from './components' -import { GotoAnythingProvider, useGotoAnythingContext } from './context' -import { useGotoAnythingModal } from './hooks/use-goto-anything-modal' -import { useGotoAnythingNavigation } from './hooks/use-goto-anything-navigation' -import { useGotoAnythingResults } from './hooks/use-goto-anything-results' -import { useGotoAnythingSearch } from './hooks/use-goto-anything-search' +import { knowledgeSearchQueryOptions } from './actions/knowledge' +import { pluginSearchQueryOptions } from './actions/plugin' +import { addRecentItem, getRecentItems } from './actions/recent-store' +import { EmptyState } from './components/empty-state' +import { Footer } from './components/footer' +import { gotoAnythingDialogHandle } from './dialog-handle' -type Props = Readonly<{ - onHide?: () => void -}> +const appWorkflowPathPattern = /^\/app\/[^/]+\/workflow$/ +const sharedWorkflowPathPattern = /^\/workflow\/[^/]+$/ +const ragPipelinePathPattern = /^\/datasets\/[^/]+\/pipeline$/ +const searchHotkey = 'Mod+K' +const searchShortcut = searchHotkey.split('+') -const GotoAnythingDialog: FC = ({ onHide }) => { - const { t } = useTranslation() - const { isWorkflowPage, isRagPipelinePage } = useGotoAnythingContext() - const { canInstallPlugin, currentDifyVersion } = useWorkspacePluginInstallPermission() - const prevShowRef = useRef(false) +type CommandOption = { + kind: 'command-option' + shortcut: string + description: string +} - // Search state management (called first so setSearchQuery is available) - const { - searchQuery, - setSearchQuery, - searchQueryDebouncedValue, - searchMode, - isCommandsMode, - cmdVal, - setCmdVal, - clearSelection, - Actions, - } = useGotoAnythingSearch() +type GotoAnythingOption = CommandOption | SearchResult - // Modal state management - const { open, onOpenChange, inputRef } = useGotoAnythingModal() +const slashCommandDescriptionKeys = { + '/create': 'gotoAnything.actions.createCategoryDesc', + '/refine': 'gotoAnything.actions.refineCategoryDesc', + '/theme': 'gotoAnything.actions.themeCategoryDesc', + '/language': 'gotoAnything.actions.languageChangeDesc', + '/account': 'gotoAnything.actions.accountDesc', + '/feedback': 'gotoAnything.actions.feedbackDesc', + '/docs': 'gotoAnything.actions.docDesc', + '/community': 'gotoAnything.actions.communityDesc', +} as const - // Reset state when modal opens/closes - useEffect(() => { - if (open && !prevShowRef.current) { - // Modal just opened - reset search - setSearchQuery('') - } else if (!open && prevShowRef.current) { - // Modal just closed - setSearchQuery('') - clearSelection() - onHide?.() - } - prevShowRef.current = open - }, [open, setSearchQuery, clearSelection, onHide]) +const actionDescriptionKeys = { + '@app': 'gotoAnything.actions.searchApplicationsDesc', + '@plugin': 'gotoAnything.actions.searchPluginsDesc', + '@knowledge': 'gotoAnything.actions.searchKnowledgeBasesDesc', + '@node': 'gotoAnything.actions.searchWorkflowNodesDesc', +} as const - // Results fetching and processing - const { dedupedResults, groupedResults, isLoading, isError, error } = useGotoAnythingResults({ - searchQueryDebouncedValue, - searchMode, - isCommandsMode, - Actions, - isWorkflowPage, - isRagPipelinePage, - cmdVal, - setCmdVal, +const groupLabelKeys = { + app: 'gotoAnything.groups.apps', + plugin: 'gotoAnything.groups.plugins', + knowledge: 'gotoAnything.groups.knowledgeBases', + 'workflow-node': 'gotoAnything.groups.workflowNodes', + command: 'gotoAnything.groups.commands', + recent: 'gotoAnything.groups.recent', +} as const + +function getCommandOptions(actions: Record, query: string): CommandOption[] { + const trimmedQuery = query.trim() + const filter = trimmedQuery.slice(1).toLowerCase() + + if (trimmedQuery.startsWith('/')) { + return slashCommandRegistry + .getAvailableCommands() + .filter((command) => !filter || command.name.toLowerCase().includes(filter)) + .map((command) => ({ + kind: 'command-option', + shortcut: `/${command.name}`, + description: command.description, + })) + } + + return Object.values(actions) + .filter((action) => action.key !== '/') + .filter((action) => !filter || action.shortcut.toLowerCase().includes(filter)) + .map((action) => ({ + kind: 'command-option', + shortcut: action.shortcut, + description: action.description, + })) +} + +function isCommandOption(option: GotoAnythingOption): option is CommandOption { + return 'kind' in option && option.kind === 'command-option' +} + +function optionToInputValue(option: GotoAnythingOption) { + return isCommandOption(option) ? `${option.shortcut} ` : option.title +} + +function isEditableShortcutTarget(target: EventTarget | null) { + if (!(target instanceof HTMLElement)) return false + + return target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName) +} + +function getSearchModeLabel(searchMode: string) { + if (searchMode === 'scopes') return 'SCOPES' + if (searchMode === 'commands') return 'COMMANDS' + return searchMode.replace('@', '').toUpperCase() +} + +function getSearchMode( + searchQuery: string, + isCommandsMode: boolean, + actions: Record, +) { + if (isCommandsMode) return searchQuery.trim().startsWith('@') ? 'scopes' : 'commands' + + const action = matchAction(searchQuery.trim().toLowerCase(), actions) + if (!action) return 'general' + + return action.key === '/' ? '@command' : action.key +} + +function isCommandSelectionQuery(query: string, actions: Record) { + const trimmedQuery = query.trim() + if (trimmedQuery === '@' || trimmedQuery === '/') return true + + return ( + (trimmedQuery.startsWith('@') || trimmedQuery.startsWith('/')) && + !matchAction(trimmedQuery, actions) + ) +} + +function getRecentSearchResults(): RecentSearchResult[] { + return getRecentItems().map((item) => ({ + id: `recent-${item.id}`, + title: item.title, + description: item.description, + type: 'recent', + originalType: item.originalType, + path: item.path, + icon: ( +
+ +
+ ), + data: { path: item.path }, + })) +} + +function dedupeSearchResults(results: SearchResult[]) { + const seen = new Set() + return results.filter((result) => { + const key = `${result.type}-${result.id}` + if (seen.has(key)) return false + + seen.add(key) + return true }) +} - // Navigation handlers - const { handleCommandSelect, handleNavigate, activePlugin, setActivePlugin } = - useGotoAnythingNavigation({ - Actions, - setSearchQuery, - clearSelection, - inputRef, - onClose: () => onOpenChange(false), +function groupSearchResults(results: SearchResult[]) { + return results.reduce>((groups, result) => { + const group = groups[result.type] ?? [] + group.push(result) + groups[result.type] = group + return groups + }, {}) +} + +function GotoAnythingDialog() { + const { t } = useTranslation() + const pathname = usePathname() + const router = useRouter() + const defaultLocale = useGetLanguage() + const isWorkflowPage = + appWorkflowPathPattern.test(pathname) || sharedWorkflowPathPattern.test(pathname) + const isRagPipelinePage = ragPipelinePathPattern.test(pathname) + const { canInstallPlugin, currentDifyVersion } = useWorkspacePluginInstallPermission() + const [searchQuery, setSearchQuery] = useState('') + const [activePlugin, setActivePlugin] = useState() + const inputRef = useRef(null) + const actions = useMemo( + () => createActions(isWorkflowPage, isRagPipelinePage), + [isWorkflowPage, isRagPipelinePage], + ) + const trimmedSearchQuery = searchQuery.trim() + const isCommandsMode = isCommandSelectionQuery(searchQuery, actions) + const searchMode = getSearchMode(searchQuery, isCommandsMode, actions) + const debouncedSearchQuery = useDebounce(searchQuery, { wait: 300 }) + const normalizedDebouncedQuery = debouncedSearchQuery.trim().toLowerCase() + const isDebouncedCommandsMode = isCommandSelectionQuery(debouncedSearchQuery, actions) + const debouncedAction = matchAction(normalizedDebouncedQuery, actions) + const debouncedSearchTerm = debouncedAction + ? getActionSearchTerm(normalizedDebouncedQuery, debouncedAction) + : normalizedDebouncedQuery + const remoteSearchEnabled = Boolean(normalizedDebouncedQuery) && !isDebouncedCommandsMode + const appSearchEnabled = + remoteSearchEnabled && (!debouncedAction || debouncedAction.key === '@app') + const knowledgeSearchEnabled = + remoteSearchEnabled && (!debouncedAction || debouncedAction.key === '@knowledge') + const pluginSearchEnabled = + remoteSearchEnabled && (!debouncedAction || debouncedAction.key === '@plugin') + const appSearchQuery = useQuery({ + ...appSearchQueryOptions(debouncedSearchTerm, debouncedAction?.key === '@app'), + enabled: appSearchEnabled, + }) + const knowledgeSearchQuery = useQuery({ + ...knowledgeSearchQueryOptions(debouncedSearchTerm), + enabled: knowledgeSearchEnabled, + }) + const pluginSearchQuery = useQuery({ + ...pluginSearchQueryOptions(debouncedSearchTerm, defaultLocale), + enabled: pluginSearchEnabled, + }) + const localSearchResults = useMemo(() => { + if (!trimmedSearchQuery || isCommandsMode) return [] + + const normalizedQuery = trimmedSearchQuery.toLowerCase() + const action = matchAction(normalizedQuery, actions) + if (action?.source === 'local') { + return action.search( + normalizedQuery, + getActionSearchTerm(normalizedQuery, action), + defaultLocale, + ) + } + if (action) return [] + + return Object.values(actions).flatMap((candidate) => { + if (candidate.source !== 'local' || candidate.key === '/') return [] + return candidate.search(normalizedQuery, normalizedQuery, defaultLocale) + }) + }, [actions, defaultLocale, isCommandsMode, trimmedSearchQuery]) + const activeRemoteQueries = [ + appSearchEnabled ? appSearchQuery : undefined, + knowledgeSearchEnabled ? knowledgeSearchQuery : undefined, + pluginSearchEnabled ? pluginSearchQuery : undefined, + ].filter((query) => query !== undefined) + const isDebouncing = remoteSearchEnabled && searchQuery.trim() !== debouncedSearchQuery.trim() + const isLoading = isDebouncing || activeRemoteQueries.some((query) => query.isLoading) + const failedRemoteQueries = activeRemoteQueries.filter((query) => query.isError) + const isError = + activeRemoteQueries.length > 0 && failedRemoteQueries.length === activeRemoteQueries.length + const hasUnavailableServices = failedRemoteQueries.length > 0 + const queryError = failedRemoteQueries[0]?.error + const error = queryError instanceof Error ? queryError : null + const remoteSearchResults = isDebouncing + ? [] + : activeRemoteQueries.flatMap((query) => query.data ?? []) + const searchResults = [...localSearchResults, ...remoteSearchResults] + const recentResults = trimmedSearchQuery || isCommandsMode ? [] : getRecentSearchResults() + const dedupedResults = dedupeSearchResults(recentResults.length ? recentResults : searchResults) + const groupedResults = groupSearchResults(dedupedResults) + + function resetSearch() { + setSearchQuery('') + } + + useHotkey( + searchHotkey, + (event) => { + if (event.defaultPrevented) return + if (!gotoAnythingDialogHandle.isOpen && isEditableShortcutTarget(event.target)) return + + event.preventDefault() + event.stopPropagation() + + if (!gotoAnythingDialogHandle.isOpen) gotoAnythingDialogHandle.open(null) + }, + { + ignoreInputs: false, + preventDefault: false, + stopPropagation: false, + }, + ) + + function handleCommandSelect(commandKey: string) { + if (commandKey.startsWith('/')) { + const handler = slashCommandRegistry.findCommand(commandKey.slice(1)) + if (handler?.mode === 'direct' && handler.execute) { + handler.execute() + gotoAnythingDialogHandle.close() + return + } + } + + setSearchQuery(`${commandKey} `) + } + + function handleNavigate(result: SearchResult) { + gotoAnythingDialogHandle.close() + + switch (result.type) { + case 'command': + actions.slash.action?.(result) + break + case 'plugin': + setActivePlugin(result.data) + break + case 'workflow-node': + if (result.metadata?.nodeId) selectWorkflowNode(result.metadata.nodeId, true) + break + case 'recent': + if (result.path) router.push(result.path) + break + default: + if ((result.type === 'app' || result.type === 'knowledge') && result.path) { + addRecentItem({ + id: result.id, + title: result.title, + description: result.description, + path: result.path, + originalType: result.type, + }) + } + if (result.path) router.push(result.path) + } + } + + function handleAutocompleteOpenChange( + nextOpen: boolean, + eventDetails: AutocompleteChangeEventDetails, + ) { + if (!nextOpen && eventDetails.reason === 'escape-key') gotoAnythingDialogHandle.close() + } + + function handleAutocompleteValueChange( + nextValue: string, + eventDetails: AutocompleteChangeEventDetails, + ) { + if (eventDetails.reason !== 'item-press') setSearchQuery(nextValue) + } + + function selectOption(option: GotoAnythingOption) { + if (isCommandOption(option)) handleCommandSelect(option.shortcut) + else handleNavigate(option) + } + + const commandOptions = getCommandOptions(actions, searchQuery) + const autocompleteOptions: GotoAnythingOption[] = isCommandsMode ? commandOptions : dedupedResults + const visibleOptions = isLoading || isError ? [] : autocompleteOptions + const autocompleteResultCount = visibleOptions.length + const isSlashMode = searchQuery.trim().startsWith('/') + + let autocompleteStatus: string | null = null + if (isLoading) autocompleteStatus = t(($) => $['gotoAnything.searching'], { ns: 'app' }) + else if (isError) autocompleteStatus = t(($) => $['gotoAnything.searchFailed'], { ns: 'app' }) + else if (hasUnavailableServices) + autocompleteStatus = t(($) => $['gotoAnything.someServicesUnavailable'], { ns: 'app' }) + else if (trimmedSearchQuery) + autocompleteStatus = t(($) => $['gotoAnything.resultCount'], { + ns: 'app', + count: autocompleteResultCount, }) - // Handle search input change - const handleSearchChange = useCallback( - (value: string) => { - setSearchQuery(value) - if (!value.startsWith('@') && !value.startsWith('/')) clearSelection() - }, - [setSearchQuery, clearSelection], - ) - - // Handle search input keydown for slash commands - const handleSearchKeyDown = useCallback( - (e: KeyboardEvent) => { - if (e.key === 'Enter') { - const query = searchQuery.trim() - // Check if it's a complete slash command - if (query.startsWith('/')) { - const commandName = query.substring(1).split(' ')[0] - const handler = slashCommandRegistry.findCommand(commandName!) - - // If it's a direct mode command, execute immediately - const isAvailable = handler?.isAvailable?.() ?? true - if (handler?.mode === 'direct' && handler.execute && isAvailable) { - e.preventDefault() - handler.execute() - onOpenChange(false) - setSearchQuery('') - } - } - } - }, - [searchQuery, onOpenChange, setSearchQuery], - ) - - // Determine which empty state to show - const emptyStateVariant = useMemo(() => { - if (isLoading) return 'loading' - if (isError) return 'error' - if (!searchQuery.trim()) { - // Show default hint only when there are no recent items to display - return dedupedResults.length === 0 ? 'default' : null - } - if (dedupedResults.length === 0 && !isCommandsMode) return 'no-results' - return null - }, [isLoading, isError, searchQuery, dedupedResults.length, isCommandsMode]) + let emptyStateVariant: 'loading' | 'error' | 'default' | 'no-results' | null = null + if (isLoading) emptyStateVariant = 'loading' + else if (isError) emptyStateVariant = 'error' + else if (!trimmedSearchQuery && autocompleteResultCount === 0) emptyStateVariant = 'default' + else if (autocompleteResultCount === 0 && !isCommandsMode) emptyStateVariant = 'no-results' return ( <> - - - + + + - + {t(($) => $['gotoAnything.searchTitle'], { ns: 'app' })} + + + items={visibleOptions} value={searchQuery} - onChange={handleSearchChange} - onKeyDown={handleSearchKeyDown} - searchMode={searchMode} - placeholder={t(($) => $['gotoAnything.searchPlaceholder'], { ns: 'app' })} + onValueChange={handleAutocompleteValueChange} + onOpenChange={handleAutocompleteOpenChange} + itemToStringValue={optionToInputValue} + filter={null} + open + inline + autoHighlight="always" + keepHighlight + loopFocus + > + + +
+ $['gotoAnything.searchTitle'], { ns: 'app' })} + placeholder={t(($) => $['gotoAnything.searchPlaceholder'], { ns: 'app' })} + className="px-0" + /> + {searchMode !== 'general' && ( +
+ {getSearchModeLabel(searchMode)} +
+ )} +
+ + {searchShortcut.map((key) => ( + {formatForDisplay(key)} + ))} + +
+ + {autocompleteStatus} + + + + + {emptyStateVariant === 'loading' && } + + {emptyStateVariant === 'error' && } + + {!isLoading && !isError && isCommandsMode && autocompleteResultCount === 0 && ( +
+
+
+ {t(($) => $['gotoAnything.noMatchingCommands'], { ns: 'app' })} +
+
+ {t(($) => $['gotoAnything.tryDifferentSearch'], { ns: 'app' })} +
+
+
+ )} + + {!isLoading && !isError && isCommandsMode && autocompleteResultCount > 0 && ( + + + + {isSlashMode + ? t(($) => $['gotoAnything.groups.commands'], { ns: 'app' }) + : t(($) => $['gotoAnything.selectSearchType'], { ns: 'app' })} + + + {(option: CommandOption) => ( + selectOption(option)} + > + + {option.shortcut} + + + {isSlashMode + ? t( + ($) => + $[ + slashCommandDescriptionKeys[ + option.shortcut as keyof typeof slashCommandDescriptionKeys + ] || option.description + ], + { ns: 'app' }, + ) + : t( + ($) => + $[ + actionDescriptionKeys[ + option.shortcut as keyof typeof actionDescriptionKeys + ] + ], + { ns: 'app' }, + )} + + + )} + + + + )} + + {!isLoading && !isError && !isCommandsMode && emptyStateVariant && ( + + )} + + {!isLoading && + !isError && + !isCommandsMode && + !emptyStateVariant && + autocompleteResultCount > 0 && ( + + {Object.entries(groupedResults).map(([type, results]) => ( + + + {t( + ($) => + $[ + groupLabelKeys[type as keyof typeof groupLabelKeys] || + `${type}s` + ], + { ns: 'app' }, + )} + + + {(result: SearchResult) => ( + selectOption(result)} + > + {result.icon} +
+
+ {result.title} +
+ {result.description && ( +
+ {result.description} +
+ )} +
+
+ {result.type} +
+
+ )} +
+
+ ))} +
+ )} +
+
+ + + +
+ +
+ + $['operation.close'], { ns: 'common' })} /> - - - {emptyStateVariant === 'loading' && } - - {emptyStateVariant === 'error' && } - - {!isLoading && !isError && ( - <> - {isCommandsMode ? ( - - ) : ( - - )} - - {!isCommandsMode && emptyStateVariant === 'no-results' && ( - - )} - - {!isCommandsMode && emptyStateVariant === 'default' && ( - - )} - - )} - - -
- - + +
{activePlugin && canInstallPlugin && ( @@ -205,10 +613,6 @@ const GotoAnythingDialog: FC = ({ onHide }) => { ) } -export const GotoAnything: FC = (props) => { - return ( - - - - ) +export function GotoAnything() { + return } diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index fed291a0355..df3a60260e8 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -5,8 +5,9 @@ import type { ModalContextState } from '@/context/modal-context' import type { ProviderContextState } from '@/context/provider-context' import type { ICurrentWorkspace, IWorkspace } from '@/models/common' import type { InstalledApp } from '@/models/explore' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { fireEvent, screen, waitFor } from '@testing-library/react' -import { createStore, Provider as JotaiProvider } from 'jotai' +import { Provider as JotaiProvider } from 'jotai' import { createTestQueryClient, renderWithSystemFeatures, @@ -14,7 +15,7 @@ import { import { Plan } from '@/app/components/billing/type' import { DETAIL_SIDEBAR_STORAGE_KEY } from '@/app/components/detail-sidebar/storage' import { LEARN_DIFY_HIDDEN_STORAGE_KEY } from '@/app/components/explore/learn-dify/storage' -import { useGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' import { useModalContext } from '@/context/modal-context' import { useProviderContext } from '@/context/provider-context' @@ -263,7 +264,7 @@ const defaultMainNavSystemFeatures: MainNavSystemFeatures = { const renderMainNav = ( systemFeatures: MainNavSystemFeatures = defaultMainNavSystemFeatures, - options: { store?: ReturnType; extra?: ReactNode } = {}, + options: { extra?: ReactNode } = {}, ) => { const queryClient = createTestQueryClient() const currentAppContext = mockAppContextState.current ?? appContextValue @@ -282,7 +283,7 @@ const renderMainNav = ( }, } return renderWithSystemFeatures( - + {options.extra} , @@ -290,14 +291,10 @@ const renderMainNav = ( ) } -function GotoAnythingOpenProbe() { - const open = useGotoAnythingOpen() - return
{String(open)}
-} - describe('MainNav', () => { beforeEach(() => { vi.clearAllMocks() + gotoAnythingDialogHandle.close() localStorage.clear() mockPathname = '/apps' mockInstalledApps = [] @@ -686,14 +683,20 @@ describe('MainNav', () => { ) }) - it('opens goto anything from the search button', () => { - const store = createStore() + it('opens goto anything from the search button', async () => { + renderMainNav(undefined, { + extra: ( + + + Goto Anything + + + ), + }) - renderMainNav(undefined, { store, extra: }) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('false') fireEvent.click(screen.getByRole('button', { name: 'app.gotoAnything.searchTitle' })) - expect(screen.getByTestId('goto-anything-open')).toHaveTextContent('true') + expect(await screen.findByRole('dialog', { name: 'Goto Anything' })).toBeInTheDocument() }) it('shows Learn Dify switch in help menu and restores it from localStorage', async () => { diff --git a/web/app/components/main-nav/components/search-button.tsx b/web/app/components/main-nav/components/search-button.tsx index 7444f66690e..3fa360fd4dd 100644 --- a/web/app/components/main-nav/components/search-button.tsx +++ b/web/app/components/main-nav/components/search-button.tsx @@ -1,22 +1,26 @@ 'use client' +import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { Kbd } from '@langgenius/dify-ui/kbd' import { formatForDisplay } from '@tanstack/react-hotkeys' import { useTranslation } from 'react-i18next' -import { useSetGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' const searchShortcut = ['Mod', 'K'] export function MainNavSearchButton() { const { t } = useTranslation() - const setGotoAnythingOpen = useSetGotoAnythingOpen() return ( - + ) } diff --git a/web/app/components/rag-pipeline/goto-anything-search.ts b/web/app/components/rag-pipeline/goto-anything-search.ts new file mode 100644 index 00000000000..310c770e0a8 --- /dev/null +++ b/web/app/components/rag-pipeline/goto-anything-search.ts @@ -0,0 +1,17 @@ +import type { SearchResult } from '@/app/components/goto-anything/actions/types' + +type SearchRagPipelineNodes = (query: string) => SearchResult[] + +let searchRagPipelineNodes: SearchRagPipelineNodes | undefined + +export function registerRagPipelineNodeSearch(search: SearchRagPipelineNodes) { + searchRagPipelineNodes = search + + return () => { + if (searchRagPipelineNodes === search) searchRagPipelineNodes = undefined + } +} + +export function findRagPipelineNodes(query: string) { + return searchRagPipelineNodes?.(query) ?? [] +} diff --git a/web/app/components/rag-pipeline/hooks/__tests__/use-rag-pipeline-search.spec.tsx b/web/app/components/rag-pipeline/hooks/__tests__/use-rag-pipeline-search.spec.tsx index 9f0a6223603..70b3b837384 100644 --- a/web/app/components/rag-pipeline/hooks/__tests__/use-rag-pipeline-search.spec.tsx +++ b/web/app/components/rag-pipeline/hooks/__tests__/use-rag-pipeline-search.spec.tsx @@ -1,6 +1,7 @@ import { renderHook } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { BlockEnum } from '@/app/components/workflow/types' +import { findRagPipelineNodes } from '../../goto-anything-search' import { useRagPipelineSearch } from '../use-rag-pipeline-search' const mockNodes: Array<{ id: string; data: Record }> = [] @@ -23,20 +24,6 @@ vi.mock('@/app/components/workflow/block-icon', () => ({ default: () => null, })) -type MockSearchResult = { - title: string - type: string - description?: string - metadata?: { nodeId: string } -} - -const mockRagPipelineNodesAction = vi.hoisted(() => { - return { searchFn: undefined as undefined | ((query: string) => MockSearchResult[]) } -}) -vi.mock('@/app/components/goto-anything/actions/rag-pipeline-nodes', () => ({ - ragPipelineNodesAction: mockRagPipelineNodesAction, -})) - const mockCleanupListener = vi.fn() vi.mock('@/app/components/workflow/utils/node-navigation', () => ({ setupNodeSelectionListener: () => mockCleanupListener, @@ -46,7 +33,6 @@ describe('useRagPipelineSearch', () => { beforeEach(() => { vi.clearAllMocks() mockNodes.length = 0 - mockRagPipelineNodesAction.searchFn = undefined }) afterEach(() => { @@ -65,15 +51,21 @@ describe('useRagPipelineSearch', () => { data: { type: BlockEnum.LLM, title: 'LLM Node', desc: '' }, }) - renderHook(() => useRagPipelineSearch()) + const { unmount } = renderHook(() => useRagPipelineSearch()) - expect(mockRagPipelineNodesAction.searchFn).toBeDefined() + expect(findRagPipelineNodes('LLM')).toEqual([ + expect.objectContaining({ id: 'node-1', title: 'LLM Node' }), + ]) + + unmount() }) - it('should not register search function when no nodes', () => { - renderHook(() => useRagPipelineSearch()) + it('should expose empty search results when no nodes exist', () => { + const { unmount } = renderHook(() => useRagPipelineSearch()) - expect(mockRagPipelineNodesAction.searchFn).toBeUndefined() + expect(findRagPipelineNodes('LLM')).toEqual([]) + + unmount() }) it('should cleanup search function on unmount', () => { @@ -84,11 +76,11 @@ describe('useRagPipelineSearch', () => { const { unmount } = renderHook(() => useRagPipelineSearch()) - expect(mockRagPipelineNodesAction.searchFn).toBeDefined() + expect(findRagPipelineNodes('Start')).not.toEqual([]) unmount() - expect(mockRagPipelineNodesAction.searchFn).toBeUndefined() + expect(findRagPipelineNodes('Start')).toEqual([]) }) it('should setup node selection listener', () => { @@ -136,8 +128,7 @@ describe('useRagPipelineSearch', () => { it('should find nodes by title', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('GPT') + const results = findRagPipelineNodes('GPT') expect(results.length).toBeGreaterThan(0) expect(results[0]!.title).toBe('GPT Model') @@ -146,8 +137,7 @@ describe('useRagPipelineSearch', () => { it('should find nodes by type', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn(BlockEnum.LLM) + const results = findRagPipelineNodes(BlockEnum.LLM) expect(results.some((r) => r.title === 'GPT Model')).toBe(true) }) @@ -155,8 +145,7 @@ describe('useRagPipelineSearch', () => { it('should find nodes by description', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('knowledge') + const results = findRagPipelineNodes('knowledge') expect(results.some((r) => r.title === 'Knowledge Base')).toBe(true) }) @@ -164,8 +153,7 @@ describe('useRagPipelineSearch', () => { it('should return all nodes when search term is empty', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('') + const results = findRagPipelineNodes('') expect(results.length).toBe(4) }) @@ -173,8 +161,7 @@ describe('useRagPipelineSearch', () => { it('should sort by alphabetical order when no search term', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('') + const results = findRagPipelineNodes('') const titles = results.map((r) => r.title) const sortedTitles = [...titles].sort((a, b) => a.localeCompare(b)) @@ -184,8 +171,7 @@ describe('useRagPipelineSearch', () => { it('should sort by relevance score when search term provided', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('Search') + const results = findRagPipelineNodes('Search') expect(results[0]!.title).toBe('Web Search') }) @@ -193,8 +179,7 @@ describe('useRagPipelineSearch', () => { it('should return empty array when no nodes match', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('nonexistent-xyz-12345') + const results = findRagPipelineNodes('nonexistent-xyz-12345') expect(results).toEqual([]) }) @@ -202,8 +187,7 @@ describe('useRagPipelineSearch', () => { it('should enhance Tool node description from tool_description', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('web') + const results = findRagPipelineNodes('web') const toolResult = results.find((r) => r.title === 'Web Search') expect(toolResult).toBeDefined() @@ -213,18 +197,18 @@ describe('useRagPipelineSearch', () => { it('should include metadata with nodeId', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('Start') + const results = findRagPipelineNodes('Start') const startResult = results.find((r) => r.title === 'Start Node') + expect(startResult?.type).toBe('workflow-node') + if (startResult?.type !== 'workflow-node') throw new Error('Expected a workflow node result') expect(startResult?.metadata?.nodeId).toBe('node-4') }) it('should set result type as workflow-node', () => { renderHook(() => useRagPipelineSearch()) - const searchFn = mockRagPipelineNodesAction.searchFn! - const results = searchFn('Start') + const results = findRagPipelineNodes('Start') expect(results[0]!.type).toBe('workflow-node') }) diff --git a/web/app/components/rag-pipeline/hooks/use-rag-pipeline-search.tsx b/web/app/components/rag-pipeline/hooks/use-rag-pipeline-search.tsx index 44380028c1b..286c708041b 100644 --- a/web/app/components/rag-pipeline/hooks/use-rag-pipeline-search.tsx +++ b/web/app/components/rag-pipeline/hooks/use-rag-pipeline-search.tsx @@ -5,7 +5,7 @@ import type { LLMNodeType } from '@/app/components/workflow/nodes/llm/types' import type { ToolNodeType } from '@/app/components/workflow/nodes/tool/types' import type { CommonNodeType } from '@/app/components/workflow/types' import { useCallback, useEffect, useMemo } from 'react' -import { ragPipelineNodesAction } from '@/app/components/goto-anything/actions/rag-pipeline-nodes' +import { registerRagPipelineNodeSearch } from '@/app/components/rag-pipeline/goto-anything-search' import BlockIcon from '@/app/components/workflow/block-icon' import { useNodesInteractions } from '@/app/components/workflow/hooks/use-nodes-interactions' import { useGetToolIcon } from '@/app/components/workflow/hooks/use-tool-icon' @@ -107,7 +107,7 @@ export const useRagPipelineSearch = () => { ) // Create search function for RAG pipeline nodes - const searchRagPipelineNodes = useCallback( + const findRagPipelineNodes = useCallback( (query: string) => { if (!searchableNodes.length) return [] @@ -154,18 +154,9 @@ export const useRagPipelineSearch = () => { [searchableNodes, calculateScore], ) - // Directly set the search function on the action object useEffect(() => { - if (searchableNodes.length > 0) { - // Set the search function directly on the action - ragPipelineNodesAction.searchFn = searchRagPipelineNodes - } - - return () => { - // Clean up when component unmounts - ragPipelineNodesAction.searchFn = undefined - } - }, [searchableNodes, searchRagPipelineNodes]) + return registerRagPipelineNodeSearch(findRagPipelineNodes) + }, [findRagPipelineNodes]) // Set up node selection event listener using the utility function useEffect(() => { diff --git a/web/app/components/snippets/components/snippet-detail-top.tsx b/web/app/components/snippets/components/snippet-detail-top.tsx index ca45372fefb..0d643f30304 100644 --- a/web/app/components/snippets/components/snippet-detail-top.tsx +++ b/web/app/components/snippets/components/snippet-detail-top.tsx @@ -1,11 +1,12 @@ 'use client' +import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { formatForDisplay } from '@tanstack/react-hotkeys' import { useTranslation } from 'react-i18next' import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon' -import { useSetGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import Link from '@/next/link' import { useRouter } from '@/next/navigation' import ToggleButton from '../../app-sidebar/toggle-button' @@ -20,7 +21,6 @@ const SEARCH_SHORTCUT = ['Mod', 'K'] const SnippetDetailTop = ({ expand = true, onToggle }: SnippetDetailTopProps) => { const { t } = useTranslation() const router = useRouter() - const setGotoAnythingOpen = useSetGotoAnythingOpen() if (!expand) { return ( @@ -68,14 +68,18 @@ const SnippetDetailTop = ({ expand = true, onToggle }: SnippetDetailTopProps) => $['gotoAnything.searchTitle'], { ns: 'app' })} - className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary" - onClick={() => setGotoAnythingOpen(true)} - > - - + $['gotoAnything.searchTitle'], { ns: 'app' })} + className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + > + + + } + /> } /> SearchResult[] + +let searchWorkflowNodes: SearchWorkflowNodes | undefined + +export function registerWorkflowNodeSearch(search: SearchWorkflowNodes) { + searchWorkflowNodes = search + + return () => { + if (searchWorkflowNodes === search) searchWorkflowNodes = undefined + } +} + +export function findWorkflowNodes(query: string) { + return searchWorkflowNodes?.(query) ?? [] +} diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx index c3d43a921fc..ba013d143d2 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-search.spec.tsx @@ -1,7 +1,7 @@ import type { CommonNodeType, Node, ToolWithProvider } from '../../types' import { act, renderHook } from '@testing-library/react' -import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes' import { CollectionType } from '@/app/components/tools/types' +import { findWorkflowNodes } from '../../goto-anything-search' import { BlockEnum } from '../../types' import { useWorkflowSearch } from '../use-workflow-search' @@ -49,7 +49,6 @@ describe('useWorkflowSearch', () => { beforeEach(() => { vi.clearAllMocks() runtimeNodes.length = 0 - workflowNodesAction.searchFn = undefined }) it('registers workflow node search results with tool icons and llm metadata scoring', async () => { @@ -89,17 +88,17 @@ describe('useWorkflowSearch', () => { const { unmount } = renderHook(() => useWorkflowSearch()) - const llmResults = await workflowNodesAction.search('', 'gpt') + const llmResults = findWorkflowNodes('gpt') expect(llmResults.map((item) => item.id)).toEqual(['llm-1']) expect(llmResults[0]?.title).toBe('Writer') - const toolResults = await workflowNodesAction.search('', 'search') + const toolResults = findWorkflowNodes('search') expect(toolResults.map((item) => item.id)).toEqual(['tool-1']) expect(toolResults[0]?.description).toBe('Search the web') unmount() - expect(workflowNodesAction.searchFn).toBeUndefined() + expect(findWorkflowNodes('gpt')).toEqual([]) }) it('binds the node selection listener to handleNodeSelect', () => { diff --git a/web/app/components/workflow/hooks/use-workflow-search.tsx b/web/app/components/workflow/hooks/use-workflow-search.tsx index f649ea8fd9b..9f9a1d1dfb7 100644 --- a/web/app/components/workflow/hooks/use-workflow-search.tsx +++ b/web/app/components/workflow/hooks/use-workflow-search.tsx @@ -5,9 +5,9 @@ import type { CommonNodeType } from '../types' import type { Emoji } from '@/app/components/tools/types' import { useCallback, useEffect, useMemo } from 'react' import { useNodes } from 'reactflow' -import { workflowNodesAction } from '@/app/components/goto-anything/actions/workflow-nodes' import { CollectionType } from '@/app/components/tools/types' import BlockIcon from '@/app/components/workflow/block-icon' +import { registerWorkflowNodeSearch } from '@/app/components/workflow/goto-anything-search' import { useAllBuiltInTools, useAllCustomTools, @@ -135,7 +135,7 @@ export const useWorkflowSearch = () => { ) // Create search function for workflow nodes - const searchWorkflowNodes = useCallback( + const findWorkflowNodes = useCallback( (query: string) => { if (!searchableNodes.length) return [] @@ -182,18 +182,9 @@ export const useWorkflowSearch = () => { [searchableNodes, calculateScore], ) - // Directly set the search function on the action object useEffect(() => { - if (searchableNodes.length > 0) { - // Set the search function directly on the action - workflowNodesAction.searchFn = searchWorkflowNodes - } - - return () => { - // Clean up when component unmounts - workflowNodesAction.searchFn = undefined - } - }, [searchableNodes, searchWorkflowNodes]) + return registerWorkflowNodeSearch(findWorkflowNodes) + }, [findWorkflowNodes]) // Set up node selection event listener using the utility function useEffect(() => { diff --git a/web/features/agent-v2/agent-detail/navigation.tsx b/web/features/agent-v2/agent-detail/navigation.tsx index 8c75a616ccb..38d57e7ab8a 100644 --- a/web/features/agent-v2/agent-detail/navigation.tsx +++ b/web/features/agent-v2/agent-detail/navigation.tsx @@ -5,6 +5,7 @@ import type { ComponentProps } from 'react' import type { AgentDetailSectionKey } from './section' import type { NavIcon } from '@/app/components/app-sidebar/nav-link' import { cn } from '@langgenius/dify-ui/cn' +import { DialogTrigger } from '@langgenius/dify-ui/dialog' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { formatForDisplay } from '@tanstack/react-hotkeys' @@ -15,7 +16,7 @@ import ToggleButton from '@/app/components/app-sidebar/toggle-button' import AppIcon from '@/app/components/base/app-icon' import Divider from '@/app/components/base/divider' import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon' -import { useSetGotoAnythingOpen } from '@/app/components/goto-anything/atoms' +import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' import Link from '@/next/link' import { usePathname } from '@/next/navigation' import { consoleQuery } from '@/service/client' @@ -86,7 +87,6 @@ const getAgentDetailNavigation = (agentId: string): AgentDetailNavItem[] => [ export function AgentDetailTop({ expand = true, onToggle }: AgentDetailTopProps) { const { t: tApp } = useTranslation('app') const { t: tCommon } = useTranslation('common') - const setGotoAnythingOpen = useSetGotoAnythingOpen() if (!expand) { return ( @@ -125,14 +125,18 @@ export function AgentDetailTop({ expand = true, onToggle }: AgentDetailTopProps) $['gotoAnything.searchTitle'])} - className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" - onClick={() => setGotoAnythingOpen(true)} - > - - + $['gotoAnything.searchTitle'])} + className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + > + + + } + /> } /> void }) { const { t } = useTranslation() - const setGotoAnythingOpen = useSetGotoAnythingOpen() if (!expand) { return ( @@ -231,14 +231,18 @@ export function DeploymentDetailTop({ $['gotoAnything.searchTitle'], { ns: 'app' })} - className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary" - onClick={() => setGotoAnythingOpen(true)} - > - - + $['gotoAnything.searchTitle'], { ns: 'app' })} + className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-[10px] text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + > + + + } + /> } /> -}): Promise => { - return get(url, { params }) -} - export const fetchAppDetail = ({ url, id,