From 1ce6e279f0ee0f6de67528333651521fd9e08a6f Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Thu, 9 Apr 2026 15:30:51 +0800 Subject: [PATCH 1/8] test: add unit tests for AppPublisher, Sidebar, Chat, FileUploader, Form Demo, Notion Page Selector, Prompt Editor, and Header Navigation components (#34802) Co-authored-by: CodingOnStar --- .../app-sidebar/dataset-info-flow.test.tsx | 224 +++++++++++++ .../app-sidebar/sidebar-shell-flow.test.tsx | 199 ++++++++++++ .../app/app-access-control-flow.test.tsx | 139 +++++++++ web/__tests__/app/app-publisher-flow.test.tsx | 243 +++++++++++++++ web/__tests__/base/chat-flow.test.tsx | 154 +++++++++ .../base/file-uploader-flow.test.tsx | 106 +++++++ web/__tests__/base/form-demo-flow.test.tsx | 65 ++++ .../base/notion-page-selector-flow.test.tsx | 151 +++++++++ .../base/prompt-editor-flow.test.tsx | 191 ++++++++++++ .../custom/custom-page-flow.test.tsx | 107 +++++++ .../header/account-dropdown-flow.test.tsx | 182 +++++++++++ web/__tests__/header/nav-flow.test.tsx | 237 ++++++++++++++ .../plugins/plugin-page-shell-flow.test.tsx | 163 ++++++++++ .../share/text-generation-mode-flow.test.tsx | 155 +++++++++ .../tools/provider-list-shell-flow.test.tsx | 205 ++++++++++++ .../__tests__/dropdown-callbacks.spec.tsx | 50 +++ .../dataset-info/__tests__/index.spec.tsx | 68 +++- .../apps/__tests__/app-card-skeleton.spec.tsx | 24 ++ .../chat/__tests__/chat-log-modals.spec.tsx | 144 +++++++++ .../chat/__tests__/use-chat-layout.spec.tsx | 293 +++++++++++++++++ .../base/chat/chat/chat-log-modals.tsx | 56 ++++ web/app/components/base/chat/chat/index.tsx | 176 ++--------- .../base/chat/chat/use-chat-layout.ts | 144 +++++++++ .../page-selector/__tests__/page-row.spec.tsx | 113 +++++++ .../__tests__/use-page-selector-model.spec.ts | 127 ++++++++ .../page-selector/__tests__/utils.spec.ts | 118 +++++++ .../__tests__/virtual-page-list.spec.tsx | 144 +++++++++ .../__tests__/prompt-editor-content.spec.tsx | 295 ++++++++++++++++++ .../components/base/prompt-editor/index.tsx | 182 +---------- .../prompt-editor/prompt-editor-content.tsx | 257 +++++++++++++++ .../preview/__tests__/loading.spec.tsx | 30 ++ .../detail/__tests__/context.spec.tsx | 30 ++ .../__tests__/segment-list-context.spec.tsx | 55 ++++ .../hooks/__tests__/use-doc-toc.spec.tsx | 125 ++++++++ .../actions/__tests__/node-actions.spec.ts | 69 ++++ .../actions/commands/__tests__/slash.spec.tsx | 124 ++++++++ .../__tests__/menu-item-content.spec.tsx | 28 ++ .../model-auth/__tests__/index.spec.ts | 23 ++ .../__tests__/credits-fallback-alert.spec.tsx | 18 ++ .../__tests__/downloading-icon.spec.tsx | 16 + .../text-generation/__tests__/index.spec.tsx | 219 +++++++++++++ .../hooks/__tests__/use-is-chat-mode.spec.ts | 41 +++ web/eslint-suppressions.json | 6 - web/service/fetch.ts | 12 +- 44 files changed, 5177 insertions(+), 331 deletions(-) create mode 100644 web/__tests__/app-sidebar/dataset-info-flow.test.tsx create mode 100644 web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx create mode 100644 web/__tests__/app/app-access-control-flow.test.tsx create mode 100644 web/__tests__/app/app-publisher-flow.test.tsx create mode 100644 web/__tests__/base/chat-flow.test.tsx create mode 100644 web/__tests__/base/file-uploader-flow.test.tsx create mode 100644 web/__tests__/base/form-demo-flow.test.tsx create mode 100644 web/__tests__/base/notion-page-selector-flow.test.tsx create mode 100644 web/__tests__/base/prompt-editor-flow.test.tsx create mode 100644 web/__tests__/custom/custom-page-flow.test.tsx create mode 100644 web/__tests__/header/account-dropdown-flow.test.tsx create mode 100644 web/__tests__/header/nav-flow.test.tsx create mode 100644 web/__tests__/plugins/plugin-page-shell-flow.test.tsx create mode 100644 web/__tests__/share/text-generation-mode-flow.test.tsx create mode 100644 web/__tests__/tools/provider-list-shell-flow.test.tsx create mode 100644 web/app/components/apps/__tests__/app-card-skeleton.spec.tsx create mode 100644 web/app/components/base/chat/chat/__tests__/chat-log-modals.spec.tsx create mode 100644 web/app/components/base/chat/chat/__tests__/use-chat-layout.spec.tsx create mode 100644 web/app/components/base/chat/chat/chat-log-modals.tsx create mode 100644 web/app/components/base/chat/chat/use-chat-layout.ts create mode 100644 web/app/components/base/notion-page-selector/page-selector/__tests__/page-row.spec.tsx create mode 100644 web/app/components/base/notion-page-selector/page-selector/__tests__/use-page-selector-model.spec.ts create mode 100644 web/app/components/base/notion-page-selector/page-selector/__tests__/utils.spec.ts create mode 100644 web/app/components/base/notion-page-selector/page-selector/__tests__/virtual-page-list.spec.tsx create mode 100644 web/app/components/base/prompt-editor/__tests__/prompt-editor-content.spec.tsx create mode 100644 web/app/components/base/prompt-editor/prompt-editor-content.tsx create mode 100644 web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/loading.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/__tests__/context.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/__tests__/segment-list-context.spec.tsx create mode 100644 web/app/components/develop/hooks/__tests__/use-doc-toc.spec.tsx create mode 100644 web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts create mode 100644 web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx create mode 100644 web/app/components/header/account-dropdown/__tests__/menu-item-content.spec.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/index.spec.ts create mode 100644 web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/credits-fallback-alert.spec.tsx create mode 100644 web/app/components/header/plugins-nav/__tests__/downloading-icon.spec.tsx create mode 100644 web/app/components/share/text-generation/__tests__/index.spec.tsx create mode 100644 web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts diff --git a/web/__tests__/app-sidebar/dataset-info-flow.test.tsx b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx new file mode 100644 index 0000000000..d1ca233d96 --- /dev/null +++ b/web/__tests__/app-sidebar/dataset-info-flow.test.tsx @@ -0,0 +1,224 @@ +import type { DataSet } from '@/models/datasets' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import DatasetInfo from '@/app/components/app-sidebar/dataset-info' +import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { RETRIEVE_METHOD } from '@/types/app' + +const mockReplace = vi.fn() +const mockInvalidDatasetList = vi.fn() +const mockInvalidDatasetDetail = vi.fn() +const mockExportPipeline = vi.fn() +const mockCheckIsUsedInApp = vi.fn() +const mockDeleteDataset = vi.fn() +const mockDownloadBlob = vi.fn() + +let mockDataset: DataSet + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, + }), +})) + +vi.mock('@/next/navigation', () => ({ + useRouter: () => ({ + replace: mockReplace, + }), +})) + +vi.mock('@/context/dataset-detail', () => ({ + useDatasetDetailContextWithSelector: (selector: (state: { dataset?: DataSet }) => unknown) => selector({ + dataset: mockDataset, + }), +})) + +vi.mock('@/context/app-context', () => ({ + useSelector: (selector: (state: { isCurrentWorkspaceDatasetOperator: boolean }) => unknown) => + selector({ isCurrentWorkspaceDatasetOperator: false }), +})) + +vi.mock('@/hooks/use-knowledge', () => ({ + useKnowledge: () => ({ + formatIndexingTechniqueAndMethod: () => 'indexing-technique', + }), +})) + +vi.mock('@/service/knowledge/use-dataset', () => ({ + datasetDetailQueryKeyPrefix: ['dataset', 'detail'], + useInvalidDatasetList: () => mockInvalidDatasetList, +})) + +vi.mock('@/service/use-base', () => ({ + useInvalid: () => mockInvalidDatasetDetail, +})) + +vi.mock('@/service/use-pipeline', () => ({ + useExportPipelineDSL: () => ({ + mutateAsync: mockExportPipeline, + }), +})) + +vi.mock('@/service/datasets', () => ({ + checkIsUsedInApp: (...args: unknown[]) => mockCheckIsUsedInApp(...args), + deleteDataset: (...args: unknown[]) => mockDeleteDataset(...args), +})) + +vi.mock('@/utils/download', () => ({ + downloadBlob: (...args: unknown[]) => mockDownloadBlob(...args), +})) + +vi.mock('@/app/components/datasets/rename-modal', () => ({ + default: ({ + show, + onClose, + onSuccess, + }: { + show: boolean + onClose: () => void + onSuccess: () => void + }) => show + ? ( +
+ + +
+ ) + : null, +})) + +const createDataset = (overrides: Partial = {}): DataSet => ({ + id: 'dataset-1', + name: 'Dataset Name', + indexing_status: 'completed', + icon_info: { + icon: '📙', + icon_background: '#FFF4ED', + icon_type: 'emoji', + icon_url: '', + }, + description: 'Dataset description', + permission: DatasetPermission.onlyMe, + data_source_type: DataSourceType.FILE, + indexing_technique: 'high_quality' as DataSet['indexing_technique'], + created_by: 'user-1', + updated_by: 'user-1', + updated_at: 1690000000, + app_count: 0, + doc_form: ChunkingMode.text, + document_count: 1, + total_document_count: 1, + word_count: 1000, + provider: 'internal', + embedding_model: 'text-embedding-3', + embedding_model_provider: 'openai', + embedding_available: true, + retrieval_model_dict: { + search_method: RETRIEVE_METHOD.semantic, + reranking_enable: false, + reranking_model: { + reranking_provider_name: '', + reranking_model_name: '', + }, + top_k: 5, + score_threshold_enabled: false, + score_threshold: 0, + }, + retrieval_model: { + search_method: RETRIEVE_METHOD.semantic, + reranking_enable: false, + reranking_model: { + reranking_provider_name: '', + reranking_model_name: '', + }, + top_k: 5, + score_threshold_enabled: false, + score_threshold: 0, + }, + tags: [], + external_knowledge_info: { + external_knowledge_id: '', + external_knowledge_api_id: '', + external_knowledge_api_name: '', + external_knowledge_api_endpoint: '', + }, + external_retrieval_model: { + top_k: 0, + score_threshold: 0, + score_threshold_enabled: false, + }, + built_in_field_enabled: false, + runtime_mode: 'rag_pipeline', + pipeline_id: 'pipeline-1', + enable_api: false, + is_multimodal: false, + is_published: true, + ...overrides, +}) + +const openDropdown = () => { + fireEvent.click(screen.getByRole('button')) +} + +describe('App Sidebar Dataset Info Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockDataset = createDataset() + mockExportPipeline.mockResolvedValue({ data: 'pipeline: demo' }) + mockCheckIsUsedInApp.mockResolvedValue({ is_using: false }) + mockDeleteDataset.mockResolvedValue({}) + }) + + it('exports the published pipeline from the dropdown menu', async () => { + render() + + expect(screen.getByText('Dataset Name')).toBeInTheDocument() + + openDropdown() + fireEvent.click(await screen.findByText('datasetPipeline.operations.exportPipeline')) + + await waitFor(() => { + expect(mockExportPipeline).toHaveBeenCalledWith({ + pipelineId: 'pipeline-1', + include: false, + }) + expect(mockDownloadBlob).toHaveBeenCalledWith(expect.objectContaining({ + fileName: 'Dataset Name.pipeline', + })) + }) + }) + + it('opens the rename modal and refreshes dataset caches after a successful rename', async () => { + render() + + openDropdown() + fireEvent.click(await screen.findByText('common.operation.edit')) + + expect(screen.getByTestId('rename-dataset-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'rename-success' })) + + expect(mockInvalidDatasetList).toHaveBeenCalledTimes(1) + expect(mockInvalidDatasetDetail).toHaveBeenCalledTimes(1) + }) + + it('checks app usage before deleting and redirects back to the dataset list after confirmation', async () => { + render() + + openDropdown() + fireEvent.click(await screen.findByText('common.operation.delete')) + + await waitFor(() => { + expect(mockCheckIsUsedInApp).toHaveBeenCalledWith('dataset-1') + expect(screen.getByText('dataset.deleteDatasetConfirmTitle')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: 'common.operation.confirm' })) + + await waitFor(() => { + expect(mockDeleteDataset).toHaveBeenCalledWith('dataset-1') + expect(mockInvalidDatasetList).toHaveBeenCalled() + expect(mockReplace).toHaveBeenCalledWith('/datasets') + }) + }) +}) diff --git a/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx new file mode 100644 index 0000000000..3e3edba5dd --- /dev/null +++ b/web/__tests__/app-sidebar/sidebar-shell-flow.test.tsx @@ -0,0 +1,199 @@ +import type { SVGProps } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AppDetailNav from '@/app/components/app-sidebar' + +const mockSetAppSidebarExpand = vi.fn() + +let mockAppSidebarExpand = 'expand' +let mockPathname = '/app/app-1/logs' +let mockSelectedSegment = 'logs' +let mockIsHovering = true +let keyPressHandler: ((event: { preventDefault: () => void }) => void) | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: Record) => unknown) => selector({ + appDetail: { + id: 'app-1', + name: 'Demo App', + mode: 'chat', + icon: '🤖', + icon_type: 'emoji', + icon_background: '#FFEAD5', + icon_url: null, + }, + appSidebarExpand: mockAppSidebarExpand, + setAppSidebarExpand: mockSetAppSidebarExpand, + }), +})) + +vi.mock('zustand/react/shallow', () => ({ + useShallow: (selector: unknown) => selector, +})) + +vi.mock('@/next/navigation', () => ({ + usePathname: () => mockPathname, + useSelectedLayoutSegment: () => mockSelectedSegment, +})) + +vi.mock('@/next/link', () => ({ + default: ({ + href, + children, + className, + title, + }: { + href: string + children?: React.ReactNode + className?: string + title?: string + }) => ( + + {children} + + ), +})) + +vi.mock('ahooks', () => ({ + useHover: () => mockIsHovering, + useKeyPress: (_key: string, handler: (event: { preventDefault: () => void }) => void) => { + keyPressHandler = handler + }, +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + default: () => 'desktop', + MediaType: { + mobile: 'mobile', + desktop: 'desktop', + }, +})) + +vi.mock('@/context/event-emitter', () => ({ + useEventEmitterContextContext: () => ({ + eventEmitter: { + useSubscription: vi.fn(), + }, + }), +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: () => ({ + isCurrentWorkspaceEditor: true, + }), +})) + +vi.mock('@/app/components/workflow/utils', () => ({ + getKeyboardKeyCodeBySystem: () => 'ctrl', + getKeyboardKeyNameBySystem: (key: string) => key, +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const React = await vi.importActual('react') + const OpenContext = React.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ + children, + onClick, + }: { + children: React.ReactNode + onClick?: () => void + }) => ( + + ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => { + const open = React.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +vi.mock('@/app/components/base/tooltip', () => ({ + default: ({ children }: { children?: React.ReactNode }) => <>{children}, +})) + +vi.mock('@/app/components/app-sidebar/app-info', () => ({ + default: ({ + expand, + onlyShowDetail, + openState, + }: { + expand: boolean + onlyShowDetail?: boolean + openState?: boolean + }) => ( +
+ ), +})) + +const MockIcon = (props: SVGProps) => + +const navigation = [ + { name: 'Overview', href: '/app/app-1/overview', icon: MockIcon, selectedIcon: MockIcon }, + { name: 'Logs', href: '/app/app-1/logs', icon: MockIcon, selectedIcon: MockIcon }, +] + +describe('App Sidebar Shell Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + mockAppSidebarExpand = 'expand' + mockPathname = '/app/app-1/logs' + mockSelectedSegment = 'logs' + mockIsHovering = true + keyPressHandler = null + }) + + it('renders the expanded sidebar, marks the active nav item, and toggles collapse by click and shortcut', () => { + render() + + expect(screen.getByTestId('app-info')).toHaveAttribute('data-expand', 'true') + + const logsLink = screen.getByRole('link', { name: /Logs/i }) + expect(logsLink.className).toContain('bg-components-menu-item-bg-active') + + fireEvent.click(screen.getByRole('button')) + expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse') + + const preventDefault = vi.fn() + keyPressHandler?.({ preventDefault }) + + expect(preventDefault).toHaveBeenCalled() + expect(mockSetAppSidebarExpand).toHaveBeenCalledWith('collapse') + }) + + it('switches to the workflow fullscreen dropdown shell and opens its navigation menu', () => { + mockPathname = '/app/app-1/workflow' + mockSelectedSegment = 'workflow' + localStorage.setItem('workflow-canvas-maximize', 'true') + + render() + + expect(screen.queryByTestId('app-info')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('portal-trigger')) + + expect(screen.getByText('Demo App')).toBeInTheDocument() + expect(screen.getByRole('link', { name: /Overview/i })).toBeInTheDocument() + expect(screen.getByRole('link', { name: /Logs/i })).toBeInTheDocument() + }) +}) diff --git a/web/__tests__/app/app-access-control-flow.test.tsx b/web/__tests__/app/app-access-control-flow.test.tsx new file mode 100644 index 0000000000..49443eb4ec --- /dev/null +++ b/web/__tests__/app/app-access-control-flow.test.tsx @@ -0,0 +1,139 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AppPublisher from '@/app/components/app/app-publisher' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' + +const mockFetchAppDetailDirect = vi.fn() +const mockSetAppDetail = vi.fn() +const mockRefetch = vi.fn() + +let mockAppDetail: { + id: string + name: string + mode: AppModeEnum + access_mode: AccessMode + description: string + icon: string + icon_type: string + icon_background: string + site: { + app_base_url: string + access_token: string + } +} | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, + }), +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: Record) => unknown) => selector({ + appDetail: mockAppDetail, + setAppDetail: mockSetAppDetail, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: Record) => unknown) => selector({ + systemFeatures: { + webapp_auth: { + enabled: true, + }, + }, + }), +})) + +vi.mock('@/hooks/use-format-time-from-now', () => ({ + useFormatTimeFromNow: () => ({ + formatTimeFromNow: (value: number) => `ago:${value}`, + }), +})) + +vi.mock('@/hooks/use-async-window-open', () => ({ + useAsyncWindowOpen: () => vi.fn(), +})) + +vi.mock('@/service/access-control', () => ({ + useGetUserCanAccessApp: () => ({ + data: { result: true }, + isLoading: false, + refetch: mockRefetch, + }), + useAppWhiteListSubjects: () => ({ + data: { groups: [], members: [] }, + isLoading: false, + }), +})) + +vi.mock('@/service/apps', () => ({ + fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args), +})) + +vi.mock('@/app/components/app/overview/embedded', () => ({ + default: () => null, +})) + +vi.mock('@/app/components/app/app-access-control', () => ({ + default: ({ + onConfirm, + onClose, + }: { + onConfirm: () => Promise + onClose: () => void + }) => ( +
+ + +
+ ), +})) + +describe('App Access Control Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockAppDetail = { + id: 'app-1', + name: 'Demo App', + mode: AppModeEnum.CHAT, + access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + description: 'Demo app description', + icon: '🤖', + icon_type: 'emoji', + icon_background: '#FFEAD5', + site: { + app_base_url: 'https://example.com', + access_token: 'token-1', + }, + } + mockFetchAppDetailDirect.mockResolvedValue({ + ...mockAppDetail, + access_mode: AccessMode.PUBLIC, + }) + }) + + it('refreshes app detail after confirming access control updates', async () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'workflow.common.publish' })) + fireEvent.click(screen.getByText('app.accessControlDialog.accessItems.specific')) + + expect(screen.getByTestId('access-control-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'confirm-access-control' })) + + await waitFor(() => { + expect(mockFetchAppDetailDirect).toHaveBeenCalledWith({ url: '/apps', id: 'app-1' }) + expect(mockSetAppDetail).toHaveBeenCalledWith(expect.objectContaining({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + })) + }) + + await waitFor(() => { + expect(screen.queryByTestId('access-control-modal')).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/__tests__/app/app-publisher-flow.test.tsx b/web/__tests__/app/app-publisher-flow.test.tsx new file mode 100644 index 0000000000..5c330cf71e --- /dev/null +++ b/web/__tests__/app/app-publisher-flow.test.tsx @@ -0,0 +1,243 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import AppPublisher from '@/app/components/app/app-publisher' +import { AccessMode } from '@/models/access-control' +import { AppModeEnum } from '@/types/app' + +const mockTrackEvent = vi.fn() +const mockRefetch = vi.fn() +const mockFetchInstalledAppList = vi.fn() +const mockFetchAppDetailDirect = vi.fn() +const mockToastError = vi.fn() +const mockOpenAsyncWindow = vi.fn() +const mockSetAppDetail = vi.fn() + +let mockAppDetail: { + id: string + name: string + mode: AppModeEnum + access_mode: AccessMode + description: string + icon: string + icon_type: string + icon_background: string + site: { + app_base_url: string + access_token: string + } +} | null = null + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('ahooks', () => ({ + useKeyPress: vi.fn(), +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: Record) => unknown) => selector({ + appDetail: mockAppDetail, + setAppDetail: mockSetAppDetail, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: Record) => unknown) => selector({ + systemFeatures: { + webapp_auth: { + enabled: true, + }, + }, + }), +})) + +vi.mock('@/hooks/use-format-time-from-now', () => ({ + useFormatTimeFromNow: () => ({ + formatTimeFromNow: (value: number) => `ago:${value}`, + }), +})) + +vi.mock('@/hooks/use-async-window-open', () => ({ + useAsyncWindowOpen: () => mockOpenAsyncWindow, +})) + +vi.mock('@/service/access-control', () => ({ + useGetUserCanAccessApp: () => ({ + data: { result: true }, + isLoading: false, + refetch: mockRefetch, + }), + useAppWhiteListSubjects: () => ({ + data: { groups: [], members: [] }, + isLoading: false, + }), +})) + +vi.mock('@/service/explore', () => ({ + fetchInstalledAppList: (...args: unknown[]) => mockFetchInstalledAppList(...args), +})) + +vi.mock('@/service/apps', () => ({ + fetchAppDetailDirect: (...args: unknown[]) => mockFetchAppDetailDirect(...args), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (...args: unknown[]) => mockToastError(...args), + }, +})) + +vi.mock('@/app/components/base/amplitude', () => ({ + trackEvent: (...args: unknown[]) => mockTrackEvent(...args), +})) + +vi.mock('@/app/components/app/overview/embedded', () => ({ + default: ({ isShow, onClose }: { isShow: boolean, onClose: () => void }) => ( + isShow + ? ( +
+ +
+ ) + : null + ), +})) + +vi.mock('@/app/components/app/app-access-control', () => ({ + default: () =>
, +})) + +vi.mock('@/app/components/base/portal-to-follow-elem', async () => { + const React = await vi.importActual('react') + const OpenContext = React.createContext(false) + + return { + PortalToFollowElem: ({ children, open }: { children: React.ReactNode, open: boolean }) => ( + +
{children}
+
+ ), + PortalToFollowElemTrigger: ({ + children, + onClick, + }: { + children: React.ReactNode + onClick?: () => void + }) => ( +
+ {children} +
+ ), + PortalToFollowElemContent: ({ children }: { children: React.ReactNode }) => { + const open = React.useContext(OpenContext) + return open ?
{children}
: null + }, + } +}) + +vi.mock('@/app/components/workflow/utils', () => ({ + getKeyboardKeyCodeBySystem: () => 'ctrl', + getKeyboardKeyNameBySystem: (key: string) => key, +})) + +describe('App Publisher Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockAppDetail = { + id: 'app-1', + name: 'Demo App', + mode: AppModeEnum.CHAT, + access_mode: AccessMode.SPECIFIC_GROUPS_MEMBERS, + description: 'Demo app description', + icon: '🤖', + icon_type: 'emoji', + icon_background: '#FFEAD5', + site: { + app_base_url: 'https://example.com', + access_token: 'token-1', + }, + } + mockFetchInstalledAppList.mockResolvedValue({ + installed_apps: [{ id: 'installed-1' }], + }) + mockFetchAppDetailDirect.mockResolvedValue({ + id: 'app-1', + access_mode: AccessMode.PUBLIC, + }) + mockOpenAsyncWindow.mockImplementation(async ( + resolver: () => Promise, + options?: { onError?: (error: Error) => void }, + ) => { + try { + return await resolver() + } + catch (error) { + options?.onError?.(error as Error) + } + }) + }) + + it('publishes from the summary panel and tracks the publish event', async () => { + const onPublish = vi.fn().mockResolvedValue(undefined) + + render( + , + ) + + fireEvent.click(screen.getByText('common.publish')) + + expect(screen.getByText('common.latestPublished')).toBeInTheDocument() + expect(screen.getByText('common.publishUpdate')).toBeInTheDocument() + + fireEvent.click(screen.getByText('common.publishUpdate')) + + await waitFor(() => { + expect(onPublish).toHaveBeenCalledTimes(1) + expect(mockTrackEvent).toHaveBeenCalledWith('app_published_time', expect.objectContaining({ + action_mode: 'app', + app_id: 'app-1', + app_name: 'Demo App', + })) + }) + + expect(mockRefetch).toHaveBeenCalled() + }) + + it('opens embedded modal and resolves the installed explore target', async () => { + render() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('common.embedIntoSite')) + + expect(screen.getByTestId('embedded-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('common.openInExplore')) + + await waitFor(() => { + expect(mockFetchInstalledAppList).toHaveBeenCalledWith('app-1') + expect(mockOpenAsyncWindow).toHaveBeenCalledTimes(1) + }) + }) + + it('shows a toast error when no installed explore app is available', async () => { + mockFetchInstalledAppList.mockResolvedValue({ + installed_apps: [], + }) + + render() + + fireEvent.click(screen.getByText('common.publish')) + fireEvent.click(screen.getByText('common.openInExplore')) + + await waitFor(() => { + expect(mockToastError).toHaveBeenCalledWith('No app found in Explore') + }) + }) +}) diff --git a/web/__tests__/base/chat-flow.test.tsx b/web/__tests__/base/chat-flow.test.tsx new file mode 100644 index 0000000000..2a02c063fd --- /dev/null +++ b/web/__tests__/base/chat-flow.test.tsx @@ -0,0 +1,154 @@ +import type { RefObject } from 'react' +import type { ChatConfig } from '@/app/components/base/chat/types' +import type { AppConversationData, AppData, AppMeta, ConversationItem } from '@/models/share' +import { fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import ChatWithHistory from '@/app/components/base/chat/chat-with-history' +import { useChatWithHistory } from '@/app/components/base/chat/chat-with-history/hooks' +import { useThemeContext } from '@/app/components/base/chat/embedded-chatbot/theme/theme-context' +import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import useDocumentTitle from '@/hooks/use-document-title' + +vi.mock('@/app/components/base/chat/chat-with-history/hooks', () => ({ + useChatWithHistory: vi.fn(), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + default: vi.fn(), + MediaType: { + mobile: 'mobile', + tablet: 'tablet', + pc: 'pc', + }, +})) + +vi.mock('@/hooks/use-document-title', () => ({ + default: vi.fn(), +})) + +vi.mock('@/next/navigation', () => ({ + useRouter: vi.fn(() => ({ + push: vi.fn(), + replace: vi.fn(), + prefetch: vi.fn(), + })), + usePathname: vi.fn(() => '/'), + useSearchParams: vi.fn(() => new URLSearchParams()), + useParams: vi.fn(() => ({})), +})) + +type HookReturn = ReturnType + +const mockAppData = { + site: { title: 'Test Chat', chat_color_theme: 'blue', chat_color_theme_inverted: false }, +} as unknown as AppData + +const defaultHookReturn: HookReturn = { + isInstalledApp: false, + appId: 'test-app-id', + currentConversationId: '', + currentConversationItem: undefined, + handleConversationIdInfoChange: vi.fn(), + appData: mockAppData, + appParams: {} as ChatConfig, + appMeta: {} as AppMeta, + appPinnedConversationData: { data: [] as ConversationItem[], has_more: false, limit: 20 } as AppConversationData, + appConversationData: { data: [] as ConversationItem[], has_more: false, limit: 20 } as AppConversationData, + appConversationDataLoading: false, + appChatListData: { data: [] as ConversationItem[], has_more: false, limit: 20 } as AppConversationData, + appChatListDataLoading: false, + appPrevChatTree: [], + pinnedConversationList: [], + conversationList: [], + setShowNewConversationItemInList: vi.fn(), + newConversationInputs: {}, + newConversationInputsRef: { current: {} } as unknown as RefObject>, + handleNewConversationInputsChange: vi.fn(), + inputsForms: [], + handleNewConversation: vi.fn(), + handleStartChat: vi.fn(), + handleChangeConversation: vi.fn(), + handlePinConversation: vi.fn(), + handleUnpinConversation: vi.fn(), + conversationDeleting: false, + handleDeleteConversation: vi.fn(), + conversationRenaming: false, + handleRenameConversation: vi.fn(), + handleNewConversationCompleted: vi.fn(), + newConversationId: '', + chatShouldReloadKey: 'test-reload-key', + handleFeedback: vi.fn(), + currentChatInstanceRef: { current: { handleStop: vi.fn() } }, + sidebarCollapseState: false, + handleSidebarCollapse: vi.fn(), + clearChatList: false, + setClearChatList: vi.fn(), + isResponding: false, + setIsResponding: vi.fn(), + currentConversationInputs: {}, + setCurrentConversationInputs: vi.fn(), + allInputsHidden: false, + initUserVariables: {}, +} + +describe('Base Chat Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.mocked(useBreakpoints).mockReturnValue(MediaType.pc) + vi.mocked(useChatWithHistory).mockReturnValue(defaultHookReturn) + renderHook(() => useThemeContext()).result.current.buildTheme() + }) + + // Chat-with-history shell integration across layout, responsive shell, and theme setup. + describe('Chat With History Shell', () => { + it('builds theme, updates the document title, and expands the collapsed desktop sidebar on hover', async () => { + const themeBuilder = renderHook(() => useThemeContext()).result.current + const { container } = render() + + const titles = screen.getAllByText('Test Chat') + expect(titles.length).toBeGreaterThan(0) + expect(useDocumentTitle).toHaveBeenCalledWith('Test Chat') + + await waitFor(() => { + expect(themeBuilder.theme.primaryColor).toBe('blue') + expect(themeBuilder.theme.chatColorThemeInverted).toBe(false) + }) + + vi.mocked(useChatWithHistory).mockReturnValue({ + ...defaultHookReturn, + sidebarCollapseState: true, + }) + + const { container: collapsedContainer } = render() + const hoverArea = collapsedContainer.querySelector('.absolute.top-0.z-20') + + expect(container.querySelector('.chat-history-shell')).toBeInTheDocument() + expect(hoverArea).toBeInTheDocument() + + if (hoverArea) { + fireEvent.mouseEnter(hoverArea) + expect(hoverArea).toHaveClass('left-0') + + fireEvent.mouseLeave(hoverArea) + expect(hoverArea).toHaveClass('left-[-248px]') + } + }) + + it('falls back to the mobile loading shell when site metadata is unavailable', () => { + vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile) + vi.mocked(useChatWithHistory).mockReturnValue({ + ...defaultHookReturn, + appData: null, + appChatListDataLoading: true, + }) + + const { container } = render() + + expect(useDocumentTitle).toHaveBeenCalledWith('Chat') + expect(screen.getByRole('status')).toBeInTheDocument() + expect(container.querySelector('.mobile-chat-shell')).toBeInTheDocument() + expect(container.querySelector('.rounded-t-2xl')).toBeInTheDocument() + expect(container.querySelector('.rounded-2xl')).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/__tests__/base/file-uploader-flow.test.tsx b/web/__tests__/base/file-uploader-flow.test.tsx new file mode 100644 index 0000000000..81dccedfe5 --- /dev/null +++ b/web/__tests__/base/file-uploader-flow.test.tsx @@ -0,0 +1,106 @@ +import type { FileUpload } from '@/app/components/base/features/types' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import FileUploaderInAttachmentWrapper from '@/app/components/base/file-uploader/file-uploader-in-attachment' +import FileUploaderInChatInput from '@/app/components/base/file-uploader/file-uploader-in-chat-input' +import { FileContextProvider } from '@/app/components/base/file-uploader/store' +import { TransferMethod } from '@/types/app' + +const mockUploadRemoteFileInfo = vi.fn() + +vi.mock('@/next/navigation', () => ({ + useParams: () => ({}), +})) + +vi.mock('@/service/common', () => ({ + uploadRemoteFileInfo: (...args: unknown[]) => mockUploadRemoteFileInfo(...args), +})) + +const createFileConfig = (overrides: Partial = {}): FileUpload => ({ + enabled: true, + allowed_file_types: ['document'], + allowed_file_extensions: [], + allowed_file_upload_methods: [TransferMethod.remote_url], + number_limits: 5, + preview_config: { + enabled: false, + mode: 'current_page', + file_type_list: [], + }, + ...overrides, +} as FileUpload) + +const renderChatInput = (fileConfig: FileUpload, readonly = false) => { + return render( + + + , + ) +} + +describe('Base File Uploader Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUploadRemoteFileInfo.mockResolvedValue({ + id: 'remote-file-1', + mime_type: 'application/pdf', + size: 2048, + name: 'guide.pdf', + url: 'https://cdn.example.com/guide.pdf', + }) + }) + + it('uploads a remote file from the attachment wrapper and pushes the updated file list to consumers', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + + render( + , + ) + + await user.click(screen.getByRole('button', { name: /fileUploader\.pasteFileLink/i })) + await user.type(screen.getByPlaceholderText(/fileUploader\.pasteFileLinkInputPlaceholder/i), 'https://example.com/guide.pdf') + await user.click(screen.getByRole('button', { name: /operation\.ok/i })) + + await waitFor(() => { + expect(mockUploadRemoteFileInfo).toHaveBeenCalledWith('https://example.com/guide.pdf', false) + }) + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith([ + expect.objectContaining({ + name: 'https://example.com/guide.pdf', + uploadedId: 'remote-file-1', + url: 'https://cdn.example.com/guide.pdf', + progress: 100, + }), + ]) + }) + + expect(screen.getByText('https://example.com/guide.pdf')).toBeInTheDocument() + }) + + it('opens the link picker from chat input and keeps the trigger disabled in readonly mode', async () => { + const user = userEvent.setup() + const fileConfig = createFileConfig() + + const { unmount } = renderChatInput(fileConfig) + + const activeTrigger = screen.getByRole('button') + expect(activeTrigger).toBeEnabled() + + await user.click(activeTrigger) + expect(screen.getByPlaceholderText(/fileUploader\.pasteFileLinkInputPlaceholder/i)).toBeInTheDocument() + expect(screen.queryByText(/fileUploader\.uploadFromComputer/i)).not.toBeInTheDocument() + + unmount() + renderChatInput(fileConfig, true) + + expect(screen.getByRole('button')).toBeDisabled() + }) +}) diff --git a/web/__tests__/base/form-demo-flow.test.tsx b/web/__tests__/base/form-demo-flow.test.tsx new file mode 100644 index 0000000000..afb36528c0 --- /dev/null +++ b/web/__tests__/base/form-demo-flow.test.tsx @@ -0,0 +1,65 @@ +import { render, screen, waitFor, within } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import DemoForm from '@/app/components/base/form/form-scenarios/demo' + +describe('Base Form Demo Flow', () => { + const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) + + beforeEach(() => { + vi.clearAllMocks() + }) + + it('reveals contact fields and submits the composed form values through the shared form actions', async () => { + const user = userEvent.setup() + render() + + expect(screen.queryByRole('heading', { name: /contacts/i })).not.toBeInTheDocument() + + await user.type(screen.getByRole('textbox', { name: /^name$/i }), 'Alice') + await user.type(screen.getByRole('textbox', { name: /^surname$/i }), 'Smith') + await user.click(screen.getByText(/i accept the terms and conditions/i)) + + expect(await screen.findByRole('heading', { name: /contacts/i })).toBeInTheDocument() + + await user.type(screen.getByRole('textbox', { name: /^email$/i }), 'alice@example.com') + + const preferredMethodLabel = screen.getByText('Preferred Contact Method') + const preferredMethodField = preferredMethodLabel.parentElement?.parentElement + expect(preferredMethodField).toBeTruthy() + + await user.click(within(preferredMethodField as HTMLElement).getByText('Email')) + await user.click(screen.getByText('Whatsapp')) + + const submitButton = screen.getByRole('button', { name: /operation\.submit/i }) + expect(submitButton).toBeEnabled() + await user.click(submitButton) + + await waitFor(() => { + expect(consoleLogSpy).toHaveBeenCalledWith('Form submitted:', expect.objectContaining({ + name: 'Alice', + surname: 'Smith', + isAcceptingTerms: true, + contact: expect.objectContaining({ + email: 'alice@example.com', + preferredContactMethod: 'whatsapp', + }), + })) + }) + }) + + it('removes the nested contact section again when the name field is cleared', async () => { + const user = userEvent.setup() + render() + + const nameInput = screen.getByRole('textbox', { name: /^name$/i }) + await user.type(nameInput, 'Alice') + expect(await screen.findByRole('heading', { name: /contacts/i })).toBeInTheDocument() + + await user.clear(nameInput) + + await waitFor(() => { + expect(screen.queryByRole('heading', { name: /contacts/i })).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/__tests__/base/notion-page-selector-flow.test.tsx b/web/__tests__/base/notion-page-selector-flow.test.tsx new file mode 100644 index 0000000000..6295d2dc00 --- /dev/null +++ b/web/__tests__/base/notion-page-selector-flow.test.tsx @@ -0,0 +1,151 @@ +import type { DataSourceCredential } from '@/app/components/header/account-setting/data-source-page-new/types' +import type { DataSourceNotionWorkspace } from '@/models/common' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import NotionPageSelector from '@/app/components/base/notion-page-selector/base' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' +import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types' + +const mockInvalidPreImportNotionPages = vi.fn() +const mockSetShowAccountSettingModal = vi.fn() +const mockUsePreImportNotionPages = vi.fn() + +vi.mock('@tanstack/react-virtual', () => ({ + useVirtualizer: ({ count }: { count: number }) => ({ + getVirtualItems: () => Array.from({ length: count }, (_, index) => ({ + index, + size: 28, + start: index * 28, + })), + getTotalSize: () => count * 28 + 16, + }), +})) + +vi.mock('@/service/knowledge/use-import', () => ({ + usePreImportNotionPages: (params: { datasetId: string, credentialId: string }) => mockUsePreImportNotionPages(params), + useInvalidPreImportNotionPages: () => mockInvalidPreImportNotionPages, +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContextSelector: (selector: (state: { setShowAccountSettingModal: typeof mockSetShowAccountSettingModal }) => unknown) => + selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }), +})) + +const buildCredential = (id: string, name: string, workspaceName: string): DataSourceCredential => ({ + id, + name, + type: CredentialTypeEnum.OAUTH2, + is_default: false, + avatar_url: '', + credential: { + workspace_icon: '', + workspace_name: workspaceName, + }, +}) + +const credentials: DataSourceCredential[] = [ + buildCredential('c1', 'Cred 1', 'Workspace 1'), + buildCredential('c2', 'Cred 2', 'Workspace 2'), +] + +const workspacePagesByCredential: Record = { + c1: [ + { + workspace_id: 'w1', + workspace_icon: '', + workspace_name: 'Workspace 1', + pages: [ + { page_id: 'root-1', page_name: 'Root 1', parent_id: 'root', page_icon: null, type: 'page', is_bound: false }, + { page_id: 'child-1', page_name: 'Child 1', parent_id: 'root-1', page_icon: null, type: 'page', is_bound: false }, + { page_id: 'bound-1', page_name: 'Bound 1', parent_id: 'root', page_icon: null, type: 'page', is_bound: true }, + ], + }, + ], + c2: [ + { + workspace_id: 'w2', + workspace_icon: '', + workspace_name: 'Workspace 2', + pages: [ + { page_id: 'external-1', page_name: 'External 1', parent_id: 'root', page_icon: null, type: 'page', is_bound: false }, + ], + }, + ], +} + +describe('Base Notion Page Selector Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUsePreImportNotionPages.mockImplementation(({ credentialId }: { credentialId: string }) => ({ + data: { + notion_info: workspacePagesByCredential[credentialId] ?? workspacePagesByCredential.c1, + }, + isFetching: false, + isError: false, + })) + }) + + it('selects a page tree, filters through search, clears search, and previews the current page', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + const onPreview = vi.fn() + + render( + , + ) + + await user.click(screen.getByTestId('checkbox-notion-page-checkbox-root-1')) + + expect(onSelect).toHaveBeenLastCalledWith(expect.arrayContaining([ + expect.objectContaining({ page_id: 'root-1', workspace_id: 'w1' }), + expect.objectContaining({ page_id: 'child-1', workspace_id: 'w1' }), + expect.objectContaining({ page_id: 'bound-1', workspace_id: 'w1' }), + ])) + + await user.type(screen.getByTestId('notion-search-input'), 'missing-page') + expect(screen.getByText('common.dataSource.notion.selector.noSearchResult')).toBeInTheDocument() + + await user.click(screen.getByTestId('notion-search-input-clear')) + expect(screen.getByTestId('notion-page-name-root-1')).toBeInTheDocument() + + await user.click(screen.getByTestId('notion-page-preview-root-1')) + expect(onPreview).toHaveBeenCalledWith(expect.objectContaining({ page_id: 'root-1', workspace_id: 'w1' })) + }) + + it('switches workspace credentials and opens the configuration entry point', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + const onSelectCredential = vi.fn() + + render( + , + ) + + expect(onSelectCredential).toHaveBeenCalledWith('c1') + + await user.click(screen.getByTestId('notion-credential-selector-btn')) + await user.click(screen.getByTestId('notion-credential-item-c2')) + + expect(mockInvalidPreImportNotionPages).toHaveBeenCalledWith({ datasetId: 'dataset-1', credentialId: 'c2' }) + expect(onSelect).toHaveBeenCalledWith([]) + + await waitFor(() => { + expect(onSelectCredential).toHaveBeenLastCalledWith('c2') + expect(screen.getByTestId('notion-page-name-external-1')).toBeInTheDocument() + }) + + await user.click(screen.getByRole('button', { name: 'common.dataSource.notion.selector.configure' })) + expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE }) + }) +}) diff --git a/web/__tests__/base/prompt-editor-flow.test.tsx b/web/__tests__/base/prompt-editor-flow.test.tsx new file mode 100644 index 0000000000..5fa96e6ee2 --- /dev/null +++ b/web/__tests__/base/prompt-editor-flow.test.tsx @@ -0,0 +1,191 @@ +import type { EventEmitter } from 'ahooks/lib/useEventEmitter' +import type { ComponentProps } from 'react' +import type { EventEmitterValue } from '@/context/event-emitter' +import { act, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { getNearestEditorFromDOMNode } from 'lexical' +import { useEffect } from 'react' +import PromptEditor from '@/app/components/base/prompt-editor' +import { + UPDATE_DATASETS_EVENT_EMITTER, + UPDATE_HISTORY_EVENT_EMITTER, +} from '@/app/components/base/prompt-editor/constants' +import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EventEmitterContextProvider } from '@/context/event-emitter-provider' + +type Captures = { + eventEmitter: EventEmitter | null + events: EventEmitterValue[] +} + +const EventProbe = ({ captures }: { captures: Captures }) => { + const { eventEmitter } = useEventEmitterContextContext() + + useEffect(() => { + captures.eventEmitter = eventEmitter + }, [captures, eventEmitter]) + + eventEmitter?.useSubscription((value) => { + captures.events.push(value) + }) + + return +} + +const PromptEditorHarness = ({ + captures, + ...props +}: ComponentProps & { captures: Captures }) => ( + + + + +) + +describe('Base Prompt Editor Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Real prompt editor integration should emit block updates and transform editor updates into text output. + describe('Editor Shell', () => { + it('should render with the real editor, emit dataset/history events, and convert update events into text changes', async () => { + const captures: Captures = { eventEmitter: null, events: [] } + const onChange = vi.fn() + const onFocus = vi.fn() + const onBlur = vi.fn() + const user = userEvent.setup() + + const { rerender, container } = render( + , + ) + + expect(screen.getByText('Type prompt')).toBeInTheDocument() + + await waitFor(() => { + expect(captures.eventEmitter).not.toBeNull() + }) + + const editable = container.querySelector('[contenteditable="true"]') as HTMLElement + expect(editable).toBeInTheDocument() + + await user.click(editable) + await waitFor(() => { + expect(onFocus).toHaveBeenCalledTimes(1) + }) + + await user.click(screen.getByRole('button', { name: 'outside' })) + await waitFor(() => { + expect(onBlur).toHaveBeenCalledTimes(1) + }) + + act(() => { + captures.eventEmitter?.emit({ + type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, + instanceId: 'editor-1', + payload: 'first line\nsecond line', + }) + }) + + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith('first line\nsecond line') + }) + + expect(captures.events).toContainEqual({ + type: UPDATE_DATASETS_EVENT_EMITTER, + payload: [{ id: 'ds-1', name: 'Dataset One', type: 'dataset' }], + }) + expect(captures.events).toContainEqual({ + type: UPDATE_HISTORY_EVENT_EMITTER, + payload: { user: 'user-role', assistant: 'assistant-role' }, + }) + + rerender( + , + ) + + await waitFor(() => { + expect(captures.events).toContainEqual({ + type: UPDATE_DATASETS_EVENT_EMITTER, + payload: [{ id: 'ds-2', name: 'Dataset Two', type: 'dataset' }], + }) + }) + expect(captures.events).toContainEqual({ + type: UPDATE_HISTORY_EVENT_EMITTER, + payload: { user: 'user-next', assistant: 'assistant-next' }, + }) + }) + + it('should tolerate updates without onChange and rethrow lexical runtime errors through the configured handler', async () => { + const captures: Captures = { eventEmitter: null, events: [] } + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + + const { container } = render( + , + ) + + await waitFor(() => { + expect(captures.eventEmitter).not.toBeNull() + }) + + act(() => { + captures.eventEmitter?.emit({ + type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, + instanceId: 'editor-2', + payload: 'silent update', + }) + }) + + const editable = container.querySelector('[contenteditable="false"]') as HTMLElement + const editor = getNearestEditorFromDOMNode(editable) + + expect(editable).toBeInTheDocument() + expect(editor).not.toBeNull() + expect(screen.getByRole('textbox')).toHaveTextContent('silent update') + + expect(() => { + act(() => { + editor?.update(() => { + throw new Error('prompt-editor boom') + }) + }) + }).toThrow('prompt-editor boom') + + consoleErrorSpy.mockRestore() + }) + }) +}) diff --git a/web/__tests__/custom/custom-page-flow.test.tsx b/web/__tests__/custom/custom-page-flow.test.tsx new file mode 100644 index 0000000000..6eb5ccadb9 --- /dev/null +++ b/web/__tests__/custom/custom-page-flow.test.tsx @@ -0,0 +1,107 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createMockProviderContextValue } from '@/__mocks__/provider-context' +import { contactSalesUrl, defaultPlan } from '@/app/components/billing/config' +import { Plan } from '@/app/components/billing/type' +import CustomPage from '@/app/components/custom/custom-page' +import useWebAppBrand from '@/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand' + +const mockSetShowPricingModal = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, + }), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: vi.fn(), +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowPricingModal: mockSetShowPricingModal, + }), +})) + +vi.mock('@/app/components/custom/custom-web-app-brand/hooks/use-web-app-brand', () => ({ + __esModule: true, + default: vi.fn(), +})) + +const { useProviderContext } = await import('@/context/provider-context') + +const mockUseProviderContext = vi.mocked(useProviderContext) +const mockUseWebAppBrand = vi.mocked(useWebAppBrand) + +const createBrandState = (overrides: Partial> = {}): ReturnType => ({ + fileId: '', + imgKey: 1, + uploadProgress: 0, + uploading: false, + webappLogo: 'https://example.com/logo.png', + webappBrandRemoved: false, + uploadDisabled: false, + workspaceLogo: 'https://example.com/workspace-logo.png', + isCurrentWorkspaceManager: true, + isSandbox: false, + handleApply: vi.fn(), + handleCancel: vi.fn(), + handleChange: vi.fn(), + handleRestore: vi.fn(), + handleSwitch: vi.fn(), + ...overrides, +}) + +const setProviderPlan = (planType: Plan, enableBilling = true) => { + mockUseProviderContext.mockReturnValue(createMockProviderContextValue({ + enableBilling, + plan: { + ...defaultPlan, + type: planType, + }, + })) +} + +describe('Custom Page Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + setProviderPlan(Plan.professional) + mockUseWebAppBrand.mockReturnValue(createBrandState()) + }) + + it('shows the billing upgrade banner for sandbox workspaces and opens pricing modal', () => { + setProviderPlan(Plan.sandbox) + + render() + + expect(screen.getByText('custom.upgradeTip.title')).toBeInTheDocument() + expect(screen.queryByText('custom.customize.contactUs')).not.toBeInTheDocument() + + fireEvent.click(screen.getByText('billing.upgradeBtn.encourageShort')) + + expect(mockSetShowPricingModal).toHaveBeenCalledTimes(1) + }) + + it('renders the branding controls and the sales contact footer for paid workspaces', () => { + const hookState = createBrandState({ + fileId: 'pending-logo', + }) + mockUseWebAppBrand.mockReturnValue(hookState) + + render() + + const contactLink = screen.getByText('custom.customize.contactUs').closest('a') + expect(contactLink).toHaveAttribute('href', contactSalesUrl) + + fireEvent.click(screen.getByRole('switch')) + fireEvent.click(screen.getByRole('button', { name: 'custom.restore' })) + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + fireEvent.click(screen.getByRole('button', { name: 'custom.apply' })) + + expect(hookState.handleSwitch).toHaveBeenCalledWith(true) + expect(hookState.handleRestore).toHaveBeenCalledTimes(1) + expect(hookState.handleCancel).toHaveBeenCalledTimes(1) + expect(hookState.handleApply).toHaveBeenCalledTimes(1) + }) +}) diff --git a/web/__tests__/header/account-dropdown-flow.test.tsx b/web/__tests__/header/account-dropdown-flow.test.tsx new file mode 100644 index 0000000000..6a645c7a43 --- /dev/null +++ b/web/__tests__/header/account-dropdown-flow.test.tsx @@ -0,0 +1,182 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { Plan } from '@/app/components/billing/type' +import AccountDropdown from '@/app/components/header/account-dropdown' +import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants' + +const { + mockPush, + mockLogout, + mockResetUser, + mockSetShowAccountSettingModal, +} = vi.hoisted(() => ({ + mockPush: vi.fn(), + mockLogout: vi.fn(), + mockResetUser: vi.fn(), + mockSetShowAccountSettingModal: vi.fn(), +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string, version?: string }) => { + if (options?.version) + return `${options.ns}.${key}:${options.version}` + return options?.ns ? `${options.ns}.${key}` : key + }, + }), +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: () => ({ + userProfile: { + name: 'Ada Lovelace', + email: 'ada@example.com', + avatar_url: '', + }, + langGeniusVersionInfo: { + current_version: '1.0.0', + latest_version: '1.1.0', + release_notes: 'https://example.com/releases/1.1.0', + }, + isCurrentWorkspaceOwner: false, + }), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: () => ({ + isEducationAccount: false, + plan: { + type: Plan.professional, + }, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector?: (state: Record) => unknown) => { + const state = { + systemFeatures: { + branding: { + enabled: false, + workspace_logo: null, + }, + }, + } + return selector ? selector(state) : state + }, +})) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowAccountSettingModal: mockSetShowAccountSettingModal, + }), +})) + +vi.mock('@/context/i18n', () => ({ + useDocLink: () => (path: string) => `https://docs.example.com${path}`, +})) + +vi.mock('@/service/use-common', () => ({ + useLogout: () => ({ + mutateAsync: mockLogout, + }), +})) + +vi.mock('@/app/components/base/amplitude/utils', () => ({ + resetUser: mockResetUser, +})) + +vi.mock('@/next/navigation', () => ({ + useRouter: () => ({ + push: mockPush, + }), +})) + +vi.mock('@/next/link', () => ({ + default: ({ + href, + children, + ...props + }: { + href: string + children?: React.ReactNode + } & Record) => ( + + {children} + + ), +})) + +const renderAccountDropdown = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) + + return render( + + + , + ) +} + +describe('Header Account Dropdown Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify({ + repo: { stars: 123456 }, + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + })) + localStorage.clear() + }) + + it('opens account actions, fetches github stars, and opens the settings and about flows', async () => { + renderAccountDropdown() + + fireEvent.click(screen.getByRole('button', { name: 'common.account.account' })) + + expect(screen.getByText('Ada Lovelace')).toBeInTheDocument() + expect(screen.getByText('ada@example.com')).toBeInTheDocument() + expect(await screen.findByText('123,456')).toBeInTheDocument() + + fireEvent.click(screen.getByText('common.userProfile.settings')) + + expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ + payload: ACCOUNT_SETTING_TAB.MEMBERS, + }) + + fireEvent.click(screen.getByText('common.userProfile.about')) + + await waitFor(() => { + expect(screen.getByText(/Version/)).toBeInTheDocument() + expect(screen.getByText(/1\.0\.0/)).toBeInTheDocument() + }) + }) + + it('logs out, resets cached user markers, and redirects to signin', async () => { + localStorage.setItem('setup_status', 'done') + localStorage.setItem('education-reverify-prev-expire-at', '1') + localStorage.setItem('education-reverify-has-noticed', '1') + localStorage.setItem('education-expired-has-noticed', '1') + + renderAccountDropdown() + + fireEvent.click(screen.getByRole('button', { name: 'common.account.account' })) + fireEvent.click(screen.getByText('common.userProfile.logout')) + + await waitFor(() => { + expect(mockLogout).toHaveBeenCalledTimes(1) + expect(mockResetUser).toHaveBeenCalledTimes(1) + expect(mockPush).toHaveBeenCalledWith('/signin') + }) + + expect(localStorage.getItem('setup_status')).toBeNull() + expect(localStorage.getItem('education-reverify-prev-expire-at')).toBeNull() + expect(localStorage.getItem('education-reverify-has-noticed')).toBeNull() + expect(localStorage.getItem('education-expired-has-noticed')).toBeNull() + }) +}) diff --git a/web/__tests__/header/nav-flow.test.tsx b/web/__tests__/header/nav-flow.test.tsx new file mode 100644 index 0000000000..05955a6c83 --- /dev/null +++ b/web/__tests__/header/nav-flow.test.tsx @@ -0,0 +1,237 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import Nav from '@/app/components/header/nav' +import { AppModeEnum } from '@/types/app' + +const mockPush = vi.fn() +const mockSetAppDetail = vi.fn() +const mockOnCreate = vi.fn() +const mockOnLoadMore = vi.fn() + +let mockSelectedSegment = 'app' +let mockIsCurrentWorkspaceEditor = true + +vi.mock('@headlessui/react', () => { + type MenuContextValue = { + open: boolean + setOpen: React.Dispatch> + } + const MenuContext = React.createContext(null) + + const Menu = ({ + children, + }: { + children: React.ReactNode | ((props: { open: boolean }) => React.ReactNode) + }) => { + const [open, setOpen] = React.useState(false) + const value = React.useMemo(() => ({ open, setOpen }), [open]) + + return ( + + {typeof children === 'function' ? children({ open }) : children} + + ) + } + + const MenuButton = ({ + children, + onClick, + ...props + }: React.ButtonHTMLAttributes) => { + const context = React.useContext(MenuContext) + + return ( + + ) + } + + const MenuItems = ({ + as: Component = 'div', + children, + ...props + }: { + as?: React.ElementType + children: React.ReactNode + } & Record) => { + const context = React.useContext(MenuContext) + if (!context?.open) + return null + + return {children} + } + + const MenuItem = ({ + as: Component = 'div', + children, + ...props + }: { + as?: React.ElementType + children: React.ReactNode + } & Record) => {children} + + return { + Menu, + MenuButton, + MenuItems, + MenuItem, + Transition: ({ children }: { children: React.ReactNode }) => <>{children}, + } +}) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +vi.mock('@/next/navigation', () => ({ + useSelectedLayoutSegment: () => mockSelectedSegment, + useRouter: () => ({ + push: mockPush, + }), +})) + +vi.mock('@/next/link', () => ({ + default: ({ + href, + children, + }: { + href: string + children?: React.ReactNode + }) => {children}, +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: () => mockSetAppDetail, +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: () => ({ + isCurrentWorkspaceEditor: mockIsCurrentWorkspaceEditor, + }), +})) + +const navigationItems = [ + { + id: 'app-1', + name: 'Alpha', + link: '/app/app-1/configuration', + icon_type: 'emoji' as const, + icon: '🤖', + icon_background: '#FFEAD5', + icon_url: null, + mode: AppModeEnum.CHAT, + }, + { + id: 'app-2', + name: 'Bravo', + link: '/app/app-2/workflow', + icon_type: 'emoji' as const, + icon: '⚙️', + icon_background: '#E0F2FE', + icon_url: null, + mode: AppModeEnum.WORKFLOW, + }, +] + +const curNav = { + id: 'app-1', + name: 'Alpha', + icon_type: 'emoji' as const, + icon: '🤖', + icon_background: '#FFEAD5', + icon_url: null, + mode: AppModeEnum.CHAT, +} + +const renderNav = (nav = curNav) => { + return render( +
} + marketplace={
marketplace view
} + />, + { searchParams }, + ) +} + +describe('Plugin Page Shell Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + mockFetchManifestFromMarketPlace.mockResolvedValue({ + data: { + plugin: { + org: 'langgenius', + name: 'plugin-demo', + }, + version: { + version: '1.0.0', + }, + }, + }) + }) + + it('switches from installed plugins to marketplace and syncs the active tab into the URL', async () => { + const { onUrlUpdate } = renderPluginPage() + + expect(screen.getByTestId('plugins-view')).toBeInTheDocument() + expect(screen.queryByTestId('marketplace-view')).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId('tab-item-discover')) + + await waitFor(() => { + expect(screen.getByTestId('marketplace-view')).toBeInTheDocument() + }) + + const tabUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + expect(tabUpdate.searchParams.get('tab')).toBe('discover') + }) + + it('hydrates marketplace installation from query params and clears the install state when closed', async () => { + const { onUrlUpdate } = renderPluginPage('?package-ids=%5B%22langgenius%2Fplugin-demo%22%5D') + + await waitFor(() => { + expect(mockFetchManifestFromMarketPlace).toHaveBeenCalledWith('langgenius%2Fplugin-demo') + expect(screen.getByTestId('install-from-marketplace-modal')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: 'close-install-modal' })) + + await waitFor(() => { + const clearUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + expect(clearUpdate.searchParams.has('package-ids')).toBe(false) + }) + }) +}) diff --git a/web/__tests__/share/text-generation-mode-flow.test.tsx b/web/__tests__/share/text-generation-mode-flow.test.tsx new file mode 100644 index 0000000000..0d4307bca0 --- /dev/null +++ b/web/__tests__/share/text-generation-mode-flow.test.tsx @@ -0,0 +1,155 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import TextGeneration from '@/app/components/share/text-generation' + +const useSearchParamsMock = vi.fn(() => new URLSearchParams()) +const mockUseTextGenerationAppState = vi.fn() +const mockUseTextGenerationBatch = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, + }), +})) + +vi.mock('@/next/navigation', () => ({ + useSearchParams: () => useSearchParamsMock(), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + __esModule: true, + default: () => 'pc', + MediaType: { pc: 'pc', pad: 'pad', mobile: 'mobile' }, +})) + +vi.mock('@/app/components/share/text-generation/hooks/use-text-generation-app-state', () => ({ + useTextGenerationAppState: (...args: unknown[]) => mockUseTextGenerationAppState(...args), +})) + +vi.mock('@/app/components/share/text-generation/hooks/use-text-generation-batch', () => ({ + useTextGenerationBatch: (...args: unknown[]) => mockUseTextGenerationBatch(...args), +})) + +vi.mock('@/app/components/share/text-generation/text-generation-sidebar', () => ({ + default: ({ + currentTab, + onTabChange, + }: { + currentTab: string + onTabChange: (tab: string) => void + }) => ( +
+ {currentTab} + + +
+ ), +})) + +vi.mock('@/app/components/share/text-generation/text-generation-result-panel', () => ({ + default: ({ + isCallBatchAPI, + resultExisted, + }: { + isCallBatchAPI: boolean + resultExisted: boolean + }) => ( +
+ ), +})) + +const createReadyAppState = () => ({ + accessMode: 'public', + appId: 'app-123', + appSourceType: 'published', + customConfig: { + remove_webapp_brand: false, + replace_webapp_logo: '', + }, + handleRemoveSavedMessage: vi.fn(), + handleSaveMessage: vi.fn(), + moreLikeThisConfig: { + enabled: true, + }, + promptConfig: { + user_input_form: [], + }, + savedMessages: [], + siteInfo: { + title: 'Text Generation', + }, + systemFeatures: { + branding: { + enabled: false, + workspace_logo: null, + }, + }, + textToSpeechConfig: { + enabled: true, + }, + visionConfig: null, +}) + +const createBatchState = () => ({ + allFailedTaskList: [], + allSuccessTaskList: [], + allTaskList: [], + allTasksRun: false, + controlRetry: 0, + exportRes: vi.fn(), + handleCompleted: vi.fn(), + handleRetryAllFailedTask: vi.fn(), + handleRunBatch: vi.fn(), + isCallBatchAPI: false, + noPendingTask: true, + resetBatchExecution: vi.fn(), + setIsCallBatchAPI: vi.fn(), + showTaskList: false, +}) + +describe('Text Generation Mode Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + useSearchParamsMock.mockReturnValue(new URLSearchParams()) + mockUseTextGenerationAppState.mockReturnValue(createReadyAppState()) + mockUseTextGenerationBatch.mockReturnValue(createBatchState()) + }) + + it('shows the loading state before app metadata is ready', () => { + mockUseTextGenerationAppState.mockReturnValue({ + ...createReadyAppState(), + appId: '', + promptConfig: null, + siteInfo: null, + }) + + render() + + expect(screen.getByRole('status', { name: 'appApi.loading' })).toBeInTheDocument() + }) + + it('hydrates the initial tab from the mode query parameter and lets the sidebar switch it', () => { + useSearchParamsMock.mockReturnValue(new URLSearchParams('mode=batch')) + + render() + + expect(screen.getByTestId('current-tab')).toHaveTextContent('batch') + + fireEvent.click(screen.getByRole('button', { name: 'switch-to-create' })) + + expect(screen.getByTestId('current-tab')).toHaveTextContent('create') + }) + + it('falls back to create mode for unsupported query values', () => { + useSearchParamsMock.mockReturnValue(new URLSearchParams('mode=unsupported')) + + render() + + expect(screen.getByTestId('current-tab')).toHaveTextContent('create') + expect(screen.getByTestId('text-generation-result-panel')).toHaveAttribute('data-batch', 'false') + }) +}) diff --git a/web/__tests__/tools/provider-list-shell-flow.test.tsx b/web/__tests__/tools/provider-list-shell-flow.test.tsx new file mode 100644 index 0000000000..5b6ba8a64b --- /dev/null +++ b/web/__tests__/tools/provider-list-shell-flow.test.tsx @@ -0,0 +1,205 @@ +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import ProviderList from '@/app/components/tools/provider-list' +import { CollectionType } from '@/app/components/tools/types' +import { renderWithNuqs } from '@/test/nuqs-testing' + +const mockInvalidateInstalledPluginList = vi.fn() + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { ns?: string }) => options?.ns ? `${options.ns}.${key}` : key, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: Record) => unknown) => selector({ + systemFeatures: { + enable_marketplace: true, + }, + }), +})) + +vi.mock('@/app/components/plugins/hooks', () => ({ + useTags: () => ({ + getTagLabel: (name: string) => name, + }), +})) + +vi.mock('@/service/use-tools', () => ({ + useAllToolProviders: () => ({ + data: [ + { + id: 'builtin-plugin', + name: 'plugin-tool', + author: 'Dify', + description: { en_US: 'Plugin Tool' }, + icon: 'icon-plugin', + label: { en_US: 'Plugin Tool' }, + type: CollectionType.builtIn, + team_credentials: {}, + is_team_authorization: false, + allow_delete: false, + labels: ['search'], + plugin_id: 'langgenius/plugin-tool', + }, + { + id: 'builtin-basic', + name: 'basic-tool', + author: 'Dify', + description: { en_US: 'Basic Tool' }, + icon: 'icon-basic', + label: { en_US: 'Basic Tool' }, + type: CollectionType.builtIn, + team_credentials: {}, + is_team_authorization: false, + allow_delete: false, + labels: ['utility'], + }, + ], + refetch: vi.fn(), + }), +})) + +vi.mock('@/service/use-plugins', () => ({ + useCheckInstalled: ({ enabled }: { enabled: boolean }) => ({ + data: enabled + ? { + plugins: [{ + plugin_id: 'langgenius/plugin-tool', + declaration: { + category: 'tool', + }, + }], + } + : null, + }), + useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList, +})) + +vi.mock('@/app/components/tools/labels/filter', () => ({ + default: ({ onChange }: { onChange: (value: string[]) => void }) => ( +
+ +
+ ), +})) + +vi.mock('@/app/components/plugins/card', () => ({ + default: ({ payload, className }: { payload: { name: string }, className?: string }) => ( +
+ {payload.name} +
+ ), +})) + +vi.mock('@/app/components/plugins/card/card-more-info', () => ({ + default: ({ tags }: { tags: string[] }) =>
{tags.join(',')}
, +})) + +vi.mock('@/app/components/tools/provider/detail', () => ({ + default: ({ collection, onHide }: { collection: { name: string }, onHide: () => void }) => ( +
+ {collection.name} + +
+ ), +})) + +vi.mock('@/app/components/plugins/plugin-detail-panel', () => ({ + default: ({ + detail, + onHide, + onUpdate, + }: { + detail?: { plugin_id: string } + onHide: () => void + onUpdate: () => void + }) => detail + ? ( +
+ {detail.plugin_id} + + +
+ ) + : null, +})) + +vi.mock('@/app/components/tools/provider/empty', () => ({ + default: () =>
workflow empty
, +})) + +vi.mock('@/app/components/plugins/marketplace/empty', () => ({ + default: ({ text }: { text: string }) =>
{text}
, +})) + +vi.mock('@/app/components/tools/marketplace', () => ({ + default: ({ + isMarketplaceArrowVisible, + showMarketplacePanel, + }: { + isMarketplaceArrowVisible: boolean + showMarketplacePanel: () => void + }) => ( + + ), +})) + +vi.mock('@/app/components/tools/marketplace/hooks', () => ({ + useMarketplace: () => ({ + handleScroll: vi.fn(), + }), +})) + +vi.mock('@/app/components/tools/mcp', () => ({ + default: ({ searchText }: { searchText: string }) =>
{searchText}
, +})) + +const renderProviderList = (searchParams = '') => { + return renderWithNuqs(, { searchParams }) +} + +describe('Tool Provider List Shell Flow', () => { + beforeEach(() => { + vi.clearAllMocks() + Element.prototype.scrollTo = vi.fn() + }) + + it('opens a plugin-backed provider detail panel and invalidates installed plugins on update', async () => { + renderProviderList('?category=builtin') + + fireEvent.click(screen.getByTestId('tool-card-plugin-tool')) + + await waitFor(() => { + expect(screen.getByTestId('tool-plugin-detail-panel')).toBeInTheDocument() + }) + + fireEvent.click(screen.getByRole('button', { name: 'update-plugin-detail' })) + expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1) + + fireEvent.click(screen.getByRole('button', { name: 'close-plugin-detail' })) + + await waitFor(() => { + expect(screen.queryByTestId('tool-plugin-detail-panel')).not.toBeInTheDocument() + }) + }) + + it('scrolls to the marketplace section and syncs workflow tab selection into the URL', async () => { + const { onUrlUpdate } = renderProviderList('?category=builtin') + + fireEvent.click(screen.getByTestId('marketplace-arrow')) + expect(Element.prototype.scrollTo).toHaveBeenCalled() + + fireEvent.click(screen.getByTestId('tab-item-workflow')) + + await waitFor(() => { + expect(screen.getByTestId('workflow-empty')).toBeInTheDocument() + }) + + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + expect(update.searchParams.get('category')).toBe('workflow') + }) +}) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx index 1df6fa79b7..dcc9f9c98e 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/dropdown-callbacks.spec.tsx @@ -18,6 +18,7 @@ const mockInvalidDatasetDetail = vi.fn() const mockExportPipeline = vi.fn() const mockCheckIsUsedInApp = vi.fn() const mockDeleteDataset = vi.fn() +const mockToast = vi.fn() const createDataset = (overrides: Partial = {}): DataSet => ({ id: 'dataset-1', @@ -111,6 +112,10 @@ vi.mock('@/service/datasets', () => ({ deleteDataset: (...args: unknown[]) => mockDeleteDataset(...args), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: (...args: unknown[]) => mockToast(...args), +})) + vi.mock('@/app/components/datasets/rename-modal', () => ({ default: ({ show, @@ -225,4 +230,49 @@ describe('Dropdown callback coverage', () => { expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() }) }) + + it('should show the used-by-app confirmation copy when the dataset is referenced by apps', async () => { + const user = userEvent.setup() + mockCheckIsUsedInApp.mockResolvedValueOnce({ is_using: true }) + + render() + + await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByText('common.operation.delete')) + + await waitFor(() => { + expect(screen.getByText('dataset.datasetUsedByApp')).toBeInTheDocument() + }) + }) + + it('should surface an export failure toast when pipeline export fails', async () => { + const user = userEvent.setup() + mockExportPipeline.mockRejectedValueOnce(new Error('export failed')) + + render() + + await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByText('datasetPipeline.operations.exportPipeline')) + + await waitFor(() => { + expect(mockToast).toHaveBeenCalledWith('app.exportFailed', { type: 'error' }) + }) + }) + + it('should surface the backend message when checking app usage fails', async () => { + const user = userEvent.setup() + mockCheckIsUsedInApp.mockRejectedValueOnce({ + json: vi.fn().mockResolvedValue({ message: 'check failed' }), + }) + + render() + + await user.click(screen.getByTestId('portal-trigger')) + await user.click(screen.getByText('common.operation.delete')) + + await waitFor(() => { + expect(mockToast).toHaveBeenCalledWith('check failed', { type: 'error' }) + }) + expect(screen.queryByTestId('confirm-dialog')).not.toBeInTheDocument() + }) }) diff --git a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx index a1e275d731..bb85e00c14 100644 --- a/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/dataset-info/__tests__/index.spec.tsx @@ -1,6 +1,6 @@ import type { DataSet } from '@/models/datasets' import { RiEditLine } from '@remixicon/react' -import { render, screen, waitFor } from '@testing-library/react' +import { createEvent, fireEvent, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' import { @@ -218,6 +218,31 @@ describe('MenuItem', () => { // Assert expect(handleClick).toHaveBeenCalledTimes(1) }) + + it('should stop propagation before invoking the handler', () => { + const parentClick = vi.fn() + const handleClick = vi.fn() + + render( +
+ +
, + ) + + fireEvent.click(screen.getByText('Edit')) + + expect(handleClick).toHaveBeenCalledTimes(1) + expect(parentClick).not.toHaveBeenCalled() + }) + + it('should not crash when no click handler is provided', () => { + render() + + const event = createEvent.click(screen.getByText('Edit')) + fireEvent(screen.getByText('Edit'), event) + + expect(event.defaultPrevented).toBe(true) + }) }) }) @@ -265,6 +290,47 @@ describe('Menu', () => { expect(screen.queryByText('common.operation.delete')).not.toBeInTheDocument() }) }) + + describe('Interactions', () => { + it('should invoke the rename callback when edit is clicked', async () => { + const user = userEvent.setup() + const openRenameModal = vi.fn() + + render( + , + ) + + await user.click(screen.getByText('common.operation.edit')) + + expect(openRenameModal).toHaveBeenCalledTimes(1) + }) + + it('should invoke export and delete callbacks from their menu items', async () => { + const user = userEvent.setup() + const handleExportPipeline = vi.fn() + const detectIsUsedByApp = vi.fn() + + render( + , + ) + + await user.click(screen.getByText('datasetPipeline.operations.exportPipeline')) + await user.click(screen.getByText('common.operation.delete')) + + expect(handleExportPipeline).toHaveBeenCalledTimes(1) + expect(detectIsUsedByApp).toHaveBeenCalledTimes(1) + }) + }) }) describe('Dropdown', () => { diff --git a/web/app/components/apps/__tests__/app-card-skeleton.spec.tsx b/web/app/components/apps/__tests__/app-card-skeleton.spec.tsx new file mode 100644 index 0000000000..f43db2f5f9 --- /dev/null +++ b/web/app/components/apps/__tests__/app-card-skeleton.spec.tsx @@ -0,0 +1,24 @@ +import { render } from '@testing-library/react' +import { AppCardSkeleton } from '../app-card-skeleton' + +describe('AppCardSkeleton', () => { + it('should render six skeleton cards by default', () => { + const { container } = render() + + expect(container.childElementCount).toBe(6) + expect(AppCardSkeleton.displayName).toBe('AppCardSkeleton') + }) + + it('should respect the custom skeleton count and card classes', () => { + const { container } = render() + + expect(container.childElementCount).toBe(2) + expect(container.firstElementChild).toHaveClass( + 'h-[160px]', + 'rounded-xl', + 'border-[0.5px]', + 'bg-components-card-bg', + 'p-4', + ) + }) +}) diff --git a/web/app/components/base/chat/chat/__tests__/chat-log-modals.spec.tsx b/web/app/components/base/chat/chat/__tests__/chat-log-modals.spec.tsx new file mode 100644 index 0000000000..36e7cec67d --- /dev/null +++ b/web/app/components/base/chat/chat/__tests__/chat-log-modals.spec.tsx @@ -0,0 +1,144 @@ +import type { IChatItem } from '../type' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useStore as useAppStore } from '@/app/components/app/store' +import { fetchAgentLogDetail } from '@/service/log' +import ChatLogModals from '../chat-log-modals' + +vi.mock('@/service/log', () => ({ + fetchAgentLogDetail: vi.fn(), +})) + +describe('ChatLogModals', () => { + beforeEach(() => { + vi.clearAllMocks() + useAppStore.setState({ appDetail: { id: 'app-1' } as ReturnType['appDetail'] }) + }) + + // Modal visibility should follow the two booleans unless log modals are globally hidden. + describe('Rendering', () => { + it('should render real prompt and agent log modals when enabled', async () => { + vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + + render( + , + ) + + expect(screen.getByText('PROMPT LOG')).toBeInTheDocument() + expect(screen.getByText('Prompt body')).toBeInTheDocument() + + await waitFor(() => { + expect(screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i })).toBeInTheDocument() + }) + }) + + it('should render nothing when hideLogModal is true', () => { + render( + , + ) + + expect(screen.queryByText('PROMPT LOG')).not.toBeInTheDocument() + expect(screen.queryByRole('heading', { name: /appLog.runDetail.workflowTitle/i })).not.toBeInTheDocument() + }) + }) + + // Cancel actions should clear the current item and close only the targeted modal. + describe('User Interactions', () => { + it('should close the prompt log modal through the real close action', async () => { + const user = userEvent.setup() + const setCurrentLogItem = vi.fn() + const setShowPromptLogModal = vi.fn() + const setShowAgentLogModal = vi.fn() + + render( + , + ) + + await user.click(screen.getByTestId('close-btn-container')) + + expect(setCurrentLogItem).toHaveBeenCalled() + expect(setShowPromptLogModal).toHaveBeenCalledWith(false) + expect(setShowAgentLogModal).not.toHaveBeenCalled() + }) + + it('should close the agent log modal through the real close action', async () => { + const user = userEvent.setup() + const setCurrentLogItem = vi.fn() + const setShowPromptLogModal = vi.fn() + const setShowAgentLogModal = vi.fn() + vi.mocked(fetchAgentLogDetail).mockReturnValue(new Promise(() => {})) + + render( + , + ) + + await waitFor(() => { + expect(screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i })).toBeInTheDocument() + }) + await user.click(screen.getByRole('heading', { name: /appLog.runDetail.workflowTitle/i }).nextElementSibling as HTMLElement) + + expect(setCurrentLogItem).toHaveBeenCalled() + expect(setShowAgentLogModal).toHaveBeenCalledWith(false) + expect(setShowPromptLogModal).not.toHaveBeenCalled() + }) + }) +}) diff --git a/web/app/components/base/chat/chat/__tests__/use-chat-layout.spec.tsx b/web/app/components/base/chat/chat/__tests__/use-chat-layout.spec.tsx new file mode 100644 index 0000000000..15da63e4d0 --- /dev/null +++ b/web/app/components/base/chat/chat/__tests__/use-chat-layout.spec.tsx @@ -0,0 +1,293 @@ +import type { ChatItem } from '../../types' +import { act, fireEvent, render, screen } from '@testing-library/react' +import { + afterEach, + beforeEach, + describe, + expect, + it, + vi, +} from 'vitest' +import { useChatLayout } from '../use-chat-layout' + +type ResizeCallback = (entries: ResizeObserverEntry[], observer: ResizeObserver) => void + +let capturedResizeCallbacks: ResizeCallback[] = [] +let disconnectSpy: ReturnType +let rafCallbacks: FrameRequestCallback[] = [] + +const makeChatItem = (overrides: Partial = {}): ChatItem => ({ + id: `item-${Math.random().toString(36).slice(2)}`, + content: 'Test content', + isAnswer: false, + ...overrides, +}) + +const makeResizeEntry = (blockSize: number, inlineSize: number): ResizeObserverEntry => ({ + borderBoxSize: [{ blockSize, inlineSize } as ResizeObserverSize], + contentBoxSize: [{ blockSize, inlineSize } as ResizeObserverSize], + contentRect: new DOMRect(0, 0, inlineSize, blockSize), + devicePixelContentBoxSize: [{ blockSize, inlineSize } as ResizeObserverSize], + target: document.createElement('div'), +}) + +const assignMetric = (node: HTMLElement, key: 'clientWidth' | 'clientHeight' | 'scrollHeight', value: number) => { + Object.defineProperty(node, key, { + configurable: true, + value, + }) +} + +const LayoutHarness = ({ + chatList, + sidebarCollapseState, + attachRefs = true, +}: { + chatList: ChatItem[] + sidebarCollapseState?: boolean + attachRefs?: boolean +}) => { + const { + width, + chatContainerRef, + chatContainerInnerRef, + chatFooterRef, + chatFooterInnerRef, + } = useChatLayout({ chatList, sidebarCollapseState }) + + return ( + <> +
{ + chatContainerRef.current = attachRefs ? node : null + if (node && attachRefs) { + assignMetric(node, 'clientWidth', 400) + assignMetric(node, 'clientHeight', 240) + assignMetric(node, 'scrollHeight', 640) + if (!node.dataset.metricsReady) { + node.scrollTop = 0 + node.dataset.metricsReady = 'true' + } + } + }} + > +
{ + chatContainerInnerRef.current = attachRefs ? node : null + if (node && attachRefs) + assignMetric(node, 'clientWidth', 360) + }} + /> +
+
{ + chatFooterRef.current = attachRefs ? node : null + }} + > +
{ + chatFooterInnerRef.current = attachRefs ? node : null + }} + /> +
+ {width} + + ) +} + +const flushAnimationFrames = () => { + const queuedCallbacks = [...rafCallbacks] + rafCallbacks = [] + queuedCallbacks.forEach(callback => callback(0)) +} + +describe('useChatLayout', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + capturedResizeCallbacks = [] + disconnectSpy = vi.fn() + rafCallbacks = [] + + Object.defineProperty(document.body, 'clientWidth', { + configurable: true, + value: 1024, + }) + + vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) => { + rafCallbacks.push(cb) + return rafCallbacks.length + }) + + vi.stubGlobal('ResizeObserver', class { + constructor(cb: ResizeCallback) { + capturedResizeCallbacks.push(cb) + } + + observe() { } + unobserve() { } + disconnect = disconnectSpy + }) + }) + + afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + }) + + // The hook should compute shell dimensions and auto-scroll when enough chat items exist. + describe('Layout Calculation', () => { + it('should auto-scroll and compute the chat shell widths on mount', () => { + const addSpy = vi.spyOn(window, 'addEventListener') + + render( + , + ) + + act(() => { + flushAnimationFrames() + vi.runAllTimers() + }) + + expect(screen.getByTestId('layout-width')).toHaveTextContent('600') + expect(screen.getByTestId('chat-footer').style.width).toBe('400px') + expect(screen.getByTestId('chat-footer-inner').style.width).toBe('360px') + expect((screen.getByTestId('chat-container') as HTMLDivElement).scrollTop).toBe(640) + expect(addSpy).toHaveBeenCalledWith('resize', expect.any(Function)) + }) + }) + + // Resize observers should keep padding and widths in sync, then fully clean up on unmount. + describe('Resize Observers', () => { + it('should react to observer updates and disconnect both observers on unmount', () => { + const removeSpy = vi.spyOn(window, 'removeEventListener') + const { unmount } = render( + , + ) + + act(() => { + capturedResizeCallbacks[0]?.([makeResizeEntry(80, 400)], {} as ResizeObserver) + }) + expect(screen.getByTestId('chat-container').style.paddingBottom).toBe('80px') + + act(() => { + capturedResizeCallbacks[1]?.([makeResizeEntry(50, 560)], {} as ResizeObserver) + }) + expect(screen.getByTestId('chat-footer').style.width).toBe('560px') + + unmount() + + expect(removeSpy).toHaveBeenCalledWith('resize', expect.any(Function)) + expect(disconnectSpy).toHaveBeenCalledTimes(2) + }) + + it('should respect manual scrolling until a new first message arrives and safely ignore missing refs', () => { + const { rerender } = render( + , + ) + + const container = screen.getByTestId('chat-container') as HTMLDivElement + + act(() => { + fireEvent.scroll(container) + flushAnimationFrames() + }) + + act(() => { + container.scrollTop = 10 + fireEvent.scroll(container) + }) + + rerender( + , + ) + + act(() => { + flushAnimationFrames() + vi.runAllTimers() + }) + + act(() => { + container.scrollTop = 420 + fireEvent.scroll(container) + }) + + rerender( + , + ) + + act(() => { + flushAnimationFrames() + vi.runAllTimers() + }) + + expect(container.scrollTop).toBe(640) + + rerender( + , + ) + + act(() => { + fireEvent.scroll(container) + flushAnimationFrames() + }) + }) + + it('should keep the hook stable when the DOM refs are not attached', () => { + render( + , + ) + + act(() => { + flushAnimationFrames() + vi.runAllTimers() + }) + + expect(screen.getByTestId('layout-width')).toHaveTextContent('0') + expect(capturedResizeCallbacks).toHaveLength(0) + expect(screen.getByTestId('chat-footer').style.width).toBe('') + expect(screen.getByTestId('chat-footer-inner').style.width).toBe('') + }) + }) +}) diff --git a/web/app/components/base/chat/chat/chat-log-modals.tsx b/web/app/components/base/chat/chat/chat-log-modals.tsx new file mode 100644 index 0000000000..d1bf43b81c --- /dev/null +++ b/web/app/components/base/chat/chat/chat-log-modals.tsx @@ -0,0 +1,56 @@ +import type { FC } from 'react' +import type { IChatItem } from './type' +import AgentLogModal from '@/app/components/base/agent-log-modal' +import PromptLogModal from '@/app/components/base/prompt-log-modal' + +type ChatLogModalsProps = { + width: number + currentLogItem?: IChatItem + showPromptLogModal: boolean + showAgentLogModal: boolean + hideLogModal?: boolean + setCurrentLogItem: (item?: IChatItem) => void + setShowPromptLogModal: (showPromptLogModal: boolean) => void + setShowAgentLogModal: (showAgentLogModal: boolean) => void +} + +const ChatLogModals: FC = ({ + width, + currentLogItem, + showPromptLogModal, + showAgentLogModal, + hideLogModal, + setCurrentLogItem, + setShowPromptLogModal, + setShowAgentLogModal, +}) => { + if (hideLogModal) + return null + + return ( + <> + {showPromptLogModal && ( + { + setCurrentLogItem() + setShowPromptLogModal(false) + }} + /> + )} + {showAgentLogModal && ( + { + setCurrentLogItem() + setShowAgentLogModal(false) + }} + /> + )} + + ) +} + +export default ChatLogModals diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index ed44c8719d..f04169327f 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -13,26 +13,19 @@ import type { import type { InputForm } from './type' import type { Emoji } from '@/app/components/tools/types' import type { AppData } from '@/models/share' -import { debounce } from 'es-toolkit/compat' -import { - memo, - useCallback, - useEffect, - useRef, - useState, -} from 'react' +import { memo } from 'react' import { useTranslation } from 'react-i18next' import { useShallow } from 'zustand/react/shallow' import { useStore as useAppStore } from '@/app/components/app/store' -import AgentLogModal from '@/app/components/base/agent-log-modal' import Button from '@/app/components/base/button' -import PromptLogModal from '@/app/components/base/prompt-log-modal' import { cn } from '@/utils/classnames' import Answer from './answer' import ChatInputArea from './chat-input-area' +import ChatLogModals from './chat-log-modals' import { ChatContextProvider } from './context-provider' import Question from './question' import TryToAsk from './try-to-ask' +import { useChatLayout } from './use-chat-layout' export type ChatProps = { isTryApp?: boolean @@ -133,128 +126,17 @@ const Chat: FC = ({ showAgentLogModal: state.showAgentLogModal, setShowAgentLogModal: state.setShowAgentLogModal, }))) - const [width, setWidth] = useState(0) - const chatContainerRef = useRef(null) - const chatContainerInnerRef = useRef(null) - const chatFooterRef = useRef(null) - const chatFooterInnerRef = useRef(null) - const userScrolledRef = useRef(false) - const isAutoScrollingRef = useRef(false) - - const handleScrollToBottom = useCallback(() => { - if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current) { - isAutoScrollingRef.current = true - chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight - - requestAnimationFrame(() => { - isAutoScrollingRef.current = false - }) - } - }, [chatList.length]) - - const handleWindowResize = useCallback(() => { - if (chatContainerRef.current) - setWidth(document.body.clientWidth - (chatContainerRef.current?.clientWidth + 16) - 8) - - if (chatContainerRef.current && chatFooterRef.current) - chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px` - - if (chatContainerInnerRef.current && chatFooterInnerRef.current) - chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px` - }, []) - - useEffect(() => { - handleScrollToBottom() - handleWindowResize() - }, [handleScrollToBottom, handleWindowResize]) - - useEffect(() => { - /* v8 ignore next - @preserve */ - if (chatContainerRef.current) { - requestAnimationFrame(() => { - handleScrollToBottom() - handleWindowResize() - }) - } + const { + width, + chatContainerRef, + chatContainerInnerRef, + chatFooterRef, + chatFooterInnerRef, + } = useChatLayout({ + chatList, + sidebarCollapseState, }) - useEffect(() => { - const debouncedHandler = debounce(handleWindowResize, 200) - window.addEventListener('resize', debouncedHandler) - - return () => { - window.removeEventListener('resize', debouncedHandler) - debouncedHandler.cancel() - } - }, [handleWindowResize]) - - useEffect(() => { - /* v8 ignore next - @preserve */ - if (chatFooterRef.current && chatContainerRef.current) { - const resizeContainerObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const { blockSize } = entry.borderBoxSize[0] - chatContainerRef.current!.style.paddingBottom = `${blockSize}px` - handleScrollToBottom() - } - }) - resizeContainerObserver.observe(chatFooterRef.current) - - const resizeFooterObserver = new ResizeObserver((entries) => { - for (const entry of entries) { - const { inlineSize } = entry.borderBoxSize[0] - chatFooterRef.current!.style.width = `${inlineSize}px` - } - }) - resizeFooterObserver.observe(chatContainerRef.current) - - return () => { - resizeContainerObserver.disconnect() - resizeFooterObserver.disconnect() - } - } - }, [handleScrollToBottom]) - - useEffect(() => { - const setUserScrolled = () => { - const container = chatContainerRef.current - /* v8 ignore next 2 - @preserve */ - if (!container) - return - /* v8 ignore next 2 - @preserve */ - if (isAutoScrollingRef.current) - return - - const distanceToBottom = container.scrollHeight - container.clientHeight - container.scrollTop - const SCROLL_UP_THRESHOLD = 100 - - userScrolledRef.current = distanceToBottom > SCROLL_UP_THRESHOLD - } - - const container = chatContainerRef.current - /* v8 ignore next 2 - @preserve */ - if (!container) - return - - container.addEventListener('scroll', setUserScrolled) - return () => container.removeEventListener('scroll', setUserScrolled) - }, []) - - const prevFirstMessageIdRef = useRef(undefined) - useEffect(() => { - const firstMessageId = chatList[0]?.id - if (chatList.length <= 1 || (firstMessageId && prevFirstMessageIdRef.current !== firstMessageId)) - userScrolledRef.current = false - prevFirstMessageIdRef.current = firstMessageId - }, [chatList]) - - useEffect(() => { - if (!sidebarCollapseState) { - const timer = setTimeout(handleWindowResize, 200) - return () => clearTimeout(timer) - } - }, [handleWindowResize, sidebarCollapseState]) - const hasTryToAsk = config?.suggested_questions_after_answer?.enabled && !!suggestedQuestions?.length && onSend return ( @@ -279,7 +161,7 @@ const Chat: FC = ({
{chatNode}
= ({ !noStopResponding && isResponding && (
@@ -375,26 +257,16 @@ const Chat: FC = ({ }
- {showPromptLogModal && !hideLogModal && ( - { - setCurrentLogItem() - setShowPromptLogModal(false) - }} - /> - )} - {showAgentLogModal && !hideLogModal && ( - { - setCurrentLogItem() - setShowAgentLogModal(false) - }} - /> - )} +
) diff --git a/web/app/components/base/chat/chat/use-chat-layout.ts b/web/app/components/base/chat/chat/use-chat-layout.ts new file mode 100644 index 0000000000..41f622c523 --- /dev/null +++ b/web/app/components/base/chat/chat/use-chat-layout.ts @@ -0,0 +1,144 @@ +import type { ChatItem } from '../types' +import { debounce } from 'es-toolkit/compat' +import { + useCallback, + useEffect, + useRef, + useState, +} from 'react' + +type UseChatLayoutOptions = { + chatList: ChatItem[] + sidebarCollapseState?: boolean +} + +export const useChatLayout = ({ chatList, sidebarCollapseState }: UseChatLayoutOptions) => { + const [width, setWidth] = useState(0) + const chatContainerRef = useRef(null) + const chatContainerInnerRef = useRef(null) + const chatFooterRef = useRef(null) + const chatFooterInnerRef = useRef(null) + const userScrolledRef = useRef(false) + const isAutoScrollingRef = useRef(false) + const prevFirstMessageIdRef = useRef(undefined) + + const handleScrollToBottom = useCallback(() => { + if (chatList.length > 1 && chatContainerRef.current && !userScrolledRef.current) { + isAutoScrollingRef.current = true + chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight + + requestAnimationFrame(() => { + isAutoScrollingRef.current = false + }) + } + }, [chatList.length]) + + const handleWindowResize = useCallback(() => { + if (chatContainerRef.current) + setWidth(document.body.clientWidth - (chatContainerRef.current.clientWidth + 16) - 8) + + if (chatContainerRef.current && chatFooterRef.current) + chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px` + + if (chatContainerInnerRef.current && chatFooterInnerRef.current) + chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px` + }, []) + + useEffect(() => { + handleScrollToBottom() + const animationFrame = requestAnimationFrame(handleWindowResize) + + return () => { + cancelAnimationFrame(animationFrame) + } + }, [handleScrollToBottom, handleWindowResize]) + + useEffect(() => { + if (chatContainerRef.current) { + requestAnimationFrame(() => { + handleScrollToBottom() + handleWindowResize() + }) + } + }) + + useEffect(() => { + const debouncedHandler = debounce(handleWindowResize, 200) + window.addEventListener('resize', debouncedHandler) + + return () => { + window.removeEventListener('resize', debouncedHandler) + debouncedHandler.cancel() + } + }, [handleWindowResize]) + + useEffect(() => { + if (chatFooterRef.current && chatContainerRef.current) { + const resizeContainerObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { blockSize } = entry.borderBoxSize[0] + chatContainerRef.current!.style.paddingBottom = `${blockSize}px` + handleScrollToBottom() + } + }) + resizeContainerObserver.observe(chatFooterRef.current) + + const resizeFooterObserver = new ResizeObserver((entries) => { + for (const entry of entries) { + const { inlineSize } = entry.borderBoxSize[0] + chatFooterRef.current!.style.width = `${inlineSize}px` + } + }) + resizeFooterObserver.observe(chatContainerRef.current) + + return () => { + resizeContainerObserver.disconnect() + resizeFooterObserver.disconnect() + } + } + }, [handleScrollToBottom]) + + useEffect(() => { + const setUserScrolled = () => { + const container = chatContainerRef.current + if (!container) + return + if (isAutoScrollingRef.current) + return + + const distanceToBottom = container.scrollHeight - container.clientHeight - container.scrollTop + const scrollUpThreshold = 100 + + userScrolledRef.current = distanceToBottom > scrollUpThreshold + } + + const container = chatContainerRef.current + if (!container) + return + + container.addEventListener('scroll', setUserScrolled) + return () => container.removeEventListener('scroll', setUserScrolled) + }, []) + + useEffect(() => { + const firstMessageId = chatList[0]?.id + if (chatList.length <= 1 || (firstMessageId && prevFirstMessageIdRef.current !== firstMessageId)) + userScrolledRef.current = false + prevFirstMessageIdRef.current = firstMessageId + }, [chatList]) + + useEffect(() => { + if (!sidebarCollapseState) { + const timer = setTimeout(handleWindowResize, 200) + return () => clearTimeout(timer) + } + }, [handleWindowResize, sidebarCollapseState]) + + return { + width, + chatContainerRef, + chatContainerInnerRef, + chatFooterRef, + chatFooterInnerRef, + } +} diff --git a/web/app/components/base/notion-page-selector/page-selector/__tests__/page-row.spec.tsx b/web/app/components/base/notion-page-selector/page-selector/__tests__/page-row.spec.tsx new file mode 100644 index 0000000000..dba53d7642 --- /dev/null +++ b/web/app/components/base/notion-page-selector/page-selector/__tests__/page-row.spec.tsx @@ -0,0 +1,113 @@ +import type { ComponentProps } from 'react' +import type { NotionPageRow } from '../types' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import PageRow from '../page-row' + +const buildRow = (overrides: Partial = {}): NotionPageRow => ({ + page: { + page_id: 'page-1', + page_name: 'Page 1', + parent_id: 'root', + page_icon: null, + type: 'page', + is_bound: false, + }, + parentExists: false, + depth: 0, + expand: false, + hasChild: false, + ancestors: [], + ...overrides, +}) + +const renderPageRow = (overrides: Partial> = {}) => { + const props: ComponentProps = { + checked: false, + disabled: false, + isPreviewed: false, + onPreview: vi.fn(), + onSelect: vi.fn(), + onToggle: vi.fn(), + row: buildRow(), + searchValue: '', + selectionMode: 'multiple', + showPreview: true, + style: { height: 28 }, + ...overrides, + } + + return { + ...render(), + props, + } +} + +describe('PageRow', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should call onSelect with the page id when the checkbox is clicked', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + renderPageRow({ onSelect }) + + await user.click(screen.getByTestId('checkbox-notion-page-checkbox-page-1')) + + expect(onSelect).toHaveBeenCalledWith('page-1') + }) + + it('should call onToggle when the row has children and the toggle is clicked', async () => { + const user = userEvent.setup() + const onToggle = vi.fn() + + renderPageRow({ + onToggle, + row: buildRow({ + hasChild: true, + expand: true, + }), + }) + + await user.click(screen.getByTestId('notion-page-toggle-page-1')) + + expect(onToggle).toHaveBeenCalledWith('page-1') + }) + + it('should render breadcrumbs and hide the toggle while searching', () => { + renderPageRow({ + searchValue: 'Page', + row: buildRow({ + parentExists: true, + ancestors: ['Workspace', 'Section'], + }), + }) + + expect(screen.queryByTestId('notion-page-toggle-page-1')).not.toBeInTheDocument() + expect(screen.getByText('Workspace / Section / Page 1')).toBeInTheDocument() + }) + + it('should render preview state and call onPreview when the preview button is clicked', async () => { + const user = userEvent.setup() + const onPreview = vi.fn() + + renderPageRow({ + isPreviewed: true, + onPreview, + }) + + expect(screen.getByTestId('notion-page-row-page-1')).toHaveClass('bg-state-base-hover') + + await user.click(screen.getByTestId('notion-page-preview-page-1')) + + expect(onPreview).toHaveBeenCalledWith('page-1') + }) + + it('should hide the preview button when showPreview is false', () => { + renderPageRow({ showPreview: false }) + + expect(screen.queryByTestId('notion-page-preview-page-1')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/base/notion-page-selector/page-selector/__tests__/use-page-selector-model.spec.ts b/web/app/components/base/notion-page-selector/page-selector/__tests__/use-page-selector-model.spec.ts new file mode 100644 index 0000000000..d90c50308d --- /dev/null +++ b/web/app/components/base/notion-page-selector/page-selector/__tests__/use-page-selector-model.spec.ts @@ -0,0 +1,127 @@ +import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' +import { act, renderHook, waitFor } from '@testing-library/react' +import { usePageSelectorModel } from '../use-page-selector-model' + +const buildPage = (overrides: Partial): DataSourceNotionPage => ({ + page_id: 'page-id', + page_name: 'Page name', + parent_id: 'root', + page_icon: null, + type: 'page', + is_bound: false, + ...overrides, +}) + +const list: DataSourceNotionPage[] = [ + buildPage({ page_id: 'root-1', page_name: 'Root 1', parent_id: 'root' }), + buildPage({ page_id: 'child-1', page_name: 'Child 1', parent_id: 'root-1' }), + buildPage({ page_id: 'grandchild-1', page_name: 'Grandchild 1', parent_id: 'child-1' }), + buildPage({ page_id: 'child-2', page_name: 'Child 2', parent_id: 'root-1' }), +] + +const pagesMap: DataSourceNotionPageMap = { + 'root-1': { ...list[0], workspace_id: 'workspace-1' }, + 'child-1': { ...list[1], workspace_id: 'workspace-1' }, + 'grandchild-1': { ...list[2], workspace_id: 'workspace-1' }, + 'child-2': { ...list[3], workspace_id: 'workspace-1' }, +} + +const createProps = ( + overrides: Partial[0]> = {}, +): Parameters[0] => ({ + checkedIds: new Set(), + searchValue: '', + pagesMap, + list, + onSelect: vi.fn(), + previewPageId: undefined, + onPreview: vi.fn(), + selectionMode: 'multiple', + ...overrides, +}) + +describe('usePageSelectorModel', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should build visible rows from the expanded tree state', async () => { + const { result } = renderHook(() => usePageSelectorModel(createProps())) + + expect(result.current.rows.map(row => row.page.page_id)).toEqual(['root-1']) + + act(() => { + result.current.handleToggle('root-1') + }) + + await waitFor(() => { + expect(result.current.rows.map(row => row.page.page_id)).toEqual([ + 'root-1', + 'child-1', + 'child-2', + ]) + }) + + act(() => { + result.current.handleToggle('child-1') + }) + + await waitFor(() => { + expect(result.current.rows.map(row => row.page.page_id)).toEqual([ + 'root-1', + 'child-1', + 'grandchild-1', + 'child-2', + ]) + }) + }) + + it('should select descendants when selecting a parent in multiple mode', () => { + const onSelect = vi.fn() + const { result } = renderHook(() => usePageSelectorModel(createProps({ onSelect }))) + + act(() => { + result.current.handleSelect('root-1') + }) + + expect(onSelect).toHaveBeenCalledWith(new Set([ + 'root-1', + 'child-1', + 'grandchild-1', + 'child-2', + ])) + }) + + it('should update local preview and respect the controlled previewPageId when provided', () => { + const onPreview = vi.fn() + const { result, rerender } = renderHook( + props => usePageSelectorModel(props), + { initialProps: createProps({ onPreview }) }, + ) + + act(() => { + result.current.handlePreview('child-1') + }) + + expect(onPreview).toHaveBeenCalledWith('child-1') + expect(result.current.currentPreviewPageId).toBe('child-1') + + rerender(createProps({ onPreview, previewPageId: 'grandchild-1' })) + + expect(result.current.currentPreviewPageId).toBe('grandchild-1') + }) + + it('should expose filtered rows when the deferred search value changes', async () => { + const { result, rerender } = renderHook( + props => usePageSelectorModel(props), + { initialProps: createProps() }, + ) + + rerender(createProps({ searchValue: 'Grandchild' })) + + await waitFor(() => { + expect(result.current.effectiveSearchValue).toBe('Grandchild') + expect(result.current.rows.map(row => row.page.page_id)).toEqual(['grandchild-1']) + }) + }) +}) diff --git a/web/app/components/base/notion-page-selector/page-selector/__tests__/utils.spec.ts b/web/app/components/base/notion-page-selector/page-selector/__tests__/utils.spec.ts new file mode 100644 index 0000000000..2e6005c573 --- /dev/null +++ b/web/app/components/base/notion-page-selector/page-selector/__tests__/utils.spec.ts @@ -0,0 +1,118 @@ +import type { DataSourceNotionPage, DataSourceNotionPageMap } from '@/models/common' +import { + buildNotionPageTree, + getNextSelectedPageIds, + getRootPageIds, + getVisiblePageRows, +} from '../utils' + +const buildPage = (overrides: Partial): DataSourceNotionPage => ({ + page_id: 'page-id', + page_name: 'Page name', + parent_id: 'root', + page_icon: null, + type: 'page', + is_bound: false, + ...overrides, +}) + +const list: DataSourceNotionPage[] = [ + buildPage({ page_id: 'root-1', page_name: 'Root 1', parent_id: 'root' }), + buildPage({ page_id: 'child-1', page_name: 'Child 1', parent_id: 'root-1' }), + buildPage({ page_id: 'grandchild-1', page_name: 'Grandchild 1', parent_id: 'child-1' }), + buildPage({ page_id: 'child-2', page_name: 'Child 2', parent_id: 'root-1' }), + buildPage({ page_id: 'orphan-1', page_name: 'Orphan 1', parent_id: 'missing-parent' }), +] + +const pagesMap: DataSourceNotionPageMap = { + 'root-1': { ...list[0], workspace_id: 'workspace-1' }, + 'child-1': { ...list[1], workspace_id: 'workspace-1' }, + 'grandchild-1': { ...list[2], workspace_id: 'workspace-1' }, + 'child-2': { ...list[3], workspace_id: 'workspace-1' }, + 'orphan-1': { ...list[4], workspace_id: 'workspace-1' }, +} + +describe('page-selector utils', () => { + it('should build a tree with descendants, depth, and ancestors', () => { + const treeMap = buildNotionPageTree(list, pagesMap) + + expect(treeMap['root-1'].children).toEqual(new Set(['child-1', 'child-2'])) + expect(treeMap['root-1'].descendants).toEqual(new Set(['child-1', 'grandchild-1', 'child-2'])) + expect(treeMap['grandchild-1'].depth).toBe(2) + expect(treeMap['grandchild-1'].ancestors).toEqual(['Root 1', 'Child 1']) + }) + + it('should return root page ids for true roots and pages with missing parents', () => { + expect(getRootPageIds(list, pagesMap)).toEqual(['root-1', 'orphan-1']) + }) + + it('should return expanded tree rows in depth-first order when not searching', () => { + const treeMap = buildNotionPageTree(list, pagesMap) + + const rows = getVisiblePageRows({ + list, + pagesMap, + searchValue: '', + treeMap, + rootPageIds: ['root-1'], + expandedIds: new Set(['root-1', 'child-1']), + }) + + expect(rows.map(row => row.page.page_id)).toEqual([ + 'root-1', + 'child-1', + 'grandchild-1', + 'child-2', + ]) + }) + + it('should return filtered search rows with ancestry metadata when searching', () => { + const treeMap = buildNotionPageTree(list, pagesMap) + + const rows = getVisiblePageRows({ + list, + pagesMap, + searchValue: 'Grandchild', + treeMap, + rootPageIds: ['root-1'], + expandedIds: new Set(), + }) + + expect(rows).toEqual([ + expect.objectContaining({ + page: expect.objectContaining({ page_id: 'grandchild-1' }), + ancestors: ['Root 1', 'Child 1'], + hasChild: false, + parentExists: true, + }), + ]) + }) + + it('should toggle selected ids correctly in single and multiple mode', () => { + const treeMap = buildNotionPageTree(list, pagesMap) + + expect(getNextSelectedPageIds({ + checkedIds: new Set(['root-1']), + pageId: 'child-1', + searchValue: '', + selectionMode: 'single', + treeMap, + })).toEqual(new Set(['child-1'])) + + expect(getNextSelectedPageIds({ + checkedIds: new Set(), + pageId: 'root-1', + searchValue: '', + selectionMode: 'multiple', + treeMap, + })).toEqual(new Set(['root-1', 'child-1', 'grandchild-1', 'child-2'])) + + expect(getNextSelectedPageIds({ + checkedIds: new Set(['child-1']), + pageId: 'child-1', + searchValue: 'Child', + selectionMode: 'multiple', + treeMap, + })).toEqual(new Set()) + }) +}) diff --git a/web/app/components/base/notion-page-selector/page-selector/__tests__/virtual-page-list.spec.tsx b/web/app/components/base/notion-page-selector/page-selector/__tests__/virtual-page-list.spec.tsx new file mode 100644 index 0000000000..7ad4f29d3e --- /dev/null +++ b/web/app/components/base/notion-page-selector/page-selector/__tests__/virtual-page-list.spec.tsx @@ -0,0 +1,144 @@ +import type { ComponentProps } from 'react' +import type { NotionPageRow } from '../types' +import { render, screen } from '@testing-library/react' +import VirtualPageList from '../virtual-page-list' + +vi.mock('@tanstack/react-virtual') + +const pageRowPropsSpy = vi.fn() +type MockPageRowProps = ComponentProps + +vi.mock('../page-row', () => ({ + default: ({ + checked, + disabled, + isPreviewed, + onPreview, + onSelect, + onToggle, + row, + searchValue, + selectionMode, + showPreview, + style, + }: MockPageRowProps) => { + pageRowPropsSpy({ + checked, + disabled, + isPreviewed, + onPreview, + onSelect, + onToggle, + row, + searchValue, + selectionMode, + showPreview, + style, + }) + return
+ }, +})) + +const buildRow = (overrides: Partial = {}): NotionPageRow => ({ + page: { + page_id: 'page-1', + page_name: 'Page 1', + parent_id: 'root', + page_icon: null, + type: 'page', + is_bound: false, + }, + parentExists: false, + depth: 0, + expand: false, + hasChild: false, + ancestors: [], + ...overrides, +}) + +describe('VirtualPageList', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should render virtual rows and pass row state to PageRow', () => { + const rows = [ + buildRow(), + buildRow({ + page: { + page_id: 'page-2', + page_name: 'Page 2', + parent_id: 'root', + page_icon: null, + type: 'page', + is_bound: false, + }, + }), + ] + + render( + , + ) + + expect(screen.getByTestId('virtual-list')).toBeInTheDocument() + expect(screen.getByTestId('page-row-page-1')).toBeInTheDocument() + expect(screen.getByTestId('page-row-page-2')).toBeInTheDocument() + expect(pageRowPropsSpy).toHaveBeenNthCalledWith(1, expect.objectContaining({ + checked: true, + disabled: false, + isPreviewed: false, + searchValue: '', + selectionMode: 'multiple', + showPreview: true, + row: rows[0], + style: expect.objectContaining({ + height: '28px', + width: 'calc(100% - 16px)', + }), + })) + expect(pageRowPropsSpy).toHaveBeenNthCalledWith(2, expect.objectContaining({ + checked: false, + disabled: true, + isPreviewed: true, + row: rows[1], + })) + }) + + it('should size the virtual container using the row estimate', () => { + const rows = [buildRow(), buildRow()] + + render( + ()} + disabledValue={new Set()} + onPreview={vi.fn()} + onSelect={vi.fn()} + onToggle={vi.fn()} + previewPageId="" + rows={rows} + searchValue="" + selectionMode="multiple" + showPreview={false} + />, + ) + + const list = screen.getByTestId('virtual-list') + const innerContainer = list.firstElementChild as HTMLElement + + expect(innerContainer).toHaveStyle({ + height: '56px', + position: 'relative', + }) + }) +}) diff --git a/web/app/components/base/prompt-editor/__tests__/prompt-editor-content.spec.tsx b/web/app/components/base/prompt-editor/__tests__/prompt-editor-content.spec.tsx new file mode 100644 index 0000000000..02de482073 --- /dev/null +++ b/web/app/components/base/prompt-editor/__tests__/prompt-editor-content.spec.tsx @@ -0,0 +1,295 @@ +import type { EventEmitter } from 'ahooks/lib/useEventEmitter' +import type { LexicalEditor } from 'lexical' +import type { ComponentProps } from 'react' +import type { EventEmitterValue } from '@/context/event-emitter' +import { CodeNode } from '@lexical/code' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { + BLUR_COMMAND, + COMMAND_PRIORITY_EDITOR, + createCommand, + FOCUS_COMMAND, + TextNode, +} from 'lexical' +import { useEffect } from 'react' +import { GeneratorType } from '@/app/components/app/configuration/config/automatic/types' +import { useEventEmitterContextContext } from '@/context/event-emitter' +import { EventEmitterContextProvider } from '@/context/event-emitter-provider' +import { ContextBlockNode } from '../plugins/context-block' +import { CurrentBlockNode } from '../plugins/current-block' +import { CustomTextNode } from '../plugins/custom-text/node' +import { ErrorMessageBlockNode } from '../plugins/error-message-block' +import { HistoryBlockNode } from '../plugins/history-block' +import { HITLInputNode } from '../plugins/hitl-input-block' +import { LastRunBlockNode } from '../plugins/last-run-block' +import { QueryBlockNode } from '../plugins/query-block' +import { RequestURLBlockNode } from '../plugins/request-url-block' +import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '../plugins/update-block' +import { VariableValueBlockNode } from '../plugins/variable-value-block/node' +import { WorkflowVariableBlockNode } from '../plugins/workflow-variable-block' +import PromptEditorContent from '../prompt-editor-content' +import { textToEditorState } from '../utils' + +type Captures = { + editor: LexicalEditor | null + eventEmitter: EventEmitter | null +} + +const mockDOMRect = { + x: 100, + y: 100, + width: 100, + height: 20, + top: 100, + right: 200, + bottom: 120, + left: 100, + toJSON: () => ({}), +} + +const originalRangeGetClientRects = Range.prototype.getClientRects +const originalRangeGetBoundingClientRect = Range.prototype.getBoundingClientRect + +const setSelectionOnEditable = (editable: HTMLElement) => { + const lexicalTextNode = editable.querySelector('[data-lexical-text="true"]')?.firstChild + const range = document.createRange() + + if (lexicalTextNode) { + range.setStart(lexicalTextNode, 0) + range.setEnd(lexicalTextNode, 1) + } + else { + range.selectNodeContents(editable) + } + + const selection = window.getSelection() + selection?.removeAllRanges() + selection?.addRange(range) +} + +const CaptureEditorAndEmitter = ({ captures }: { captures: Captures }) => { + const { eventEmitter } = useEventEmitterContextContext() + const [editor] = useLexicalComposerContext() + + useEffect(() => { + captures.editor = editor + }, [captures, editor]) + + useEffect(() => { + captures.eventEmitter = eventEmitter + }, [captures, eventEmitter]) + + return null +} + +const PromptEditorContentHarness = ({ + captures, + initialText = '', + ...props +}: ComponentProps & { captures: Captures, initialText?: string }) => ( + + new CustomTextNode(node.__text), + withKlass: CustomTextNode, + }, + ContextBlockNode, + HistoryBlockNode, + QueryBlockNode, + RequestURLBlockNode, + WorkflowVariableBlockNode, + VariableValueBlockNode, + HITLInputNode, + CurrentBlockNode, + ErrorMessageBlockNode, + LastRunBlockNode, + ], + editorState: textToEditorState(initialText), + onError: (error: Error) => { + throw error + }, + }} + > + + + + +) + +describe('PromptEditorContent', () => { + beforeAll(() => { + Range.prototype.getClientRects = vi.fn(() => { + const rectList = [mockDOMRect] as unknown as DOMRectList + Object.defineProperty(rectList, 'length', { value: 1 }) + Object.defineProperty(rectList, 'item', { value: (index: number) => index === 0 ? mockDOMRect : null }) + return rectList + }) + Range.prototype.getBoundingClientRect = vi.fn(() => mockDOMRect as DOMRect) + }) + + beforeEach(() => { + vi.clearAllMocks() + }) + + afterAll(() => { + Range.prototype.getClientRects = originalRangeGetClientRects + Range.prototype.getBoundingClientRect = originalRangeGetBoundingClientRect + }) + + // The extracted content shell should run with the real Lexical stack and forward editor commands through its composed plugins. + describe('Rendering', () => { + it('should render with real dependencies and forward update/focus/blur events', async () => { + const captures: Captures = { editor: null, eventEmitter: null } + const onEditorChange = vi.fn() + const onFocus = vi.fn() + const onBlur = vi.fn() + const anchorElem = document.createElement('div') + + const { container } = render( + , + ) + + expect(screen.getByText('Type prompt')).toBeInTheDocument() + + const editable = container.querySelector('[contenteditable="true"]') as HTMLElement + expect(editable.className).toContain('text-[13px]') + + await waitFor(() => { + expect(captures.editor).not.toBeNull() + expect(captures.eventEmitter).not.toBeNull() + }) + + act(() => { + captures.eventEmitter?.emit({ + type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER, + instanceId: 'content-editor', + payload: 'updated prompt', + }) + }) + + await waitFor(() => { + expect(onEditorChange).toHaveBeenCalled() + }) + + act(() => { + captures.editor?.dispatchCommand(FOCUS_COMMAND, new FocusEvent('focus')) + captures.editor?.dispatchCommand(BLUR_COMMAND, new FocusEvent('blur', { relatedTarget: null })) + }) + + expect(onFocus).toHaveBeenCalledTimes(1) + expect(onBlur).toHaveBeenCalledTimes(1) + expect(screen.getByRole('textbox')).toBeInTheDocument() + }) + + it('should render optional blocks and open shortcut popups with the real editor runtime', async () => { + const captures: Captures = { editor: null, eventEmitter: null } + const onEditorChange = vi.fn() + const insertCommand = createCommand('prompt-editor-shortcut-insert') + const insertSpy = vi.fn() + const Popup = ({ onClose, onInsert }: { onClose: () => void, onInsert: (command: typeof insertCommand, params: string[]) => void }) => ( + <> + + + + ) + + const { container } = render( + , + ) + + await waitFor(() => { + expect(captures.editor).not.toBeNull() + }) + + const unregister = captures.editor?.registerCommand( + insertCommand, + (payload) => { + insertSpy(payload) + return true + }, + COMMAND_PRIORITY_EDITOR, + ) + + const editable = container.querySelector('[contenteditable="true"]') as HTMLElement + editable.focus() + setSelectionOnEditable(editable) + + fireEvent.keyDown(document, { key: '/', ctrlKey: true }) + + const insertButton = await screen.findByRole('button', { name: 'Insert shortcut' }) + fireEvent.click(insertButton) + + expect(insertSpy).toHaveBeenCalledWith(['from-shortcut']) + expect(onEditorChange).toHaveBeenCalled() + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'Insert shortcut' })).not.toBeInTheDocument() + }) + + unregister?.() + }) + + it('should keep the shell stable without optional anchor or placeholder overrides', async () => { + const captures: Captures = { editor: null, eventEmitter: null } + + render( + , + ) + + await waitFor(() => { + expect(captures.editor).not.toBeNull() + }) + + expect(screen.queryByTestId('draggable-target-line')).not.toBeInTheDocument() + expect(screen.getByText('common.promptEditor.placeholder')).toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/base/prompt-editor/index.tsx b/web/app/components/base/prompt-editor/index.tsx index 772d15e4cf..6f6da6901b 100644 --- a/web/app/components/base/prompt-editor/index.tsx +++ b/web/app/components/base/prompt-editor/index.tsx @@ -22,11 +22,6 @@ import type { } from './types' import { CodeNode } from '@lexical/code' import { LexicalComposer } from '@lexical/react/LexicalComposer' -import { ContentEditable } from '@lexical/react/LexicalContentEditable' -import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' -import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' -import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' -import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' import { $getRoot, TextNode, @@ -39,63 +34,37 @@ import { UPDATE_DATASETS_EVENT_EMITTER, UPDATE_HISTORY_EVENT_EMITTER, } from './constants' -import ComponentPickerBlock from './plugins/component-picker-block' import { - ContextBlock, ContextBlockNode, - ContextBlockReplacementBlock, } from './plugins/context-block' import { - CurrentBlock, CurrentBlockNode, - CurrentBlockReplacementBlock, } from './plugins/current-block' import { CustomTextNode } from './plugins/custom-text/node' -import DraggableBlockPlugin from './plugins/draggable-plugin' import { - ErrorMessageBlock, ErrorMessageBlockNode, - ErrorMessageBlockReplacementBlock, } from './plugins/error-message-block' import { - HistoryBlock, HistoryBlockNode, - HistoryBlockReplacementBlock, } from './plugins/history-block' import { - HITLInputBlock, - HITLInputBlockReplacementBlock, HITLInputNode, } from './plugins/hitl-input-block' import { - LastRunBlock, LastRunBlockNode, - LastRunReplacementBlock, } from './plugins/last-run-block' -import OnBlurBlock from './plugins/on-blur-or-focus-block' -// import TreeView from './plugins/tree-view' -import Placeholder from './plugins/placeholder' import { - QueryBlock, QueryBlockNode, - QueryBlockReplacementBlock, } from './plugins/query-block' import { - RequestURLBlock, RequestURLBlockNode, - RequestURLBlockReplacementBlock, } from './plugins/request-url-block' -import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin' -import UpdateBlock from './plugins/update-block' -import VariableBlock from './plugins/variable-block' -import VariableValueBlock from './plugins/variable-value-block' import { VariableValueBlockNode } from './plugins/variable-value-block/node' import { - WorkflowVariableBlock, WorkflowVariableBlockNode, - WorkflowVariableBlockReplacementBlock, } from './plugins/workflow-variable-block' +import PromptEditorContent from './prompt-editor-content' import { textToEditorState } from './utils' export type PromptEditorProps = { @@ -214,152 +183,31 @@ const PromptEditor: FC = ({ return (
- - )} - placeholder={( - - )} - ErrorBoundary={LexicalErrorBoundary} - /> - {shortcutPopups?.map(({ hotkey, Popup }, idx) => ( - - {(closePortal, onInsert) => } - - ))} - - - { - contextBlock?.show && ( - <> - - - - ) - } - { - queryBlock?.show && ( - <> - - - - ) - } - { - historyBlock?.show && ( - <> - - - - ) - } - { - (variableBlock?.show || externalToolBlock?.show) && ( - <> - - - - ) - } - { - workflowVariableBlock?.show && ( - <> - - - - ) - } - { - hitlInputBlock?.show && ( - <> - - - - ) - } - { - currentBlock?.show && ( - <> - - - - ) - } - { - requestURLBlock?.show && ( - <> - - - - ) - } - { - errorMessageBlock?.show && ( - <> - - - - ) - } - { - lastRunBlock?.show && ( - <> - - - - ) - } - { - isSupportFileVar && ( - - ) - } - - - - - {floatingAnchorElem && ( - - )} - {/* */}
) diff --git a/web/app/components/base/prompt-editor/prompt-editor-content.tsx b/web/app/components/base/prompt-editor/prompt-editor-content.tsx new file mode 100644 index 0000000000..07db69cfc8 --- /dev/null +++ b/web/app/components/base/prompt-editor/prompt-editor-content.tsx @@ -0,0 +1,257 @@ +import type { + EditorState, + LexicalCommand, +} from 'lexical' +import type { FC } from 'react' +import type { Hotkey } from './plugins/shortcuts-popup-plugin' +import type { + ContextBlockType, + CurrentBlockType, + ErrorMessageBlockType, + ExternalToolBlockType, + HistoryBlockType, + HITLInputBlockType, + LastRunBlockType, + QueryBlockType, + RequestURLBlockType, + VariableBlockType, + WorkflowVariableBlockType, +} from './types' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import * as React from 'react' +import { cn } from '@/utils/classnames' +import ComponentPickerBlock from './plugins/component-picker-block' +import { + ContextBlock, + ContextBlockReplacementBlock, +} from './plugins/context-block' +import { + CurrentBlock, + CurrentBlockReplacementBlock, +} from './plugins/current-block' +import DraggableBlockPlugin from './plugins/draggable-plugin' +import { + ErrorMessageBlock, + ErrorMessageBlockReplacementBlock, +} from './plugins/error-message-block' +import { + HistoryBlock, + HistoryBlockReplacementBlock, +} from './plugins/history-block' +import { + HITLInputBlock, + HITLInputBlockReplacementBlock, +} from './plugins/hitl-input-block' +import { + LastRunBlock, + LastRunReplacementBlock, +} from './plugins/last-run-block' +import OnBlurBlock from './plugins/on-blur-or-focus-block' +import Placeholder from './plugins/placeholder' +import { + QueryBlock, + QueryBlockReplacementBlock, +} from './plugins/query-block' +import { + RequestURLBlock, + RequestURLBlockReplacementBlock, +} from './plugins/request-url-block' +import ShortcutsPopupPlugin from './plugins/shortcuts-popup-plugin' +import UpdateBlock from './plugins/update-block' +import VariableBlock from './plugins/variable-block' +import VariableValueBlock from './plugins/variable-value-block' +import { + WorkflowVariableBlock, + WorkflowVariableBlockReplacementBlock, +} from './plugins/workflow-variable-block' + +type ShortcutPopup = { + hotkey: Hotkey + Popup: React.ComponentType<{ onClose: () => void, onInsert: (command: LexicalCommand, params: unknown[]) => void }> +} + +type PromptEditorContentProps = { + compact?: boolean + className?: string + placeholder?: string | React.ReactNode + placeholderClassName?: string + style?: React.CSSProperties + shortcutPopups: ShortcutPopup[] + contextBlock?: ContextBlockType + queryBlock?: QueryBlockType + requestURLBlock?: RequestURLBlockType + historyBlock?: HistoryBlockType + variableBlock?: VariableBlockType + externalToolBlock?: ExternalToolBlockType + workflowVariableBlock?: WorkflowVariableBlockType + hitlInputBlock?: HITLInputBlockType + currentBlock?: CurrentBlockType + errorMessageBlock?: ErrorMessageBlockType + lastRunBlock?: LastRunBlockType + isSupportFileVar?: boolean + onBlur?: () => void + onFocus?: () => void + instanceId?: string + floatingAnchorElem: HTMLDivElement | null + onEditorChange: (editorState: EditorState) => void +} + +const PromptEditorContent: FC = ({ + compact, + className, + placeholder, + placeholderClassName, + style, + shortcutPopups, + contextBlock, + queryBlock, + requestURLBlock, + historyBlock, + variableBlock, + externalToolBlock, + workflowVariableBlock, + hitlInputBlock, + currentBlock, + errorMessageBlock, + lastRunBlock, + isSupportFileVar, + onBlur, + onFocus, + instanceId, + floatingAnchorElem, + onEditorChange, +}) => { + return ( + <> + + )} + placeholder={( + + )} + ErrorBoundary={LexicalErrorBoundary} + /> + {shortcutPopups.map(({ hotkey, Popup }, idx) => ( + + {(closePortal, onInsert) => } + + ))} + + + {contextBlock?.show && ( + <> + + + + )} + {queryBlock?.show && ( + <> + + + + )} + {historyBlock?.show && ( + <> + + + + )} + {(variableBlock?.show || externalToolBlock?.show) && ( + <> + + + + )} + {workflowVariableBlock?.show && ( + <> + + + + )} + {hitlInputBlock?.show && ( + <> + + + + )} + {currentBlock?.show && ( + <> + + + + )} + {requestURLBlock?.show && ( + <> + + + + )} + {errorMessageBlock?.show && ( + <> + + + + )} + {lastRunBlock?.show && ( + <> + + + + )} + {isSupportFileVar && ( + + )} + + + + + {floatingAnchorElem && ( + + )} + + ) +} + +export default PromptEditorContent diff --git a/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/loading.spec.tsx b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/loading.spec.tsx new file mode 100644 index 0000000000..ada7aa7b7f --- /dev/null +++ b/web/app/components/datasets/documents/create-from-pipeline/preview/__tests__/loading.spec.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react' +import { render, screen } from '@testing-library/react' +import Loading from '../loading' + +vi.mock('@/app/components/base/skeleton', () => ({ + SkeletonContainer: ({ children, className }: { children?: ReactNode, className?: string }) => ( +
{children}
+ ), + SkeletonRectangle: ({ className }: { className?: string }) => ( +
+ ), +})) + +describe('CreateFromPipelinePreviewLoading', () => { + it('should render the preview loading shell and all skeleton blocks', () => { + const { container } = render() + + expect(container.firstElementChild).toHaveClass( + 'flex', + 'h-full', + 'w-full', + 'flex-col', + 'overflow-hidden', + 'px-6', + 'py-5', + ) + expect(screen.getAllByTestId('skeleton-container')).toHaveLength(6) + expect(screen.getAllByTestId('skeleton-rectangle')).toHaveLength(29) + }) +}) diff --git a/web/app/components/datasets/documents/detail/__tests__/context.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/context.spec.tsx new file mode 100644 index 0000000000..9524998290 --- /dev/null +++ b/web/app/components/datasets/documents/detail/__tests__/context.spec.tsx @@ -0,0 +1,30 @@ +import type { ReactNode } from 'react' +import { renderHook } from '@testing-library/react' +import { DocumentContext, useDocumentContext } from '../context' + +describe('DocumentContext', () => { + it('should return the default empty context value when no provider is present', () => { + const { result } = renderHook(() => useDocumentContext(value => value)) + + expect(result.current).toEqual({}) + }) + + it('should select values from the nearest provider', () => { + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => useDocumentContext(value => `${value.datasetId}:${value.documentId}`), + { wrapper }, + ) + + expect(result.current).toBe('dataset-1:document-1') + }) +}) diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/segment-list-context.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/segment-list-context.spec.tsx new file mode 100644 index 0000000000..f3fa0d0929 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/__tests__/segment-list-context.spec.tsx @@ -0,0 +1,55 @@ +import type { ReactNode } from 'react' +import { renderHook } from '@testing-library/react' +import { SegmentListContext, useSegmentListContext } from '../segment-list-context' + +describe('SegmentListContext', () => { + it('should expose the default collapsed state', () => { + const { result } = renderHook(() => useSegmentListContext(value => value)) + + expect(result.current).toEqual({ + isCollapsed: true, + fullScreen: false, + toggleFullScreen: expect.any(Function), + currSegment: { showModal: false }, + currChildChunk: { showModal: false }, + }) + }) + + it('should select provider values from the current segment list context', () => { + const toggleFullScreen = vi.fn() + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ) + + const { result } = renderHook( + () => useSegmentListContext(value => ({ + fullScreen: value.fullScreen, + segmentOpen: value.currSegment.showModal, + childOpen: value.currChildChunk.showModal, + })), + { wrapper }, + ) + + expect(result.current).toEqual({ + fullScreen: true, + segmentOpen: true, + childOpen: true, + }) + }) +}) diff --git a/web/app/components/develop/hooks/__tests__/use-doc-toc.spec.tsx b/web/app/components/develop/hooks/__tests__/use-doc-toc.spec.tsx new file mode 100644 index 0000000000..b153f24179 --- /dev/null +++ b/web/app/components/develop/hooks/__tests__/use-doc-toc.spec.tsx @@ -0,0 +1,125 @@ +import type { TocItem } from '../use-doc-toc' +import { act, renderHook } from '@testing-library/react' +import { useDocToc } from '../use-doc-toc' + +const mockMatchMedia = (matches: boolean) => { + vi.stubGlobal('matchMedia', vi.fn().mockImplementation((query: string) => ({ + matches, + media: query, + onchange: null, + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + addListener: vi.fn(), + removeListener: vi.fn(), + dispatchEvent: vi.fn(), + }))) +} + +const setupDocument = () => { + document.body.innerHTML = ` +
+ + ` + + const scrollContainer = document.querySelector('.overflow-auto') as HTMLDivElement + scrollContainer.scrollTo = vi.fn() + + const intro = document.getElementById('intro') as HTMLElement + const details = document.getElementById('details') as HTMLElement + + Object.defineProperty(intro, 'offsetTop', { configurable: true, value: 140 }) + Object.defineProperty(details, 'offsetTop', { configurable: true, value: 320 }) + + return { + scrollContainer, + intro, + details, + } +} + +describe('useDocToc', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + document.body.innerHTML = '' + mockMatchMedia(false) + }) + + afterEach(() => { + vi.useRealTimers() + vi.unstubAllGlobals() + }) + + it('should extract headings and expand the TOC on wide screens', async () => { + setupDocument() + mockMatchMedia(true) + + const { result } = renderHook(() => useDocToc({ + appDetail: { id: 'app-1' }, + locale: 'en', + })) + + act(() => { + vi.runAllTimers() + }) + + expect(result.current.toc).toEqual([ + { href: '#intro', text: 'Intro' }, + { href: '#details', text: 'Details' }, + ]) + expect(result.current.activeSection).toBe('intro') + expect(result.current.isTocExpanded).toBe(true) + }) + + it('should update the active section when the scroll container scrolls', async () => { + const { scrollContainer, intro, details } = setupDocument() + Object.defineProperty(window, 'innerHeight', { configurable: true, value: 800 }) + + intro.getBoundingClientRect = vi.fn(() => ({ top: 500 } as DOMRect)) + details.getBoundingClientRect = vi.fn(() => ({ top: 300 } as DOMRect)) + + const { result } = renderHook(() => useDocToc({ + appDetail: { id: 'app-1' }, + locale: 'en', + })) + + act(() => { + vi.runAllTimers() + }) + + act(() => { + scrollContainer.dispatchEvent(new Event('scroll')) + }) + + expect(result.current.activeSection).toBe('details') + }) + + it('should scroll the container to the clicked heading offset', async () => { + const { scrollContainer } = setupDocument() + const { result } = renderHook(() => useDocToc({ + appDetail: { id: 'app-1' }, + locale: 'en', + })) + + act(() => { + vi.runAllTimers() + }) + + const preventDefault = vi.fn() + act(() => { + result.current.handleTocClick( + { preventDefault } as unknown as React.MouseEvent, + { href: '#details', text: 'Details' }, + ) + }) + + expect(preventDefault).toHaveBeenCalledTimes(1) + expect(scrollContainer.scrollTo).toHaveBeenCalledWith({ + top: 240, + behavior: 'smooth', + }) + }) +}) 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 new file mode 100644 index 0000000000..1594662691 --- /dev/null +++ b/web/app/components/goto-anything/actions/__tests__/node-actions.spec.ts @@ -0,0 +1,69 @@ +import type { SearchResult } from '../types' +import { ragPipelineNodesAction } from '../rag-pipeline-nodes' +import { workflowNodesAction } from '../workflow-nodes' + +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 () => { + const results: SearchResult[] = [ + { id: 'workflow-node-1', title: 'LLM', type: 'workflow-node', data: {} as never }, + ] + workflowNodesAction.searchFn = vi.fn().mockReturnValue(results) + + await expect(workflowNodesAction.search('@node llm', 'llm', 'en')).resolves.toEqual(results) + expect(workflowNodesAction.searchFn).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') + }) + + await expect(workflowNodesAction.search('@node llm', 'llm', 'en')).resolves.toEqual([]) + expect(warnSpy).toHaveBeenCalledWith('Workflow nodes search failed:', expect.any(Error)) + + warnSpy.mockRestore() + }) +}) + +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 () => { + const results: SearchResult[] = [ + { id: 'rag-node-1', title: 'Retriever', type: 'workflow-node', data: {} as never }, + ] + ragPipelineNodesAction.searchFn = vi.fn().mockReturnValue(results) + + await expect(ragPipelineNodesAction.search('@node retrieve', 'retrieve', 'en')).resolves.toEqual(results) + expect(ragPipelineNodesAction.searchFn).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') + }) + + await expect(ragPipelineNodesAction.search('@node retrieve', 'retrieve', 'en')).resolves.toEqual([]) + expect(warnSpy).toHaveBeenCalledWith('RAG pipeline nodes search failed:', expect.any(Error)) + + warnSpy.mockRestore() + }) +}) 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 new file mode 100644 index 0000000000..46d1faba2e --- /dev/null +++ b/web/app/components/goto-anything/actions/commands/__tests__/slash.spec.tsx @@ -0,0 +1,124 @@ +import type { SearchResult } from '../../types' +import { render } from '@testing-library/react' +import { slashAction, SlashCommandProvider } from '../slash' + +const { + mockSetTheme, + mockSetLocale, + mockExecuteCommand, + mockRegister, + mockSearch, + mockUnregister, +} = vi.hoisted(() => ({ + mockSetTheme: vi.fn(), + mockSetLocale: vi.fn(), + mockExecuteCommand: vi.fn(), + mockRegister: vi.fn(), + mockSearch: vi.fn(), + mockUnregister: vi.fn(), +})) + +vi.mock('next-themes', () => ({ + useTheme: () => ({ + setTheme: mockSetTheme, + }), +})) + +vi.mock('react-i18next', () => ({ + getI18n: () => ({ + language: 'ja', + t: (key: string) => key, + }), +})) + +vi.mock('@/i18n-config', () => ({ + setLocaleOnClient: mockSetLocale, +})) + +vi.mock('../command-bus', () => ({ + executeCommand: (...args: unknown[]) => mockExecuteCommand(...args), +})) + +vi.mock('../registry', () => ({ + slashCommandRegistry: { + register: (...args: unknown[]) => mockRegister(...args), + search: (...args: unknown[]) => mockSearch(...args), + unregister: (...args: unknown[]) => mockUnregister(...args), + }, +})) + +describe('slashAction', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should expose translated title and description', () => { + expect(slashAction.title).toBe('gotoAnything.actions.slashTitle') + expect(slashAction.description).toBe('gotoAnything.actions.slashDesc') + }) + + it('should execute command results and ignore non-command results', () => { + slashAction.action?.({ + id: 'cmd-1', + title: 'Command', + type: 'command', + data: { + command: 'navigation.docs', + args: { path: '/docs' }, + }, + } as SearchResult) + + slashAction.action?.({ + id: 'app-1', + title: 'App', + type: 'app', + data: {} as never, + } as SearchResult) + + expect(mockExecuteCommand).toHaveBeenCalledTimes(1) + expect(mockExecuteCommand).toHaveBeenCalledWith('navigation.docs', { path: '/docs' }) + }) + + it('should delegate search to the slash command registry with the active language', async () => { + mockSearch.mockResolvedValue([{ id: 'theme', title: '/theme', type: 'command', data: { command: 'theme' } }]) + + const results = await slashAction.search('/theme dark', 'dark') + + expect(mockSearch).toHaveBeenCalledWith('/theme dark', 'ja') + expect(results).toEqual([{ id: 'theme', title: '/theme', type: 'command', data: { command: 'theme' } }]) + }) +}) + +describe('SlashCommandProvider', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('should register commands on mount and unregister them on unmount', () => { + const { unmount } = render() + + expect(mockRegister.mock.calls.map(call => call[0].name)).toEqual([ + 'theme', + 'language', + 'forum', + 'docs', + 'community', + 'account', + 'zen', + ]) + expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'theme' }), { setTheme: mockSetTheme }) + expect(mockRegister).toHaveBeenCalledWith(expect.objectContaining({ name: 'language' }), { setLocale: mockSetLocale }) + + unmount() + + expect(mockUnregister.mock.calls.map(call => call[0])).toEqual([ + 'theme', + 'language', + 'forum', + 'docs', + 'community', + 'account', + 'zen', + ]) + }) +}) diff --git a/web/app/components/header/account-dropdown/__tests__/menu-item-content.spec.tsx b/web/app/components/header/account-dropdown/__tests__/menu-item-content.spec.tsx new file mode 100644 index 0000000000..f7f53b6ab4 --- /dev/null +++ b/web/app/components/header/account-dropdown/__tests__/menu-item-content.spec.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react' +import { ExternalLinkIndicator, MenuItemContent } from '../menu-item-content' + +describe('MenuItemContent', () => { + it('should render the icon, label, and trailing content', () => { + const { container } = render( + Soon} + />, + ) + + expect(screen.getByText('Settings')).toBeInTheDocument() + expect(screen.getByTestId('menu-trailing')).toHaveTextContent('Soon') + expect(container.querySelector('.i-ri-settings-4-line')).toBeInTheDocument() + }) +}) + +describe('ExternalLinkIndicator', () => { + it('should render the external-link icon with aria-hidden semantics', () => { + const { container } = render() + + const indicator = container.querySelector('.i-ri-arrow-right-up-line') + expect(indicator).toBeInTheDocument() + expect(indicator).toHaveAttribute('aria-hidden') + }) +}) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/index.spec.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/index.spec.ts new file mode 100644 index 0000000000..7387234c67 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/__tests__/index.spec.ts @@ -0,0 +1,23 @@ +import * as ModelAuth from '../index' + +vi.mock('../add-credential-in-load-balancing', () => ({ default: 'AddCredentialInLoadBalancing' })) +vi.mock('../add-custom-model', () => ({ default: 'AddCustomModel' })) +vi.mock('../authorized', () => ({ default: 'Authorized' })) +vi.mock('../config-model', () => ({ default: 'ConfigModel' })) +vi.mock('../credential-selector', () => ({ default: 'CredentialSelector' })) +vi.mock('../manage-custom-model-credentials', () => ({ default: 'ManageCustomModelCredentials' })) +vi.mock('../switch-credential-in-load-balancing', () => ({ default: 'SwitchCredentialInLoadBalancing' })) + +describe('model-auth index exports', () => { + it('should re-export the model auth entry points', () => { + expect(ModelAuth).toMatchObject({ + AddCredentialInLoadBalancing: 'AddCredentialInLoadBalancing', + AddCustomModel: 'AddCustomModel', + Authorized: 'Authorized', + ConfigModel: 'ConfigModel', + CredentialSelector: 'CredentialSelector', + ManageCustomModelCredentials: 'ManageCustomModelCredentials', + SwitchCredentialInLoadBalancing: 'SwitchCredentialInLoadBalancing', + }) + }) +}) diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/credits-fallback-alert.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/credits-fallback-alert.spec.tsx new file mode 100644 index 0000000000..2d634d8673 --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-auth-dropdown/__tests__/credits-fallback-alert.spec.tsx @@ -0,0 +1,18 @@ +import { render, screen } from '@testing-library/react' +import CreditsFallbackAlert from '../credits-fallback-alert' + +describe('CreditsFallbackAlert', () => { + it('should render the credential fallback copy and description when credentials exist', () => { + render() + + expect(screen.getByText('common.modelProvider.card.apiKeyUnavailableFallback')).toBeInTheDocument() + expect(screen.getByText('common.modelProvider.card.apiKeyUnavailableFallbackDescription')).toBeInTheDocument() + }) + + it('should render the no-credentials fallback copy without the description', () => { + render() + + expect(screen.getByText('common.modelProvider.card.noApiKeysFallback')).toBeInTheDocument() + expect(screen.queryByText('common.modelProvider.card.apiKeyUnavailableFallbackDescription')).not.toBeInTheDocument() + }) +}) diff --git a/web/app/components/header/plugins-nav/__tests__/downloading-icon.spec.tsx b/web/app/components/header/plugins-nav/__tests__/downloading-icon.spec.tsx new file mode 100644 index 0000000000..2481a8c0b3 --- /dev/null +++ b/web/app/components/header/plugins-nav/__tests__/downloading-icon.spec.tsx @@ -0,0 +1,16 @@ +import { render } from '@testing-library/react' +import DownloadingIcon from '../downloading-icon' + +describe('DownloadingIcon', () => { + it('should render the animated install icon wrapper and svg markup', () => { + const { container } = render() + + const wrapper = container.firstElementChild as HTMLElement + const svg = container.querySelector('svg.install-icon') + + expect(wrapper).toHaveClass('inline-flex', 'text-components-button-secondary-text') + expect(svg).toBeInTheDocument() + expect(svg).toHaveAttribute('viewBox', '0 0 24 24') + expect(svg?.querySelectorAll('path')).toHaveLength(3) + }) +}) diff --git a/web/app/components/share/text-generation/__tests__/index.spec.tsx b/web/app/components/share/text-generation/__tests__/index.spec.tsx new file mode 100644 index 0000000000..e3746b1da1 --- /dev/null +++ b/web/app/components/share/text-generation/__tests__/index.spec.tsx @@ -0,0 +1,219 @@ +import type { TextGenerationRunControl } from '../types' +import { act, fireEvent, render, screen } from '@testing-library/react' +import { AccessMode } from '@/models/access-control' +import TextGeneration from '../index' + +const { + mockMode, + mockMedia, + mockAppStateRef, + mockBatchStateRef, + sidebarPropsSpy, + resultPanelPropsSpy, + mockSetIsCallBatchAPI, + mockResetBatchExecution, + mockHandleRunBatch, +} = vi.hoisted(() => ({ + mockMode: { value: 'create' }, + mockMedia: { value: 'pc' }, + mockAppStateRef: { value: null as unknown }, + mockBatchStateRef: { value: null as unknown }, + sidebarPropsSpy: vi.fn(), + resultPanelPropsSpy: vi.fn(), + mockSetIsCallBatchAPI: vi.fn(), + mockResetBatchExecution: vi.fn(), + mockHandleRunBatch: vi.fn(), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + MediaType: { + mobile: 'mobile', + pc: 'pc', + tablet: 'tablet', + }, + default: () => mockMedia.value, +})) + +vi.mock('@/next/navigation', () => ({ + useSearchParams: () => ({ + get: (key: string) => key === 'mode' ? mockMode.value : null, + }), +})) + +vi.mock('@/app/components/base/loading', () => ({ + default: ({ type }: { type: string }) =>
{type}
, +})) + +vi.mock('../hooks/use-text-generation-app-state', () => ({ + useTextGenerationAppState: () => mockAppStateRef.value, +})) + +vi.mock('../hooks/use-text-generation-batch', () => ({ + useTextGenerationBatch: () => mockBatchStateRef.value, +})) + +vi.mock('../text-generation-sidebar', () => ({ + default: (props: { + currentTab: string + onRunOnceSend: () => void + onBatchSend: (data: string[][]) => void + }) => { + sidebarPropsSpy(props) + return ( +
+ {props.currentTab} + + +
+ ) + }, +})) + +vi.mock('../text-generation-result-panel', () => ({ + default: (props: { + allTaskList: unknown[] + controlSend: number + controlStopResponding: number + isShowResultPanel: boolean + onRunControlChange: (value: TextGenerationRunControl | null) => void + onRunStart: () => void + }) => { + resultPanelPropsSpy(props) + return ( +
+ {props.isShowResultPanel ? 'shown' : 'hidden'} + {String(props.controlSend)} + {String(props.controlStopResponding)} + {String(props.allTaskList.length)} + + +
+ ) + }, +})) + +const createAppState = (overrides: Record = {}) => ({ + accessMode: AccessMode.PUBLIC, + appId: 'app-1', + appSourceType: 'webApp', + customConfig: { + remove_webapp_brand: false, + replace_webapp_logo: '', + }, + handleRemoveSavedMessage: vi.fn(), + handleSaveMessage: vi.fn(), + moreLikeThisConfig: { enabled: true }, + promptConfig: { + prompt_template: '', + prompt_variables: [{ key: 'name', name: 'Name', type: 'string', required: true }], + }, + savedMessages: [], + siteInfo: { + title: 'Generator', + description: 'Description', + }, + systemFeatures: {}, + textToSpeechConfig: { enabled: true }, + visionConfig: { enabled: false }, + ...overrides, +}) + +const createBatchState = (overrides: Record = {}) => ({ + allFailedTaskList: [], + allSuccessTaskList: [], + allTaskList: [], + allTasksRun: true, + controlRetry: 0, + exportRes: [], + handleCompleted: vi.fn(), + handleRetryAllFailedTask: vi.fn(), + handleRunBatch: (data: string[][], options: { onStart: () => void }) => { + mockHandleRunBatch(data, options) + options.onStart() + return true + }, + isCallBatchAPI: false, + noPendingTask: true, + resetBatchExecution: () => mockResetBatchExecution(), + setIsCallBatchAPI: (value: boolean) => mockSetIsCallBatchAPI(value), + showTaskList: [], + ...overrides, +}) + +describe('TextGeneration', () => { + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + mockMode.value = 'create' + mockMedia.value = 'pc' + mockAppStateRef.value = createAppState() + mockBatchStateRef.value = createBatchState() + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it('should render the loading state until app state is ready', () => { + mockAppStateRef.value = createAppState({ appId: '', siteInfo: null, promptConfig: null }) + + render() + + expect(screen.getByTestId('loading-app')).toHaveTextContent('app') + }) + + it('should fall back to create mode for unsupported query params and keep installed-app layout classes', () => { + mockMode.value = 'unsupported' + + const { container } = render() + + expect(screen.getByTestId('sidebar-current-tab')).toHaveTextContent('create') + expect(sidebarPropsSpy).toHaveBeenCalledWith(expect.objectContaining({ + currentTab: 'create', + isInstalledApp: true, + isPC: true, + })) + + const root = container.firstElementChild as HTMLElement + expect(root).toHaveClass('flex', 'h-full', 'rounded-2xl', 'shadow-md') + }) + + it('should orchestrate a run-once request and reveal the result panel', async () => { + render() + + fireEvent.click(screen.getByRole('button', { name: 'run-once' })) + + act(() => { + vi.runAllTimers() + }) + + expect(mockSetIsCallBatchAPI).toHaveBeenCalledWith(false) + expect(mockResetBatchExecution).toHaveBeenCalledTimes(1) + expect(screen.getByTestId('show-result')).toHaveTextContent('shown') + expect(Number(screen.getByTestId('control-send').textContent)).toBeGreaterThan(0) + }) + + it('should orchestrate batch runs through the batch hook and expose the result panel', async () => { + mockMode.value = 'batch' + + render() + + fireEvent.click(screen.getByRole('button', { name: 'run-batch' })) + + act(() => { + vi.runAllTimers() + }) + + expect(mockHandleRunBatch).toHaveBeenCalledWith( + [['name'], ['Alice']], + expect.objectContaining({ onStart: expect.any(Function) }), + ) + expect(screen.getByTestId('show-result')).toHaveTextContent('shown') + expect(Number(screen.getByTestId('control-stop').textContent)).toBeGreaterThan(0) + }) +}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts new file mode 100644 index 0000000000..76c3e6b2ce --- /dev/null +++ b/web/app/components/workflow-app/hooks/__tests__/use-is-chat-mode.spec.ts @@ -0,0 +1,41 @@ +import { renderHook } from '@testing-library/react' +import { AppModeEnum } from '@/types/app' +import { useIsChatMode } from '../use-is-chat-mode' + +const { mockStoreState } = vi.hoisted(() => ({ + mockStoreState: { + appDetail: undefined as { mode?: AppModeEnum } | undefined, + }, +})) + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof mockStoreState) => unknown) => selector(mockStoreState), +})) + +describe('useIsChatMode', () => { + beforeEach(() => { + vi.clearAllMocks() + mockStoreState.appDetail = undefined + }) + + it('should return true when the app mode is ADVANCED_CHAT', () => { + mockStoreState.appDetail = { mode: AppModeEnum.ADVANCED_CHAT } + + const { result } = renderHook(() => useIsChatMode()) + + expect(result.current).toBe(true) + }) + + it('should return false when the app mode is not chat or app detail is missing', () => { + mockStoreState.appDetail = { mode: AppModeEnum.WORKFLOW } + + const { result, rerender } = renderHook(() => useIsChatMode()) + + expect(result.current).toBe(false) + + mockStoreState.appDetail = undefined + rerender() + + expect(result.current).toBe(false) + }) +}) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 722eb8a7e4..846e9f11ec 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -1880,12 +1880,6 @@ } }, "app/components/base/chat/chat/index.tsx": { - "react/set-state-in-effect": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 2 - }, "ts/no-explicit-any": { "count": 3 } diff --git a/web/service/fetch.ts b/web/service/fetch.ts index 74ee7a9614..82870a8d2e 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -61,13 +61,15 @@ const createResponseFromHTTPError = (error: HTTPError): Response => { const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook => { return async ({ response }) => { if (!/^([23])\d{2}$/.test(String(response.status))) { - const errorData = await response.clone() - .json() - .then(data => data as ResponseError) - .catch(() => null) + let errorData: ResponseError | null = null + try { + const data: unknown = await response.clone().json() + errorData = data as ResponseError + } + catch {} const shouldNotifyError = response.status !== 401 && errorData && !otherOptions.silent - if (shouldNotifyError) + if (shouldNotifyError && errorData) toast.error(errorData.message) if (response.status === 403 && errorData?.code === 'already_setup') From 7d793e12c8ed40e4801acd28ca5030639605302c Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 9 Apr 2026 15:31:57 +0800 Subject: [PATCH 2/8] chore: update deps (#34833) --- pnpm-lock.yaml | 1249 ++++++++++++++++++++++--------------------- pnpm-workspace.yaml | 28 +- 2 files changed, 648 insertions(+), 629 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee3794d88d..b61ca1b0ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ catalogs: specifier: 1.27.6 version: 1.27.6 '@antfu/eslint-config': - specifier: 8.0.0 - version: 8.0.0 + specifier: 8.1.1 + version: 8.1.1 '@base-ui/react': specifier: 1.3.0 version: 1.3.0 @@ -88,11 +88,11 @@ catalogs: specifier: 4.7.0 version: 4.7.0 '@next/eslint-plugin-next': - specifier: 16.2.2 - version: 16.2.2 + specifier: 16.2.3 + version: 16.2.3 '@next/mdx': - specifier: 16.2.2 - version: 16.2.2 + specifier: 16.2.3 + version: 16.2.3 '@orpc/client': specifier: 1.13.13 version: 1.13.13 @@ -226,14 +226,14 @@ catalogs: specifier: 8.58.1 version: 8.58.1 '@typescript/native-preview': - specifier: 7.0.0-dev.20260407.1 - version: 7.0.0-dev.20260407.1 + specifier: 7.0.0-dev.20260408.1 + version: 7.0.0-dev.20260408.1 '@vitejs/plugin-react': specifier: 6.0.1 version: 6.0.1 '@vitejs/plugin-rsc': - specifier: 0.5.22 - version: 0.5.22 + specifier: 0.5.23 + version: 0.5.23 '@vitest/coverage-v8': specifier: 4.1.3 version: 4.1.3 @@ -301,8 +301,8 @@ catalogs: specifier: 10.2.0 version: 10.2.0 eslint-markdown: - specifier: 0.6.0 - version: 0.6.0 + specifier: 0.6.1 + version: 0.6.1 eslint-plugin-better-tailwindcss: specifier: 4.3.2 version: 4.3.2 @@ -343,8 +343,8 @@ catalogs: specifier: 1.11.13 version: 1.11.13 i18next: - specifier: 26.0.3 - version: 26.0.3 + specifier: 26.0.4 + version: 26.0.4 i18next-resources-to-backend: specifier: 1.2.1 version: 1.2.1 @@ -373,8 +373,8 @@ catalogs: specifier: 0.16.45 version: 0.16.45 knip: - specifier: 6.3.0 - version: 6.3.0 + specifier: 6.3.1 + version: 6.3.1 ky: specifier: 2.0.0 version: 2.0.0 @@ -397,8 +397,8 @@ catalogs: specifier: 1.0.0 version: 1.0.0 next: - specifier: 16.2.2 - version: 16.2.2 + specifier: 16.2.3 + version: 16.2.3 next-themes: specifier: 0.4.6 version: 0.4.6 @@ -415,17 +415,17 @@ catalogs: specifier: 4.2.0 version: 4.2.0 qs: - specifier: 6.15.0 - version: 6.15.0 + specifier: 6.15.1 + version: 6.15.1 react: - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.5 + version: 19.2.5 react-18-input-autosize: specifier: 3.0.0 version: 3.0.0 react-dom: - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.5 + version: 19.2.5 react-easy-crop: specifier: 5.5.7 version: 5.5.7 @@ -445,8 +445,8 @@ catalogs: specifier: 8.0.0-rc.0 version: 8.0.0-rc.0 react-server-dom-webpack: - specifier: 19.2.4 - version: 19.2.4 + specifier: 19.2.5 + version: 19.2.5 react-sortablejs: specifier: 6.1.4 version: 6.1.4 @@ -514,8 +514,8 @@ catalogs: specifier: 13.0.0 version: 13.0.0 vinext: - specifier: 0.0.40 - version: 0.0.40 + specifier: https://pkg.pr.new/vinext@adbf24d + version: 0.0.5 vite-plugin-inspect: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 @@ -648,22 +648,22 @@ importers: version: 1.27.6(@amplitude/rrweb@2.0.0-alpha.37)(rollup@4.59.0) '@base-ui/react': specifier: 'catalog:' - version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@emoji-mart/data': specifier: 'catalog:' version: 1.2.1 '@floating-ui/react': specifier: 'catalog:' - version: 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@formatjs/intl-localematcher': specifier: 'catalog:' version: 0.8.2 '@headlessui/react': specifier: 'catalog:' - version: 2.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@heroicons/react': specifier: 'catalog:' - version: 2.2.0(react@19.2.4) + version: 2.2.0(react@19.2.5) '@lexical/code': specifier: npm:lexical-code-no-prism@0.41.0 version: lexical-code-no-prism@0.41.0(@lexical/utils@0.42.0)(lexical@0.42.0) @@ -675,7 +675,7 @@ importers: version: 0.42.0 '@lexical/react': specifier: 'catalog:' - version: 0.42.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30) + version: 0.42.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(yjs@13.6.30) '@lexical/selection': specifier: 'catalog:' version: 0.42.0 @@ -687,7 +687,7 @@ importers: version: 0.42.0 '@monaco-editor/react': specifier: 'catalog:' - version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@orpc/client': specifier: 'catalog:' version: 1.13.13 @@ -702,13 +702,13 @@ importers: version: 1.13.13(@orpc/client@1.13.13)(@tanstack/query-core@5.96.2) '@remixicon/react': specifier: 'catalog:' - version: 4.9.0(react@19.2.4) + version: 4.9.0(react@19.2.5) '@sentry/react': specifier: 'catalog:' - version: 10.47.0(react@19.2.4) + version: 10.47.0(react@19.2.5) '@streamdown/math': specifier: 'catalog:' - version: 1.0.2(react@19.2.4) + version: 1.0.2(react@19.2.5) '@svgdotjs/svg.js': specifier: 'catalog:' version: 3.2.5 @@ -720,19 +720,19 @@ importers: version: 0.5.19(tailwindcss@4.2.2) '@tanstack/react-form': specifier: 'catalog:' - version: 1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.28.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@tanstack/react-query': specifier: 'catalog:' - version: 5.96.2(react@19.2.4) + version: 5.96.2(react@19.2.5) '@tanstack/react-virtual': specifier: 'catalog:' - version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) abcjs: specifier: 'catalog:' version: 6.6.2 ahooks: specifier: 'catalog:' - version: 3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.9.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) class-variance-authority: specifier: 'catalog:' version: 0.7.1 @@ -744,7 +744,7 @@ importers: version: 2.1.1 cmdk: specifier: 'catalog:' - version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) copy-to-clipboard: specifier: 'catalog:' version: 3.3.3 @@ -765,7 +765,7 @@ importers: version: 6.0.0 echarts-for-react: specifier: 'catalog:' - version: 3.0.6(echarts@6.0.0)(react@19.2.4) + version: 3.0.6(echarts@6.0.0)(react@19.2.5) elkjs: specifier: 'catalog:' version: 0.11.1 @@ -774,7 +774,7 @@ importers: version: 8.6.0(embla-carousel@8.6.0) embla-carousel-react: specifier: 'catalog:' - version: 8.6.0(react@19.2.4) + version: 8.6.0(react@19.2.5) emoji-mart: specifier: 'catalog:' version: 5.6.0 @@ -795,7 +795,7 @@ importers: version: 1.11.13 i18next: specifier: 'catalog:' - version: 26.0.3(typescript@6.0.2) + version: 26.0.4(typescript@6.0.2) i18next-resources-to-backend: specifier: 'catalog:' version: 1.2.1 @@ -804,7 +804,7 @@ importers: version: 11.1.4 jotai: specifier: 'catalog:' - version: 2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4) + version: 2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5) js-audio-recorder: specifier: 'catalog:' version: 1.0.7 @@ -843,58 +843,58 @@ importers: version: 1.0.0 next: specifier: 'catalog:' - version: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + version: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) next-themes: specifier: 'catalog:' - version: 0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) nuqs: specifier: 'catalog:' - version: 2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4) + version: 2.8.9(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react@19.2.5) pinyin-pro: specifier: 'catalog:' version: 3.28.0 qrcode.react: specifier: 'catalog:' - version: 4.2.0(react@19.2.4) + version: 4.2.0(react@19.2.5) qs: specifier: 'catalog:' - version: 6.15.0 + version: 6.15.1 react: specifier: 'catalog:' - version: 19.2.4 + version: 19.2.5 react-18-input-autosize: specifier: 'catalog:' - version: 3.0.0(react@19.2.4) + version: 3.0.0(react@19.2.5) react-dom: specifier: 'catalog:' - version: 19.2.4(react@19.2.4) + version: 19.2.5(react@19.2.5) react-easy-crop: specifier: 'catalog:' - version: 5.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 5.5.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-hotkeys-hook: specifier: 'catalog:' - version: 5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 5.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-i18next: specifier: 'catalog:' - version: 17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2) + version: 17.0.2(i18next@26.0.4(typescript@6.0.2))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) react-multi-email: specifier: 'catalog:' - version: 1.0.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 1.0.25(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-papaparse: specifier: 'catalog:' version: 4.4.0 react-pdf-highlighter: specifier: 'catalog:' - version: 8.0.0-rc.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 8.0.0-rc.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) react-sortablejs: specifier: 'catalog:' - version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7) + version: 6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sortablejs@1.15.7) react-textarea-autosize: specifier: 'catalog:' - version: 8.5.9(@types/react@19.2.14)(react@19.2.4) + version: 8.5.9(@types/react@19.2.14)(react@19.2.5) reactflow: specifier: 'catalog:' - version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) remark-breaks: specifier: 'catalog:' version: 4.0.0 @@ -918,7 +918,7 @@ importers: version: 1.0.8 streamdown: specifier: 'catalog:' - version: 2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 2.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) string-ts: specifier: 'catalog:' version: 2.3.1 @@ -933,7 +933,7 @@ importers: version: 5.1.0 use-context-selector: specifier: 'catalog:' - version: 2.0.0(react@19.2.4)(scheduler@0.27.0) + version: 2.0.0(react@19.2.5)(scheduler@0.27.0) uuid: specifier: 'catalog:' version: 13.0.0 @@ -942,17 +942,17 @@ importers: version: 4.3.6 zundo: specifier: 'catalog:' - version: 2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))) + version: 2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))) zustand: specifier: 'catalog:' - version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) devDependencies: '@antfu/eslint-config': specifier: 'catalog:' - version: 8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2) + version: 8.1.1(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2) '@chromatic-com/storybook': specifier: 'catalog:' - version: 5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@dify/iconify-collections': specifier: workspace:* version: link:../packages/iconify-collections @@ -976,37 +976,37 @@ importers: version: 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) '@mdx-js/react': specifier: 'catalog:' - version: 3.1.1(@types/react@19.2.14)(react@19.2.4) + version: 3.1.1(@types/react@19.2.14)(react@19.2.5) '@mdx-js/rollup': specifier: 'catalog:' version: 3.1.1(rollup@4.59.0) '@next/eslint-plugin-next': specifier: 'catalog:' - version: 16.2.2 + version: 16.2.3 '@next/mdx': specifier: 'catalog:' - version: 16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)) + version: 16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)) '@rgrove/parse-xml': specifier: 'catalog:' version: 4.2.0 '@storybook/addon-docs': specifier: 'catalog:' - version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) + version: 10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) '@storybook/addon-links': specifier: 'catalog:' - version: 10.3.5(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/addon-onboarding': specifier: 'catalog:' - version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/addon-themes': specifier: 'catalog:' - version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) + version: 10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) '@storybook/nextjs-vite': specifier: 'catalog:' - version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) + version: 10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) '@storybook/react': specifier: 'catalog:' - version: 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + version: 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) '@tailwindcss/postcss': specifier: 'catalog:' version: 4.2.2 @@ -1018,13 +1018,13 @@ importers: version: 5.96.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@tanstack/react-devtools': specifier: 'catalog:' - version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11) + version: 0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(solid-js@1.9.11) '@tanstack/react-form-devtools': specifier: 'catalog:' - version: 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) + version: 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11) '@tanstack/react-query-devtools': specifier: 'catalog:' - version: 5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4) + version: 5.96.2(@tanstack/react-query@5.96.2(react@19.2.5))(react@19.2.5) '@testing-library/dom': specifier: 'catalog:' version: 10.4.1 @@ -1033,7 +1033,7 @@ importers: version: 6.9.1 '@testing-library/react': specifier: 'catalog:' - version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@testing-library/user-event': specifier: 'catalog:' version: 14.6.1(@testing-library/dom@10.4.1) @@ -1075,19 +1075,19 @@ importers: version: 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript/native-preview': specifier: 'catalog:' - version: 7.0.0-dev.20260407.1 + version: 7.0.0-dev.20260408.1 '@vitejs/plugin-react': specifier: 'catalog:' version: 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) '@vitejs/plugin-rsc': specifier: 'catalog:' - version: 0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4) + version: 0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5) '@vitest/coverage-v8': specifier: 'catalog:' version: 4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) agentation: specifier: 'catalog:' - version: 3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 3.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) code-inspector-plugin: specifier: 'catalog:' version: 1.5.1 @@ -1096,7 +1096,7 @@ importers: version: 10.2.0(jiti@2.6.1) eslint-markdown: specifier: 'catalog:' - version: 0.6.0(eslint@10.2.0(jiti@2.6.1)) + version: 0.6.1(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-better-tailwindcss: specifier: 'catalog:' version: 4.3.2(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(tailwindcss@4.2.2)(typescript@6.0.2) @@ -1117,7 +1117,7 @@ importers: version: 4.0.2(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-storybook: specifier: 'catalog:' - version: 10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + version: 10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) happy-dom: specifier: 'catalog:' version: 20.8.9 @@ -1126,16 +1126,16 @@ importers: version: 4.12.12 knip: specifier: 'catalog:' - version: 6.3.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + version: 6.3.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) postcss: specifier: 'catalog:' version: 8.5.9 react-server-dom-webpack: specifier: 'catalog:' - version: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) + version: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) storybook: specifier: 'catalog:' - version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + version: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tailwindcss: specifier: 'catalog:' version: 4.2.2 @@ -1150,7 +1150,7 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: 0.0.40(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)(typescript@6.0.2) + version: https://pkg.pr.new/vinext@adbf24d(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' @@ -1253,8 +1253,8 @@ packages: '@amplitude/targeting@0.2.0': resolution: {integrity: sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA==} - '@antfu/eslint-config@8.0.0': - resolution: {integrity: sha512-IKiCfsa1vRgj8srB2azqiN3nOAcVyP/TZ5Ibiz0TDW9NoQPizTvkmRTSi1vo4ax0SL9TH/8uJLK6uCfd6bQzLA==} + '@antfu/eslint-config@8.1.1': + resolution: {integrity: sha512-y5/eAKlJUbQpeES2Pnb0i/VgbmqQ+srHJJNqbTKEBsxdLy3h1BqdS00zDpE+YeP71EWmlYJSTUhcJg4n4yMeAQ==} hasBin: true peerDependencies: '@angular-eslint/eslint-plugin': ^21.1.0 @@ -1564,6 +1564,10 @@ packages: resolution: {integrity: sha512-0xew1CxOam0gV5OMjh2KjFQZsKL2bByX1+q4j3E73MpYIdyUxcZb/xQct9ccUb+ve5KGUYbCUxyPnYB7RbuP+w==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@es-joy/jsdoccomment@0.86.0': + resolution: {integrity: sha512-ukZmRQ81WiTpDWO6D/cTBM7XbrNtutHKvAVnZN/8pldAwLoJArGOvkNyxPTBGsPjsoaQBJxlH+tE2TNA/92Qgw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + '@es-joy/resolve.exports@1.2.0': resolution: {integrity: sha512-Q9hjxWI5xBM+qW2enxfe8wDKdFWMfd0Z29k5ZJnuBqD/CasY5Zryj09aCA6owbGATWz+39p5uIdaHXpopOcG8g==} engines: {node: '>=10'} @@ -2260,14 +2264,14 @@ packages: '@next/env@16.0.0': resolution: {integrity: sha512-s5j2iFGp38QsG1LWRQaE2iUY3h1jc014/melHFfLdrsMJPqxqDQwWNwyQTcNoUSGZlCVZuM7t7JDMmSyRilsnA==} - '@next/env@16.2.2': - resolution: {integrity: sha512-LqSGz5+xGk9EL/iBDr2yo/CgNQV6cFsNhRR2xhSXYh7B/hb4nePCxlmDvGEKG30NMHDFf0raqSyOZiQrO7BkHQ==} + '@next/env@16.2.3': + resolution: {integrity: sha512-ZWXyj4uNu4GCWQw9cjRxWlbD+33mcDszIo9iQxFnBX3Wmgq9ulaSJcl6VhuWx5pCWqqD+9W6Wfz7N0lM5lYPMA==} - '@next/eslint-plugin-next@16.2.2': - resolution: {integrity: sha512-IOPbWzDQ+76AtjZioaCjpIY72xNSDMnarZ2GMQ4wjNLvnJEJHqxQwGFhgnIWLV9klb4g/+amg88Tk5OXVpyLTw==} + '@next/eslint-plugin-next@16.2.3': + resolution: {integrity: sha512-nE/b9mht28XJxjTwKs/yk7w4XTaU3t40UHVAky6cjiijdP/SEy3hGsnQMPxmXPTpC7W4/97okm6fngKnvCqVaA==} - '@next/mdx@16.2.2': - resolution: {integrity: sha512-2CbRTXE6sJ7zDAaKXknb5FrrPs46iJeMPzuoBXsAOV/XVnxABGD4mSDusn0VuCoII/KjUZ+zsuo2VFbchYQXng==} + '@next/mdx@16.2.3': + resolution: {integrity: sha512-mm7XNfPagSIcN8jFtozB9toeh5ESES0KCLRoo0gu6xydijvnIrV7dRIK3akNL3Tecc8AHX1FNzYZOZTeFU6RCw==} peerDependencies: '@mdx-js/loader': '>=0.15.0' '@mdx-js/react': '>=0.15.0' @@ -2277,54 +2281,54 @@ packages: '@mdx-js/react': optional: true - '@next/swc-darwin-arm64@16.2.2': - resolution: {integrity: sha512-B92G3ulrwmkDSEJEp9+XzGLex5wC1knrmCSIylyVeiAtCIfvEJYiN3v5kXPlYt5R4RFlsfO/v++aKV63Acrugg==} + '@next/swc-darwin-arm64@16.2.3': + resolution: {integrity: sha512-u37KDKTKQ+OQLvY+z7SNXixwo4Q2/IAJFDzU1fYe66IbCE51aDSAzkNDkWmLN0yjTUh4BKBd+hb69jYn6qqqSg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.2.2': - resolution: {integrity: sha512-7ZwSgNKJNQiwW0CKhNm9B1WS2L1Olc4B2XY0hPYCAL3epFnugMhuw5TMWzMilQ3QCZcCHoYm9NGWTHbr5REFxw==} + '@next/swc-darwin-x64@16.2.3': + resolution: {integrity: sha512-gHjL/qy6Q6CG3176FWbAKyKh9IfntKZTB3RY/YOJdDFpHGsUDXVH38U4mMNpHVGXmeYW4wj22dMp1lTfmu/bTQ==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.2.2': - resolution: {integrity: sha512-c3m8kBHMziMgo2fICOP/cd/5YlrxDU5YYjAJeQLyFsCqVF8xjOTH/QYG4a2u48CvvZZSj1eHQfBCbyh7kBr30Q==} + '@next/swc-linux-arm64-gnu@16.2.3': + resolution: {integrity: sha512-U6vtblPtU/P14Y/b/n9ZY0GOxbbIhTFuaFR7F4/uMBidCi2nSdaOFhA0Go81L61Zd6527+yvuX44T4ksnf8T+Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.2.2': - resolution: {integrity: sha512-VKLuscm0P/mIfzt+SDdn2+8TNNJ7f0qfEkA+az7OqQbjzKdBxAHs0UvuiVoCtbwX+dqMEL9U54b5wQ/aN3dHeg==} + '@next/swc-linux-arm64-musl@16.2.3': + resolution: {integrity: sha512-/YV0LgjHUmfhQpn9bVoGc4x4nan64pkhWR5wyEV8yCOfwwrH630KpvRg86olQHTwHIn1z59uh6JwKvHq1h4QEw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.2.2': - resolution: {integrity: sha512-kU3OPHJq6sBUjOk7wc5zJ7/lipn8yGldMoAv4z67j6ov6Xo/JvzA7L7LCsyzzsXmgLEhk3Qkpwqaq/1+XpNR3g==} + '@next/swc-linux-x64-gnu@16.2.3': + resolution: {integrity: sha512-/HiWEcp+WMZ7VajuiMEFGZ6cg0+aYZPqCJD3YJEfpVWQsKYSjXQG06vJP6F1rdA03COD9Fef4aODs3YxKx+RDQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.2.2': - resolution: {integrity: sha512-CKXRILyErMtUftp+coGcZ38ZwE/Aqq45VMCcRLr2I4OXKrgxIBDXHnBgeX/UMil0S09i2JXaDL3Q+TN8D/cKmg==} + '@next/swc-linux-x64-musl@16.2.3': + resolution: {integrity: sha512-Kt44hGJfZSefebhk/7nIdivoDr3Ugp5+oNz9VvF3GUtfxutucUIHfIO0ZYO8QlOPDQloUVQn4NVC/9JvHRk9hw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.2.2': - resolution: {integrity: sha512-sS/jSk5VUoShUqINJFvNjVT7JfR5ORYj/+/ZpOYbbIohv/lQfduWnGAycq2wlknbOql2xOR0DoV0s6Xfcy49+g==} + '@next/swc-win32-arm64-msvc@16.2.3': + resolution: {integrity: sha512-O2NZ9ie3Tq6xj5Z5CSwBT3+aWAMW2PIZ4egUi9MaWLkwaehgtB7YZjPm+UpcNpKOme0IQuqDcor7BsW6QBiQBw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.2.2': - resolution: {integrity: sha512-aHaKceJgdySReT7qeck5oShucxWRiiEuwCGK8HHALe6yZga8uyFpLkPgaRw3kkF04U7ROogL/suYCNt/+CuXGA==} + '@next/swc-win32-x64-msvc@16.2.3': + resolution: {integrity: sha512-Ibm29/GgB/ab5n7XKqlStkm54qqZE8v2FnijUPBgrd67FWrac45o/RsNlaOWjme/B5UqeWt/8KM4aWBwA1D2Kw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -4327,43 +4331,43 @@ packages: resolution: {integrity: sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-akoBfxvDbULMWLqHPDBI5sRkhjQ0blX5+iG7GBoSstqJZW4P0nzd516COGs7xWHsu3apBhaBgSTMCFO78kG80w==} + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-YcPczNLfPDB13eUBYHkTOkL7HyWqqqEhho4eSxhAvigZuxvtHQ1uyILIvLVAwipEVzhJ8QciKmLdLucpfi4XyA==} cpu: [arm64] os: [darwin] - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-j/V5BS+tgcRFGQC+y95vZB78fI45UgobAEY1+NlFZ3Yih9ICKWRfJPcalpiP5vjiO2NgqVzcFfO9XbpJyq5TTA==} + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-cHqkDg53xxxz21MThLBf4vx1kyIpRPEYNdEiQlvu9O35Tth49+aub6F+/YEMd9MG4TYZmxh1bEjkjErTUIElpA==} cpu: [x64] os: [darwin] - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-QG0E0lmcZQZimvNltxyi5Q3Oz1pd0BdztS7K5T9HTs30E3TSeYHq7Csw3SbDfAVwcqs2HTe/AVqLy6ar+1zm3Q==} + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-iHG0FEXq/QFsn+qlTPllxdcbvfQ9aRYggy4lc1z0+f11Nyk4YDNCSiR8WW7pbnOTx/VreGbbXhlpuJXTidqL8g==} cpu: [arm64] os: [linux] - '@typescript/native-preview-linux-arm@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-ZDr+zQFSTPmLIGyXDWixYFeFtktWUDGAD6s65rTI5EJgyt4X5/kEMnNd04mf4PbN0ChSiTRzJYLzaM+JGo+jww==} + '@typescript/native-preview-linux-arm@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-w26Gv9yq9LIYIhxjkQC+i0wBPDdQdX+H06ZhyVRL5grKWTIsk9Xwjp9mDRB/dGlXBKcvnM25JH16OyAA0rFH3A==} cpu: [arm] os: [linux] - '@typescript/native-preview-linux-x64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-a82yGx039yqZBS0dwKG8+kgeF2xVA7Pg6lL2SrswbaxWz3bXpI0ASX3HgUw+JMSIr4fbZ5ulKcaorPqbhc48/A==} + '@typescript/native-preview-linux-x64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-hMcUlUIzYbvbdq6j/B4RPL+kZR917NGnE9AgPZ7dJ92yamH/7LGT1Mnlc6McUx31yqTFBFHdTc7Cfx+ynua7Iw==} cpu: [x64] os: [linux] - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-e38ow5yqBrdiz4GunQCRk1E7cTtowpbXeAvVJf1wXrWbFqEc0D8BE7YPmTy9W2fOI0KFHUrsFg5h4Ad/TKVjug==} + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-avJWIEKSx4rdBLZD1FOOTuxTU51dQfYb3jZvZMaXD4thJjq+6eSwfzu2elwL36AZDlnaxggGjB5nBxp0t54iOA==} cpu: [arm64] os: [win32] - '@typescript/native-preview-win32-x64@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-1Jiij5NQOvlM72/DdfXzAVia1pdffgHiVgWZVmDwXECpzwQB0WwWfhI/0IddXP92Y9gVQFCGo9lypSAnamfGPA==} + '@typescript/native-preview-win32-x64@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-gpvEHkF/WoxkA3711c4uWNCZO9WAuwrq49COdNwxgOTzYHnMc1yCj8CpkCUJwU0f/Ydwp2s6/efn6gTMvtckPg==} cpu: [x64] os: [win32] - '@typescript/native-preview@7.0.0-dev.20260407.1': - resolution: {integrity: sha512-gf1W3UbzVTDkZJuwhNtOcfQ6l3hpDcxuWh90ANlp/cKupmAqaXNGpT23YjTYqXsaI7RDQR7JUELCKeWbW9PJIg==} + '@typescript/native-preview@7.0.0-dev.20260408.1': + resolution: {integrity: sha512-N0MZLEUnAoP/aRVk7MY119LDsESkbtEwIw+YeXi/jjx2XCqf7ni3GxIVsUYtf/troyuSedq3V/OUrkoCh5A9gA==} hasBin: true '@ungap/structured-clone@1.3.0': @@ -4420,8 +4424,8 @@ packages: babel-plugin-react-compiler: optional: true - '@vitejs/plugin-rsc@0.5.22': - resolution: {integrity: sha512-OC4wKNVHpF+LOgtasdMOAw1V0yWHj1Nx/XfkNW/9weFXd/9wXPWDyeJGcUJ03DxqJ8mYi4j9/kvo6HKYCoP9Ow==} + '@vitejs/plugin-rsc@0.5.23': + resolution: {integrity: sha512-CV6kWPE4E241qDStwK3ErYjuZqW1i1xun3/P1wsm94RJoActLTrQsGzGsf75ioeVxEK0roPqLGhcV2WlSlPePQ==} peerDependencies: react: '*' react-dom: '*' @@ -5561,8 +5565,8 @@ packages: '@eslint/json': optional: true - eslint-markdown@0.6.0: - resolution: {integrity: sha512-NrgfiNto5IJrW1F/Akf2hJYoJTCbXoClOUvtUMDgoqmQNH0VRihNvFh+MFay4E0HV2eozfgxsLSGxnndtRJA8w==} + eslint-markdown@0.6.1: + resolution: {integrity: sha512-eiHSRFnzcPWN/0YDrtELW/+GnGylAoyXVBDh0iVAttyC5rWAaZfgSrzlFUTlS7Jz4XEL36PFLsoEcXlbvl5qPQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} peerDependencies: eslint: ^9.31.0 || ^10.0.0-rc.0 @@ -5623,8 +5627,8 @@ packages: peerDependencies: eslint: ^9.0.0 || ^10.0.0 - eslint-plugin-jsdoc@62.8.1: - resolution: {integrity: sha512-e9358PdHgvcMF98foNd3L7hVCw70Lt+YcSL7JzlJebB8eT5oRJtW6bHMQKoAwJtw6q0q0w/fRIr2kwnHdFDI6A==} + eslint-plugin-jsdoc@62.9.0: + resolution: {integrity: sha512-PY7/X4jrVgoIDncUmITlUqK546Ltmx/Pd4Hdsu4CvSjryQZJI2mEV4vrdMufyTetMiZ5taNSqvK//BTgVUlNkA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 @@ -5655,8 +5659,8 @@ packages: resolution: {integrity: sha512-brcKcxGnISN2CcVhXJ/kEQlNa0MEfGRtwKtWA16SkqXHKitaKIMrfemJKLKX1YqDU5C/5JY3PvZXd5jEW04e0Q==} engines: {node: '>=5.0.0'} - eslint-plugin-perfectionist@5.7.0: - resolution: {integrity: sha512-WRHj7OZS/INutQ/gKN5C1ZGnMhkQ3oKZQAA2I7rl5yM8keBtSd9oj/qlJaHuwh5873FhMPqYlttcadF0YsTN7g==} + eslint-plugin-perfectionist@5.8.0: + resolution: {integrity: sha512-k8uIptWIxkUclonCFGyDzgYs9NI+Qh0a7cUXS3L7IYZDEsjXuimFBVbxXPQQngWqMiaxJRwbtYB4smMGMqF+cw==} engines: {node: ^20.0.0 || >=22.0.0} peerDependencies: eslint: ^8.45.0 || ^9.0.0 || ^10.0.0 @@ -6162,8 +6166,8 @@ packages: i18next-resources-to-backend@1.2.1: resolution: {integrity: sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==} - i18next@26.0.3: - resolution: {integrity: sha512-1571kXINxHKY7LksWp8wP+zP0YqHSSpl/OW0Y0owFEf2H3s8gCAffWaZivcz14rMkOvn3R/psiQxVsR9t2Nafg==} + i18next@26.0.4: + resolution: {integrity: sha512-gXF7U9bfioXPLv7mw8Qt2nfO7vij5MyINvPgVv99pX3fL1Y01pw2mKBFrlYpRxRCl2wz3ISenj6VsMJT2isfuA==} peerDependencies: typescript: ^5 || ^6 peerDependenciesMeta: @@ -6382,6 +6386,10 @@ packages: resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} engines: {node: '>=20.0.0'} + jsdoc-type-pratt-parser@7.2.0: + resolution: {integrity: sha512-dh140MMgjyg3JhJZY/+iEzW+NO5xR2gpbDFKHqotCmexElVntw7GjWjt511+C/Ef02RU5TKYrJo/Xlzk+OLaTw==} + engines: {node: '>=20.0.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -6431,8 +6439,8 @@ packages: khroma@2.1.0: resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} - knip@6.3.0: - resolution: {integrity: sha512-g6dVPoTw6iNm3cubC5IWxVkVsd0r5hXhTBTbAGIEQN53GdA2ZM/slMTPJ7n5l8pBebNQPHpxjmKxuR4xVQ2/hQ==} + knip@6.3.1: + resolution: {integrity: sha512-22kLJloVcOVOAudCxlFOC0ICAMme7dKsS7pVTEnrmyKGpswb8ieznvAiSKUeFVDJhb01ect6dkDc1Ha1g1sPpg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -6951,8 +6959,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.2.2: - resolution: {integrity: sha512-i6AJdyVa4oQjyvX/6GeER8dpY/xlIV+4NMv/svykcLtURJSy/WzDnnUk/TM4d0uewFHK7xSQz4TbIwPgjky+3A==} + next@16.2.3: + resolution: {integrity: sha512-9V3zV4oZFza3PVev5/poB9g0dEafVcgNyQ8eTRop8GvxZjV2G15FC5ARuG1eFD42QgeYkzJBJzHghNP8Ad9xtA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -7299,8 +7307,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - qs@6.15.0: - resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} engines: {node: '>=0.6'} quansync@0.2.11: @@ -7337,10 +7345,10 @@ packages: resolution: {integrity: sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==} engines: {node: ^20.9.0 || >=22} - react-dom@19.2.4: - resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} peerDependencies: - react: ^19.2.4 + react: ^19.2.5 react-draggable@4.5.0: resolution: {integrity: sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==} @@ -7432,12 +7440,12 @@ packages: react: '>=16.3.0' react-dom: '>=16.3.0' - react-server-dom-webpack@19.2.4: - resolution: {integrity: sha512-zEhkWv6RhXDctC2N7yEUHg3751nvFg81ydHj8LTTZuukF/IF1gcOKqqAL6Ds+kS5HtDVACYPik0IvzkgYXPhlQ==} + react-server-dom-webpack@19.2.5: + resolution: {integrity: sha512-bYhdd2cZJhXHqyJBoloYaJrn8MrL9Egf3ZZVn0OrIODCCORm2goFD7C+xszf6xgfsSJi0rtgB/ichcuHfkJ4yQ==} engines: {node: '>=0.10.0'} peerDependencies: - react: ^19.2.4 - react-dom: ^19.2.4 + react: ^19.2.5 + react-dom: ^19.2.5 webpack: ^5.59.0 react-sortablejs@6.1.4: @@ -7464,8 +7472,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react@19.2.4: - resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} engines: {node: '>=0.10.0'} reactflow@11.11.4: @@ -8295,17 +8303,18 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@0.0.40: - resolution: {integrity: sha512-rs0z6G2el6kS/667ERKQjSMF3R8ZD2H9xDrnRntVOa6OBnyYcOMM/AVpOy/W1lxOkq6EYTO1OUD9DbNSWxRRJw==} + vinext@https://pkg.pr.new/vinext@adbf24d: + resolution: {tarball: https://pkg.pr.new/vinext@adbf24d} + version: 0.0.5 engines: {node: '>=22'} hasBin: true peerDependencies: '@mdx-js/rollup': ^3.0.0 '@vitejs/plugin-react': ^5.1.4 || ^6.0.0 - '@vitejs/plugin-rsc': ^0.5.21 - react: '>=19.2.0' - react-dom: '>=19.2.0' - react-server-dom-webpack: ^19.2.4 + '@vitejs/plugin-rsc': ^0.5.23 + react: ^19.2.5 + react-dom: ^19.2.5 + react-server-dom-webpack: ^19.2.5 vite: ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@mdx-js/rollup': @@ -8725,7 +8734,7 @@ snapshots: idb: 8.0.0 tslib: 2.8.1 - '@antfu/eslint-config@8.0.0(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.2)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)': + '@antfu/eslint-config@8.1.1(@eslint-react/eslint-plugin@3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@next/eslint-plugin-next@16.2.3)(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(@vue/compiler-sfc@3.5.31)(eslint-plugin-react-refresh@0.5.2(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(oxlint@1.58.0(oxlint-tsgolint@0.20.0))(typescript@6.0.2)': dependencies: '@antfu/install-pkg': 1.1.0 '@clack/prompts': 1.2.0 @@ -8745,11 +8754,11 @@ snapshots: eslint-plugin-antfu: 3.2.2(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-command: 3.5.2(@typescript-eslint/rule-tester@8.57.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.2))(@typescript-eslint/utils@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-import-lite: 0.6.0(eslint@10.2.0(jiti@2.6.1)) - eslint-plugin-jsdoc: 62.8.1(eslint@10.2.0(jiti@2.6.1)) + eslint-plugin-jsdoc: 62.9.0(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-jsonc: 3.1.2(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-n: 17.24.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-no-only-tests: 3.3.0 - eslint-plugin-perfectionist: 5.7.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) + eslint-plugin-perfectionist: 5.8.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint-plugin-pnpm: 1.6.0(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-regexp: 3.1.0(eslint@10.2.0(jiti@2.6.1)) eslint-plugin-toml: 1.3.1(eslint@10.2.0(jiti@2.6.1)) @@ -8766,7 +8775,7 @@ snapshots: yaml-eslint-parser: 2.0.0 optionalDependencies: '@eslint-react/eslint-plugin': 3.0.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) - '@next/eslint-plugin-next': 16.2.2 + '@next/eslint-plugin-next': 16.2.3 eslint-plugin-react-refresh: 0.5.2(eslint@10.2.0(jiti@2.6.1)) transitivePeerDependencies: - '@eslint/json' @@ -8888,27 +8897,27 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-ui/react@1.3.0(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@base-ui/react@1.3.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 - '@base-ui/utils': 0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@base-ui/utils': 0.2.6(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@floating-ui/utils': 0.2.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tabbable: 6.4.0 - use-sync-external-store: 1.6.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 - '@base-ui/utils@0.2.6(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@base-ui/utils@0.2.6(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 '@floating-ui/utils': 0.2.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) reselect: 5.1.1 - use-sync-external-store: 1.6.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 @@ -8933,13 +8942,13 @@ snapshots: '@chevrotain/utils@11.1.2': {} - '@chromatic-com/storybook@5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@chromatic-com/storybook@5.1.1(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@neoconfetti/react': 1.0.0 chromatic: 13.3.5 filesize: 10.1.6 jsonfile: 6.2.0 - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) strip-ansi: 7.2.0 transitivePeerDependencies: - '@chromatic-com/cypress' @@ -9158,6 +9167,14 @@ snapshots: esquery: 1.7.0 jsdoc-type-pratt-parser: 7.1.1 + '@es-joy/jsdoccomment@0.86.0': + dependencies: + '@types/estree': 1.0.8 + '@typescript-eslint/types': 8.58.1 + comment-parser: 1.4.6 + esquery: 1.7.0 + jsdoc-type-pratt-parser: 7.2.0 + '@es-joy/resolve.exports@1.2.0': {} '@esbuild/aix-ppc64@0.27.2': @@ -9454,26 +9471,26 @@ snapshots: '@floating-ui/core': 1.7.5 '@floating-ui/utils': 0.2.11 - '@floating-ui/react-dom@2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@floating-ui/dom': 1.7.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@floating-ui/react@0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react@0.26.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@floating-ui/utils': 0.2.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tabbable: 6.4.0 - '@floating-ui/react@0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@floating-ui/react@0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@floating-ui/react-dom': 2.1.8(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@floating-ui/utils': 0.2.11 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tabbable: 6.4.0 '@floating-ui/utils@0.2.11': {} @@ -9484,19 +9501,19 @@ snapshots: dependencies: '@formatjs/fast-memoize': 3.1.1 - '@headlessui/react@2.2.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@headlessui/react@2.2.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@floating-ui/react': 0.26.28(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/focus': 3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@tanstack/react-virtual': 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) + '@floating-ui/react': 0.26.28(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-aria/focus': 3.21.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-aria/interactions': 3.27.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-virtual': 3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) - '@heroicons/react@2.2.0(react@19.2.4)': + '@heroicons/react@2.2.0(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 '@hono/node-server@1.19.13(hono@4.12.12)': dependencies: @@ -9700,7 +9717,7 @@ snapshots: dependencies: lexical: 0.42.0 - '@lexical/devtools-core@0.42.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@lexical/devtools-core@0.42.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@lexical/html': 0.42.0 '@lexical/link': 0.42.0 @@ -9708,8 +9725,8 @@ snapshots: '@lexical/table': 0.42.0 '@lexical/utils': 0.42.0 lexical: 0.42.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@lexical/dragon@0.42.0': dependencies: @@ -9784,10 +9801,10 @@ snapshots: '@lexical/utils': 0.42.0 lexical: 0.42.0 - '@lexical/react@0.42.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(yjs@13.6.30)': + '@lexical/react@0.42.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(yjs@13.6.30)': dependencies: - '@floating-ui/react': 0.27.19(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@lexical/devtools-core': 0.42.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@floating-ui/react': 0.27.19(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@lexical/devtools-core': 0.42.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@lexical/dragon': 0.42.0 '@lexical/extension': 0.42.0 '@lexical/hashtag': 0.42.0 @@ -9804,9 +9821,9 @@ snapshots: '@lexical/utils': 0.42.0 '@lexical/yjs': 0.42.0(yjs@13.6.30) lexical: 0.42.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-error-boundary: 6.1.1(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-error-boundary: 6.1.1(react@19.2.5) transitivePeerDependencies: - yjs @@ -9884,11 +9901,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4)': + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: '@types/mdx': 2.0.13 '@types/react': 19.2.14 - react: 19.2.4 + react: 19.2.5 '@mdx-js/rollup@3.1.1(rollup@4.59.0)': dependencies: @@ -9908,12 +9925,12 @@ snapshots: dependencies: state-local: 1.0.7 - '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@monaco-editor/react@4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@monaco-editor/loader': 1.7.0 monaco-editor: 0.55.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': dependencies: @@ -9926,41 +9943,41 @@ snapshots: '@next/env@16.0.0': {} - '@next/env@16.2.2': {} + '@next/env@16.2.3': {} - '@next/eslint-plugin-next@16.2.2': + '@next/eslint-plugin-next@16.2.3': dependencies: fast-glob: 3.3.1 - '@next/mdx@16.2.2(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.4))': + '@next/mdx@16.2.3(@mdx-js/loader@3.1.1(webpack@5.105.4(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5))': dependencies: source-map: 0.7.6 optionalDependencies: '@mdx-js/loader': 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) - '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) - '@next/swc-darwin-arm64@16.2.2': + '@next/swc-darwin-arm64@16.2.3': optional: true - '@next/swc-darwin-x64@16.2.2': + '@next/swc-darwin-x64@16.2.3': optional: true - '@next/swc-linux-arm64-gnu@16.2.2': + '@next/swc-linux-arm64-gnu@16.2.3': optional: true - '@next/swc-linux-arm64-musl@16.2.2': + '@next/swc-linux-arm64-musl@16.2.3': optional: true - '@next/swc-linux-x64-gnu@16.2.2': + '@next/swc-linux-x64-gnu@16.2.3': optional: true - '@next/swc-linux-x64-musl@16.2.2': + '@next/swc-linux-x64-musl@16.2.3': optional: true - '@next/swc-win32-arm64-msvc@16.2.2': + '@next/swc-win32-arm64-msvc@16.2.3': optional: true - '@next/swc-win32-x64-msvc@16.2.2': + '@next/swc-win32-x64-msvc@16.2.3': optional: true '@nodelib/fs.scandir@2.1.5': @@ -10384,235 +10401,235 @@ snapshots: '@radix-ui/primitive@1.1.3': {} - '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) aria-hidden: 1.2.6 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@radix-ui/primitive': 1.1.3 - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.4) - react: 19.2.4 + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.4)': + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - '@react-aria/focus@3.21.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@react-aria/focus@3.21.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@react-aria/interactions': 3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) + '@react-aria/interactions': 3.27.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-aria/utils': 3.33.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@react-types/shared': 3.33.1(react@19.2.5) '@swc/helpers': 0.5.20 clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@react-aria/interactions@3.27.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@react-aria/interactions@3.27.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) - '@react-aria/utils': 3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-aria/ssr': 3.9.10(react@19.2.5) + '@react-aria/utils': 3.33.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@react-stately/flags': 3.1.2 - '@react-types/shared': 3.33.1(react@19.2.4) + '@react-types/shared': 3.33.1(react@19.2.5) '@swc/helpers': 0.5.20 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@react-aria/ssr@3.9.10(react@19.2.4)': + '@react-aria/ssr@3.9.10(react@19.2.5)': dependencies: '@swc/helpers': 0.5.20 - react: 19.2.4 + react: 19.2.5 - '@react-aria/utils@3.33.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@react-aria/utils@3.33.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@react-aria/ssr': 3.9.10(react@19.2.4) + '@react-aria/ssr': 3.9.10(react@19.2.5) '@react-stately/flags': 3.1.2 - '@react-stately/utils': 3.11.0(react@19.2.4) - '@react-types/shared': 3.33.1(react@19.2.4) + '@react-stately/utils': 3.11.0(react@19.2.5) + '@react-types/shared': 3.33.1(react@19.2.5) '@swc/helpers': 0.5.20 clsx: 2.1.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@react-stately/flags@3.1.2': dependencies: '@swc/helpers': 0.5.20 - '@react-stately/utils@3.11.0(react@19.2.4)': + '@react-stately/utils@3.11.0(react@19.2.5)': dependencies: '@swc/helpers': 0.5.20 - react: 19.2.4 + react: 19.2.5 - '@react-types/shared@3.33.1(react@19.2.4)': + '@react-types/shared@3.33.1(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 - '@reactflow/background@11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/background@11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/controls@11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/controls@11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/core@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/core@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@types/d3': 7.4.3 '@types/d3-drag': 3.0.7 @@ -10622,55 +10639,55 @@ snapshots: d3-drag: 3.0.0 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/minimap@11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/minimap@11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@types/d3-selection': 3.0.11 '@types/d3-zoom': 3.0.8 classcat: 5.0.5 d3-selection: 3.0.0 d3-zoom: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-resizer@2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/node-resizer@2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) classcat: 5.0.5 d3-drag: 3.0.0 d3-selection: 3.0.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@reactflow/node-toolbar@1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@reactflow/node-toolbar@1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) classcat: 5.0.5 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + zustand: 4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer - '@remixicon/react@4.9.0(react@19.2.4)': + '@remixicon/react@4.9.0(react@19.2.5)': dependencies: - react: 19.2.4 + react: 19.2.5 '@resvg/resvg-wasm@2.4.0': {} @@ -10798,11 +10815,11 @@ snapshots: '@sentry/core@10.47.0': {} - '@sentry/react@10.47.0(react@19.2.4)': + '@sentry/react@10.47.0(react@19.2.5)': dependencies: '@sentry/browser': 10.47.0 '@sentry/core': 10.47.0 - react: 19.2.4 + react: 19.2.5 '@shikijs/core@4.0.2': dependencies: @@ -10889,15 +10906,15 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/addon-docs@10.3.5(@types/react@19.2.14)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.4) - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) - '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 transitivePeerDependencies: - '@types/react' @@ -10906,26 +10923,26 @@ snapshots: - vite - webpack - '@storybook/addon-links@10.3.5(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-links@10.3.5(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: '@storybook/global': 5.0.0 - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: - react: 19.2.4 + react: 19.2.5 - '@storybook/addon-onboarding@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-onboarding@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/addon-themes@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/addon-themes@10.3.5(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 - '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/builder-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@storybook/csf-plugin': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: @@ -10933,9 +10950,9 @@ snapshots: - rollup - webpack - '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/csf-plugin@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) unplugin: 2.3.11 optionalDependencies: rollup: 4.59.0 @@ -10944,23 +10961,23 @@ snapshots: '@storybook/global@5.0.0': {} - '@storybook/icons@2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@storybook/icons@2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/nextjs-vite@10.3.5(@babel/core@7.29.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': dependencies: - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) - '@storybook/react': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) - '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) - next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + '@storybook/react-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3)) + next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + vite-plugin-storybook-nextjs: 3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: @@ -10971,25 +10988,25 @@ snapshots: - supports-color - webpack - '@storybook/react-dom-shim@10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': + '@storybook/react-dom-shim@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))': dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) - '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': + '@storybook/react-vite@10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)(webpack@5.105.4(uglify-js@3.19.3))': dependencies: '@joshwooding/vite-plugin-react-docgen-typescript': 0.7.0(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(webpack@5.105.4(uglify-js@3.19.3)) - '@storybook/react': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2) + '@storybook/builder-vite': 10.3.5(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(rollup@4.59.0)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(webpack@5.105.4(uglify-js@3.19.3)) + '@storybook/react': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) empathic: 2.0.0 magic-string: 0.30.21 - react: 19.2.4 + react: 19.2.5 react-docgen: 8.0.3 - react-dom: 19.2.4(react@19.2.4) + react-dom: 19.2.5(react@19.2.5) resolve: 1.22.11 - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tsconfig-paths: 4.2.0 vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' transitivePeerDependencies: @@ -10999,24 +11016,24 @@ snapshots: - typescript - webpack - '@storybook/react@10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2)': + '@storybook/react@10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': dependencies: '@storybook/global': 5.0.0 - '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)) - react: 19.2.4 + '@storybook/react-dom-shim': 10.3.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)) + react: 19.2.5 react-docgen: 8.0.3 react-docgen-typescript: 2.4.0(typescript@6.0.2) - react-dom: 19.2.4(react@19.2.4) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-dom: 19.2.5(react@19.2.5) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) optionalDependencies: typescript: 6.0.2 transitivePeerDependencies: - supports-color - '@streamdown/math@1.0.2(react@19.2.4)': + '@streamdown/math@1.0.2(react@19.2.5)': dependencies: katex: 0.16.45 - react: 19.2.4 + react: 19.2.5 rehype-katex: 7.0.1 remark-math: 6.0.0 transitivePeerDependencies: @@ -11159,10 +11176,10 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/devtools-utils@0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/devtools-utils@0.4.0(@types/react@19.2.14)(react@19.2.5)(solid-js@1.9.11)': optionalDependencies: '@types/react': 19.2.14 - react: 19.2.4 + react: 19.2.5 solid-js: 1.9.11 '@tanstack/devtools@0.11.2(csstype@3.2.3)(solid-js@1.9.11)': @@ -11196,10 +11213,10 @@ snapshots: '@tanstack/pacer-lite': 0.1.1 '@tanstack/store': 0.9.3 - '@tanstack/form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11)': dependencies: '@tanstack/devtools-ui': 0.5.1(csstype@3.2.3)(solid-js@1.9.11) - '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) + '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.5)(solid-js@1.9.11) '@tanstack/form-core': 1.28.6 clsx: 2.1.1 dayjs: 1.11.20 @@ -11218,24 +11235,24 @@ snapshots: '@tanstack/query-devtools@5.96.2': {} - '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/react-devtools@0.10.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(solid-js@1.9.11)': dependencies: '@tanstack/devtools': 0.11.2(csstype@3.2.3)(solid-js@1.9.11) '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) transitivePeerDependencies: - bufferutil - csstype - solid-js - utf-8-validate - '@tanstack/react-form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11)': + '@tanstack/react-form-devtools@0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11)': dependencies: - '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.4)(solid-js@1.9.11) - '@tanstack/form-devtools': 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.4)(solid-js@1.9.11) - react: 19.2.4 + '@tanstack/devtools-utils': 0.4.0(@types/react@19.2.14)(react@19.2.5)(solid-js@1.9.11) + '@tanstack/form-devtools': 0.2.20(@types/react@19.2.14)(csstype@3.2.3)(react@19.2.5)(solid-js@1.9.11) + react: 19.2.5 transitivePeerDependencies: - '@types/react' - csstype @@ -11243,37 +11260,37 @@ snapshots: - solid-js - vue - '@tanstack/react-form@1.28.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-form@1.28.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/form-core': 1.28.6 - '@tanstack/react-store': 0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 + '@tanstack/react-store': 0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 transitivePeerDependencies: - react-dom - '@tanstack/react-query-devtools@5.96.2(@tanstack/react-query@5.96.2(react@19.2.4))(react@19.2.4)': + '@tanstack/react-query-devtools@5.96.2(@tanstack/react-query@5.96.2(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/query-devtools': 5.96.2 - '@tanstack/react-query': 5.96.2(react@19.2.4) - react: 19.2.4 + '@tanstack/react-query': 5.96.2(react@19.2.5) + react: 19.2.5 - '@tanstack/react-query@5.96.2(react@19.2.4)': + '@tanstack/react-query@5.96.2(react@19.2.5)': dependencies: '@tanstack/query-core': 5.96.2 - react: 19.2.4 + react: 19.2.5 - '@tanstack/react-store@0.9.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-store@0.9.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/store': 0.9.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) - '@tanstack/react-virtual@3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@tanstack/react-virtual@3.13.23(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@tanstack/virtual-core': 3.13.23 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) '@tanstack/store@0.9.3': {} @@ -11301,12 +11318,12 @@ snapshots: picocolors: 1.1.1 redent: 3.0.0 - '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@babel/runtime': 7.29.2 '@testing-library/dom': 10.4.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) @@ -11782,36 +11799,36 @@ snapshots: '@typescript-eslint/types': 8.58.1 eslint-visitor-keys: 5.0.1 - '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260407.1': + '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-darwin-x64@7.0.0-dev.20260407.1': + '@typescript/native-preview-darwin-x64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-linux-arm64@7.0.0-dev.20260407.1': + '@typescript/native-preview-linux-arm64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-linux-arm@7.0.0-dev.20260407.1': + '@typescript/native-preview-linux-arm@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-linux-x64@7.0.0-dev.20260407.1': + '@typescript/native-preview-linux-x64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-win32-arm64@7.0.0-dev.20260407.1': + '@typescript/native-preview-win32-arm64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview-win32-x64@7.0.0-dev.20260407.1': + '@typescript/native-preview-win32-x64@7.0.0-dev.20260408.1': optional: true - '@typescript/native-preview@7.0.0-dev.20260407.1': + '@typescript/native-preview@7.0.0-dev.20260408.1': optionalDependencies: - '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260407.1 - '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260407.1 - '@typescript/native-preview-linux-arm': 7.0.0-dev.20260407.1 - '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260407.1 - '@typescript/native-preview-linux-x64': 7.0.0-dev.20260407.1 - '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260407.1 - '@typescript/native-preview-win32-x64': 7.0.0-dev.20260407.1 + '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260408.1 + '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260408.1 + '@typescript/native-preview-linux-arm': 7.0.0-dev.20260408.1 + '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260408.1 + '@typescript/native-preview-linux-x64': 7.0.0-dev.20260408.1 + '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260408.1 + '@typescript/native-preview-win32-x64': 7.0.0-dev.20260408.1 '@ungap/structured-clone@1.3.0': {} @@ -11819,13 +11836,13 @@ snapshots: dependencies: unpic: 4.2.2 - '@unpic/react@1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + '@unpic/react@1.0.2(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': dependencies: '@unpic/core': 1.0.3 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) optionalDependencies: - next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) '@upsetjs/venn.js@2.0.0': optionalDependencies: @@ -11868,21 +11885,21 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.7 vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' - '@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)': + '@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)': dependencies: '@rolldown/pluginutils': 1.0.0-rc.13 es-module-lexer: 2.0.0 estree-walker: 3.0.3 magic-string: 0.30.21 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) srvx: 0.11.15 strip-literal: 3.1.0 turbo-stream: 3.2.0 vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vitefu: 1.1.3(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) optionalDependencies: - react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) + react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) '@vitest/coverage-v8@4.1.3(@voidzero-dev/vite-plus-test@0.1.16(@types/node@25.5.2)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.8.9)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))': dependencies: @@ -12168,12 +12185,12 @@ snapshots: acorn@8.16.0: {} - agentation@3.0.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + agentation@3.0.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): optionalDependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - ahooks@3.9.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + ahooks@3.9.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@babel/runtime': 7.29.2 '@types/js-cookie': 3.0.6 @@ -12181,8 +12198,8 @@ snapshots: intersection-observer: 0.12.2 js-cookie: 3.0.5 lodash: 4.18.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) react-fast-compare: 3.2.2 resize-observer-polyfill: 1.5.1 screenfull: 5.2.0 @@ -12461,14 +12478,14 @@ snapshots: clsx@2.1.1: {} - cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.4) - '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -12866,11 +12883,11 @@ snapshots: dotenv@16.6.1: {} - echarts-for-react@3.0.6(echarts@6.0.0)(react@19.2.4): + echarts-for-react@3.0.6(echarts@6.0.0)(react@19.2.5): dependencies: echarts: 6.0.0 fast-deep-equal: 3.1.3 - react: 19.2.4 + react: 19.2.5 size-sensor: 1.0.3 echarts@6.0.0: @@ -12886,11 +12903,11 @@ snapshots: dependencies: embla-carousel: 8.6.0 - embla-carousel-react@8.6.0(react@19.2.4): + embla-carousel-react@8.6.0(react@19.2.5): dependencies: embla-carousel: 8.6.0 embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) - react: 19.2.4 + react: 19.2.5 embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): dependencies: @@ -13010,7 +13027,7 @@ snapshots: esquery: 1.7.0 jsonc-eslint-parser: 3.1.0 - eslint-markdown@0.6.0(eslint@10.2.0(jiti@2.6.1)): + eslint-markdown@0.6.1(eslint@10.2.0(jiti@2.6.1)): dependencies: '@eslint/markdown': 7.5.1 micromark-util-normalize-identifier: 2.0.1 @@ -13076,12 +13093,12 @@ snapshots: dependencies: eslint: 10.2.0(jiti@2.6.1) - eslint-plugin-jsdoc@62.8.1(eslint@10.2.0(jiti@2.6.1)): + eslint-plugin-jsdoc@62.9.0(eslint@10.2.0(jiti@2.6.1)): dependencies: - '@es-joy/jsdoccomment': 0.84.0 + '@es-joy/jsdoccomment': 0.86.0 '@es-joy/resolve.exports': 1.2.0 are-docs-informative: 0.0.2 - comment-parser: 1.4.5 + comment-parser: 1.4.6 debug: 4.4.3(supports-color@8.1.1) escape-string-regexp: 4.0.0 eslint: 10.2.0(jiti@2.6.1) @@ -13156,7 +13173,7 @@ snapshots: eslint-plugin-no-only-tests@3.3.0: {} - eslint-plugin-perfectionist@5.7.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): + eslint-plugin-perfectionist@5.8.0(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2): dependencies: '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.2.0(jiti@2.6.1) @@ -13270,7 +13287,7 @@ snapshots: '@eslint-community/regexpp': 4.12.2 comment-parser: 1.4.6 eslint: 10.2.0(jiti@2.6.1) - jsdoc-type-pratt-parser: 7.1.1 + jsdoc-type-pratt-parser: 7.2.0 refa: 0.12.1 regexp-ast-analysis: 0.7.1 scslre: 0.3.0 @@ -13291,11 +13308,11 @@ snapshots: ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 - eslint-plugin-storybook@10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): + eslint-plugin-storybook@10.3.5(eslint@10.2.0(jiti@2.6.1))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): dependencies: '@typescript-eslint/utils': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) eslint: 10.2.0(jiti@2.6.1) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) transitivePeerDependencies: - supports-color - typescript @@ -13906,7 +13923,7 @@ snapshots: dependencies: '@babel/runtime': 7.29.2 - i18next@26.0.3(typescript@6.0.2): + i18next@26.0.4(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 optionalDependencies: @@ -14042,12 +14059,12 @@ snapshots: jiti@2.6.1: {} - jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.4): + jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5): optionalDependencies: '@babel/core': 7.29.0 '@babel/template': 7.28.6 '@types/react': 19.2.14 - react: 19.2.4 + react: 19.2.5 js-audio-recorder@1.0.7: {} @@ -14067,6 +14084,8 @@ snapshots: jsdoc-type-pratt-parser@7.1.1: {} + jsdoc-type-pratt-parser@7.2.0: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -14107,7 +14126,7 @@ snapshots: khroma@2.1.0: {} - knip@6.3.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.3.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -14911,30 +14930,30 @@ snapshots: neo-async@2.6.2: {} - next-themes@0.4.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + next-themes@0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0): + next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0): dependencies: - '@next/env': 16.2.2 + '@next/env': 16.2.3 '@swc/helpers': 0.5.15 baseline-browser-mapping: 2.10.12 caniuse-lite: 1.0.30001781 postcss: 8.4.31 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + styled-jsx: 5.1.6(@babel/core@7.29.0)(react@19.2.5) optionalDependencies: - '@next/swc-darwin-arm64': 16.2.2 - '@next/swc-darwin-x64': 16.2.2 - '@next/swc-linux-arm64-gnu': 16.2.2 - '@next/swc-linux-arm64-musl': 16.2.2 - '@next/swc-linux-x64-gnu': 16.2.2 - '@next/swc-linux-x64-musl': 16.2.2 - '@next/swc-win32-arm64-msvc': 16.2.2 - '@next/swc-win32-x64-msvc': 16.2.2 + '@next/swc-darwin-arm64': 16.2.3 + '@next/swc-darwin-x64': 16.2.3 + '@next/swc-linux-arm64-gnu': 16.2.3 + '@next/swc-linux-arm64-musl': 16.2.3 + '@next/swc-linux-x64-gnu': 16.2.3 + '@next/swc-linux-x64-musl': 16.2.3 + '@next/swc-win32-arm64-msvc': 16.2.3 + '@next/swc-win32-x64-msvc': 16.2.3 '@playwright/test': 1.59.1 sass: 1.98.0 sharp: 0.34.5 @@ -14969,12 +14988,12 @@ snapshots: dependencies: boolbase: 1.0.0 - nuqs@2.8.9(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react@19.2.4): + nuqs@2.8.9(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react@19.2.5): dependencies: '@standard-schema/spec': 1.0.0 - react: 19.2.4 + react: 19.2.5 optionalDependencies: - next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) + next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) object-assign@4.1.1: {} @@ -15359,11 +15378,11 @@ snapshots: punycode@2.3.1: {} - qrcode.react@4.2.0(react@19.2.4): + qrcode.react@4.2.0(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 - qs@6.15.0: + qs@6.15.1: dependencies: side-channel: '@nolyfill/side-channel@1.0.44' @@ -15381,15 +15400,15 @@ snapshots: strip-json-comments: 2.0.1 optional: true - re-resizable@6.11.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + re-resizable@6.11.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - react-18-input-autosize@3.0.0(react@19.2.4): + react-18-input-autosize@3.0.0(react@19.2.5): dependencies: prop-types: 15.8.1 - react: 19.2.4 + react: 19.2.5 react-docgen-typescript@2.4.0(typescript@6.0.2): dependencies: @@ -15410,143 +15429,143 @@ snapshots: transitivePeerDependencies: - supports-color - react-dom@19.2.4(react@19.2.4): + react-dom@19.2.5(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 scheduler: 0.27.0 - react-draggable@4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-draggable@4.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: clsx: 2.1.1 prop-types: 15.8.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - react-easy-crop@5.5.7(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-easy-crop@5.5.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: normalize-wheel: 1.0.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) tslib: 2.8.1 - react-error-boundary@6.1.1(react@19.2.4): + react-error-boundary@6.1.1(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 react-fast-compare@3.2.2: {} - react-hotkeys-hook@5.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-hotkeys-hook@5.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) - react-i18next@17.0.2(i18next@26.0.3(typescript@6.0.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(typescript@6.0.2): + react-i18next@17.0.2(i18next@26.0.4(typescript@6.0.2))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2): dependencies: '@babel/runtime': 7.29.2 html-parse-stringify: 3.0.1 - i18next: 26.0.3(typescript@6.0.2) - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + i18next: 26.0.4(typescript@6.0.2) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: - react-dom: 19.2.4(react@19.2.4) + react-dom: 19.2.5(react@19.2.5) typescript: 6.0.2 react-is@16.13.1: {} react-is@17.0.2: {} - react-multi-email@1.0.25(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-multi-email@1.0.25(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) react-papaparse@4.4.0: dependencies: '@types/papaparse': 5.5.2 papaparse: 5.5.3 - react-pdf-highlighter@8.0.0-rc.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-pdf-highlighter@8.0.0-rc.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: pdfjs-dist: 4.4.168 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-rnd: 10.5.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-rnd: 10.5.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-debounce: 4.0.0 - react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.4): + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.4) - react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.4) - use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.4) + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 - react-rnd@10.5.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-rnd@10.5.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - re-resizable: 6.11.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-draggable: 4.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + re-resizable: 6.11.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-draggable: 4.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) tslib: 2.6.2 - react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)): + react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)): dependencies: acorn-loose: 8.5.2 neo-async: 2.6.2 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) webpack: 5.105.4(uglify-js@3.19.3) webpack-sources: 3.3.4 - react-sortablejs@6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sortablejs@1.15.7): + react-sortablejs@6.1.4(@types/sortablejs@1.15.9)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sortablejs@1.15.7): dependencies: '@types/sortablejs': 1.15.9 classnames: 2.3.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) sortablejs: 1.15.7 tiny-invariant: 1.2.0 - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): dependencies: get-nonce: 1.0.1 - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.4): + react-textarea-autosize@8.5.9(@types/react@19.2.14)(react@19.2.5): dependencies: '@babel/runtime': 7.29.2 - react: 19.2.4 - use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.4) - use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + use-composed-ref: 1.4.0(@types/react@19.2.14)(react@19.2.5) + use-latest: 1.3.0(@types/react@19.2.14)(react@19.2.5) transitivePeerDependencies: - '@types/react' - react@19.2.4: {} + react@19.2.5: {} - reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + reactflow@11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: - '@reactflow/background': 11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/controls': 11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/minimap': 11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/node-resizer': 2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + '@reactflow/background': 11.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@reactflow/controls': 11.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@reactflow/core': 11.11.4(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@reactflow/minimap': 11.7.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@reactflow/node-resizer': 2.2.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) transitivePeerDependencies: - '@types/react' - immer @@ -15997,10 +16016,10 @@ snapshots: std-semver@1.0.8: {} - storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: '@storybook/global': 5.0.0 - '@storybook/icons': 2.0.1(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@storybook/icons': 2.0.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/expect': 3.2.4 @@ -16010,7 +16029,7 @@ snapshots: open: 10.2.0 recast: 0.23.11 semver: 7.7.4 - use-sync-external-store: 1.6.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.5) ws: 8.20.0 transitivePeerDependencies: - '@testing-library/dom' @@ -16019,15 +16038,15 @@ snapshots: - react-dom - utf-8-validate - streamdown@2.5.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + streamdown@2.5.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): dependencies: clsx: 2.1.1 hast-util-to-jsx-runtime: 2.3.6 html-url-attributes: 3.0.1 marked: 17.0.5 mermaid: 11.14.0 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) rehype-harden: 1.1.8 rehype-raw: 7.0.0 rehype-sanitize: 6.0.0 @@ -16096,10 +16115,10 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4): + styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5): dependencies: client-only: 0.0.1 - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@babel/core': 7.29.0 @@ -16405,50 +16424,50 @@ snapshots: dependencies: punycode: 2.3.1 - use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.4): + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 - use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.4): + use-composed-ref@1.4.0(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - use-context-selector@2.0.0(react@19.2.4)(scheduler@0.27.0): + use-context-selector@2.0.0(react@19.2.5)(scheduler@0.27.0): dependencies: - react: 19.2.4 + react: 19.2.5 scheduler: 0.27.0 - use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.4): + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 optionalDependencies: '@types/react': 19.2.14 - use-latest@1.3.0(@types/react@19.2.14)(react@19.2.4): + use-latest@1.3.0(@types/react@19.2.14)(react@19.2.5): dependencies: - react: 19.2.4 - use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.4) + react: 19.2.5 + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 - use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.4): + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): dependencies: detect-node-es: 1.1.0 - react: 19.2.4 + react: 19.2.5 tslib: 2.8.1 optionalDependencies: '@types/react': 19.2.14 use-strict@1.0.1: {} - use-sync-external-store@1.6.0(react@19.2.4): + use-sync-external-store@1.6.0(react@19.2.5): dependencies: - react: 19.2.4 + react: 19.2.5 util-arity@1.1.0: {} @@ -16482,21 +16501,21 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@0.0.40(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4)(typescript@6.0.2): + vinext@https://pkg.pr.new/vinext@adbf24d(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2): dependencies: - '@unpic/react': 1.0.2(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@unpic/react': 1.0.2(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vercel/og': 0.8.6 '@vitejs/plugin-react': 6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)) magic-string: 0.30.21 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-plugin-commonjs: 0.10.4 vite-tsconfig-paths: 6.1.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) optionalDependencies: '@mdx-js/rollup': 3.1.1(rollup@4.59.0) - '@vitejs/plugin-rsc': 0.5.22(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.4(react@19.2.4))(react-server-dom-webpack@19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.4) - react-server-dom-webpack: 19.2.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(webpack@5.105.4(uglify-js@3.19.3)) + '@vitejs/plugin-rsc': 0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5) + react-server-dom-webpack: 19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)) transitivePeerDependencies: - next - supports-color @@ -16531,14 +16550,14 @@ snapshots: - typescript - ws - vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(typescript@6.0.2): + vite-plugin-storybook-nextjs@3.2.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(storybook@10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2): dependencies: '@next/env': 16.0.0 image-size: 2.0.2 magic-string: 0.30.21 module-alias: 2.3.4 - next: 16.2.2(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.98.0) - storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + next: 16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0) + storybook: 10.3.5(@testing-library/dom@10.4.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) ts-dedent: 2.2.0 vite: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' vite-tsconfig-paths: 5.1.4(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(typescript@6.0.2) @@ -16772,23 +16791,23 @@ snapshots: dependencies: tslib: 2.3.0 - zundo@2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))): + zundo@2.3.0(zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))): dependencies: - zustand: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + zustand: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) - zustand@4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4): + zustand@4.5.7(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5): dependencies: - use-sync-external-store: 1.6.0(react@19.2.4) + use-sync-external-store: 1.6.0(react@19.2.5) optionalDependencies: '@types/react': 19.2.14 immer: 11.1.4 - react: 19.2.4 + react: 19.2.5 - zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): optionalDependencies: '@types/react': 19.2.14 immer: 11.1.4 - react: 19.2.4 - use-sync-external-store: 1.6.0(react@19.2.4) + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b7918fff1b..f715787766 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -47,7 +47,7 @@ overrides: catalog: "@amplitude/analytics-browser": 2.38.1 "@amplitude/plugin-session-replay-browser": 1.27.6 - "@antfu/eslint-config": 8.0.0 + "@antfu/eslint-config": 8.1.1 "@base-ui/react": 1.3.0 "@chromatic-com/storybook": 5.1.1 "@cucumber/cucumber": 12.7.0 @@ -73,8 +73,8 @@ catalog: "@mdx-js/react": 3.1.1 "@mdx-js/rollup": 3.1.1 "@monaco-editor/react": 4.7.0 - "@next/eslint-plugin-next": 16.2.2 - "@next/mdx": 16.2.2 + "@next/eslint-plugin-next": 16.2.3 + "@next/mdx": 16.2.3 "@orpc/client": 1.13.13 "@orpc/contract": 1.13.13 "@orpc/openapi-client": 1.13.13 @@ -120,9 +120,9 @@ catalog: "@types/sortablejs": 1.15.9 "@typescript-eslint/eslint-plugin": 8.58.1 "@typescript-eslint/parser": 8.58.1 - "@typescript/native-preview": 7.0.0-dev.20260407.1 + "@typescript/native-preview": 7.0.0-dev.20260408.1 "@vitejs/plugin-react": 6.0.1 - "@vitejs/plugin-rsc": 0.5.22 + "@vitejs/plugin-rsc": 0.5.23 "@vitest/coverage-v8": 4.1.3 abcjs: 6.6.2 agentation: 3.0.2 @@ -146,7 +146,7 @@ catalog: emoji-mart: 5.6.0 es-toolkit: 1.45.1 eslint: 10.2.0 - eslint-markdown: 0.6.0 + eslint-markdown: 0.6.1 eslint-plugin-better-tailwindcss: 4.3.2 eslint-plugin-hyoban: 0.14.1 eslint-plugin-markdown-preferences: 0.41.0 @@ -160,7 +160,7 @@ catalog: hono: 4.12.12 html-entities: 2.6.0 html-to-image: 1.11.13 - i18next: 26.0.3 + i18next: 26.0.4 i18next-resources-to-backend: 1.2.1 iconify-import-svg: 0.1.2 immer: 11.1.4 @@ -170,7 +170,7 @@ catalog: js-yaml: 4.1.1 jsonschema: 1.5.0 katex: 0.16.45 - knip: 6.3.0 + knip: 6.3.1 ky: 2.0.0 lamejs: 1.2.1 lexical: 0.42.0 @@ -178,24 +178,24 @@ catalog: mime: 4.1.0 mitt: 3.0.1 negotiator: 1.0.0 - next: 16.2.2 + next: 16.2.3 next-themes: 0.4.6 nuqs: 2.8.9 pinyin-pro: 3.28.0 postcss: 8.5.9 postcss-js: 5.1.0 qrcode.react: 4.2.0 - qs: 6.15.0 - react: 19.2.4 + qs: 6.15.1 + react: 19.2.5 react-18-input-autosize: 3.0.0 - react-dom: 19.2.4 + react-dom: 19.2.5 react-easy-crop: 5.5.7 react-hotkeys-hook: 5.2.4 react-i18next: 17.0.2 react-multi-email: 1.0.25 react-papaparse: 4.4.0 react-pdf-highlighter: 8.0.0-rc.0 - react-server-dom-webpack: 19.2.4 + react-server-dom-webpack: 19.2.5 react-sortablejs: 6.1.4 react-textarea-autosize: 8.5.9 reactflow: 11.11.4 @@ -219,7 +219,7 @@ catalog: unist-util-visit: 5.1.0 use-context-selector: 2.0.0 uuid: 13.0.0 - vinext: 0.0.40 + vinext: https://pkg.pr.new/vinext@adbf24d vite: npm:@voidzero-dev/vite-plus-core@0.1.16 vite-plugin-inspect: 12.0.0-beta.1 vite-plus: 0.1.16 From d1e33ba9eae1135490c5ca4d93ddf31096a75c27 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Thu, 9 Apr 2026 15:59:15 +0800 Subject: [PATCH 3/8] refactor(api): reduce Dify GraphInitParams usage (#34825) --- api/core/app/apps/pipeline/pipeline_runner.py | 26 ++++----- api/core/app/apps/workflow_app_runner.py | 54 ++++++++++--------- api/core/workflow/node_factory.py | 39 ++++++++++++++ .../workflow/nodes/trigger_webhook/node.py | 2 +- api/core/workflow/workflow_entry.py | 53 ++++++++++-------- api/services/workflow_service.py | 29 ++++++---- .../core/workflow/test_node_factory.py | 45 ++++++++++++++++ .../workflow/test_workflow_entry_helpers.py | 38 +++++++------ .../services/test_workflow_service.py | 19 +++++-- 9 files changed, 215 insertions(+), 90 deletions(-) diff --git a/api/core/app/apps/pipeline/pipeline_runner.py b/api/core/app/apps/pipeline/pipeline_runner.py index b4d2310da8..36daaf09e9 100644 --- a/api/core/app/apps/pipeline/pipeline_runner.py +++ b/api/core/app/apps/pipeline/pipeline_runner.py @@ -2,7 +2,6 @@ import logging import time from typing import cast -from graphon.entities import GraphInitParams from graphon.enums import WorkflowType from graphon.graph import Graph from graphon.graph_events import GraphEngineEvent, GraphRunFailedEvent @@ -22,7 +21,7 @@ from core.app.entities.app_invoke_entities import ( ) from core.app.workflow.layers.persistence import PersistenceWorkflowInfo, WorkflowPersistenceLayer from core.repositories.factory import WorkflowExecutionRepository, WorkflowNodeExecutionRepository -from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id +from core.workflow.node_factory import DifyGraphInitContext, DifyNodeFactory, get_default_root_node_id from core.workflow.system_variables import build_bootstrap_variables, build_system_variables from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool from core.workflow.workflow_entry import WorkflowEntry @@ -265,22 +264,23 @@ class PipelineRunner(WorkflowBasedAppRunner): # graph_config["nodes"] = real_run_nodes # graph_config["edges"] = real_edges # init graph - # Create required parameters for Graph.init - graph_init_params = GraphInitParams( + # Create explicit graph init context for Graph.init. + run_context = build_dify_run_context( + tenant_id=workflow.tenant_id, + app_id=self._app_id, + user_id=self.application_generate_entity.user_id, + user_from=user_from, + invoke_from=invoke_from, + ) + graph_init_context = DifyGraphInitContext( workflow_id=workflow.id, graph_config=graph_config, - run_context=build_dify_run_context( - tenant_id=workflow.tenant_id, - app_id=self._app_id, - user_id=self.application_generate_entity.user_id, - user_from=user_from, - invoke_from=invoke_from, - ), + run_context=run_context, call_depth=0, ) - node_factory = DifyNodeFactory( - graph_init_params=graph_init_params, + node_factory = DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, graph_runtime_state=graph_runtime_state, ) if start_node_id is None: diff --git a/api/core/app/apps/workflow_app_runner.py b/api/core/app/apps/workflow_app_runner.py index caa6b82bab..437432611d 100644 --- a/api/core/app/apps/workflow_app_runner.py +++ b/api/core/app/apps/workflow_app_runner.py @@ -3,7 +3,6 @@ import time from collections.abc import Mapping, Sequence from typing import Any, cast -from graphon.entities import GraphInitParams from graphon.entities.graph_config import NodeConfigDictAdapter from graphon.entities.pause_reason import HumanInputRequired from graphon.graph import Graph @@ -67,7 +66,12 @@ from core.app.entities.queue_entities import ( QueueWorkflowSucceededEvent, ) from core.rag.entities import RetrievalSourceMetadata -from core.workflow.node_factory import DifyNodeFactory, get_default_root_node_id, resolve_workflow_node_class +from core.workflow.node_factory import ( + DifyGraphInitContext, + DifyNodeFactory, + get_default_root_node_id, + resolve_workflow_node_class, +) from core.workflow.system_variables import ( build_bootstrap_variables, default_system_variables, @@ -127,24 +131,25 @@ class WorkflowBasedAppRunner: if not isinstance(graph_config.get("edges"), list): raise ValueError("edges in workflow graph must be a list") - # Create required parameters for Graph.init - graph_init_params = GraphInitParams( + # Create explicit graph init context for Graph.init. + run_context = build_dify_run_context( + tenant_id=tenant_id or "", + app_id=self._app_id, + user_id=user_id, + user_from=user_from, + invoke_from=invoke_from, + ) + graph_init_context = DifyGraphInitContext( workflow_id=workflow_id, graph_config=graph_config, - run_context=build_dify_run_context( - tenant_id=tenant_id or "", - app_id=self._app_id, - user_id=user_id, - user_from=user_from, - invoke_from=invoke_from, - ), + run_context=run_context, call_depth=0, ) # Use the provided graph_runtime_state for consistent state management - node_factory = DifyNodeFactory( - graph_init_params=graph_init_params, + node_factory = DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, graph_runtime_state=graph_runtime_state, ) @@ -289,22 +294,23 @@ class WorkflowBasedAppRunner: typed_node_configs = [NodeConfigDictAdapter.validate_python(node) for node in node_configs] - # Create required parameters for Graph.init - graph_init_params = GraphInitParams( + # Create explicit graph init context for Graph.init. + run_context = build_dify_run_context( + tenant_id=workflow.tenant_id, + app_id=self._app_id, + user_id=user_id, + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + ) + graph_init_context = DifyGraphInitContext( workflow_id=workflow.id, graph_config=graph_config, - run_context=build_dify_run_context( - tenant_id=workflow.tenant_id, - app_id=self._app_id, - user_id=user_id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.DEBUGGER, - ), + run_context=run_context, call_depth=0, ) - node_factory = DifyNodeFactory( - graph_init_params=graph_init_params, + node_factory = DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, graph_runtime_state=graph_runtime_state, ) diff --git a/api/core/workflow/node_factory.py b/api/core/workflow/node_factory.py index f6c3aee4c1..b04ac7da3d 100644 --- a/api/core/workflow/node_factory.py +++ b/api/core/workflow/node_factory.py @@ -1,6 +1,7 @@ import importlib import pkgutil from collections.abc import Callable, Iterator, Mapping, MutableMapping +from dataclasses import dataclass from functools import lru_cache from typing import TYPE_CHECKING, Any, cast, final, override @@ -67,6 +68,31 @@ _START_NODE_TYPES: frozenset[NodeType] = frozenset( ) +@dataclass(frozen=True, slots=True) +class DifyGraphInitContext: + """Explicit graph-init values owned by the workflow layer. + + Dify is gradually removing direct `GraphInitParams` construction from its + production call sites. Keep the translation here until `graphon` exposes an + equivalent explicit API. + """ + + workflow_id: str + graph_config: Mapping[str, Any] + run_context: Mapping[str, Any] + call_depth: int + + def to_graph_init_params(self) -> "GraphInitParams": + from graphon.entities import GraphInitParams + + return GraphInitParams( + workflow_id=self.workflow_id, + graph_config=self.graph_config, + run_context=self.run_context, + call_depth=self.call_depth, + ) + + def _import_node_package(package_name: str, *, excluded_modules: frozenset[str] = frozenset()) -> None: package = importlib.import_module(package_name) for _, module_name, _ in pkgutil.walk_packages(package.__path__, package.__name__ + "."): @@ -237,6 +263,19 @@ class DifyNodeFactory(NodeFactory): Default implementation of NodeFactory that resolves node classes from the live registry. """ + @classmethod + def from_graph_init_context( + cls, + *, + graph_init_context: DifyGraphInitContext, + graph_runtime_state: "GraphRuntimeState", + ) -> "DifyNodeFactory": + """Bridge Dify's explicit init context into the current `graphon` API.""" + return cls( + graph_init_params=graph_init_context.to_graph_init_params(), + graph_runtime_state=graph_runtime_state, + ) + def __init__( self, graph_init_params: "GraphInitParams", diff --git a/api/core/workflow/nodes/trigger_webhook/node.py b/api/core/workflow/nodes/trigger_webhook/node.py index 6a0d633627..8c866aea81 100644 --- a/api/core/workflow/nodes/trigger_webhook/node.py +++ b/api/core/workflow/nodes/trigger_webhook/node.py @@ -29,7 +29,7 @@ class TriggerWebhookNode(Node[WebhookData]): def post_init(self) -> None: from core.workflow.node_runtime import DifyFileReferenceFactory - self._file_reference_factory = DifyFileReferenceFactory(self.graph_init_params.run_context) + self._file_reference_factory = DifyFileReferenceFactory(self.run_context) @classmethod def get_default_config(cls, filters: Mapping[str, object] | None = None) -> Mapping[str, object]: diff --git a/api/core/workflow/workflow_entry.py b/api/core/workflow/workflow_entry.py index cecc20145a..f0a5fbb400 100644 --- a/api/core/workflow/workflow_entry.py +++ b/api/core/workflow/workflow_entry.py @@ -24,7 +24,12 @@ from core.app.entities.app_invoke_entities import InvokeFrom, UserFrom, build_di from core.app.file_access import DatabaseFileAccessController from core.app.workflow.layers.llm_quota import LLMQuotaLayer from core.app.workflow.layers.observability import ObservabilityLayer -from core.workflow.node_factory import DifyNodeFactory, is_start_node_type, resolve_workflow_node_class +from core.workflow.node_factory import ( + DifyGraphInitContext, + DifyNodeFactory, + is_start_node_type, + resolve_workflow_node_class, +) from core.workflow.system_variables import ( default_system_variables, get_node_creation_preload_selectors, @@ -251,17 +256,18 @@ class WorkflowEntry: node_version = str(node_config_data.version) node_cls = resolve_workflow_node_class(node_type=node_type, node_version=node_version) - # init graph init params and runtime state - graph_init_params = GraphInitParams( + # init graph context and runtime state + run_context = build_dify_run_context( + tenant_id=workflow.tenant_id, + app_id=workflow.app_id, + user_id=user_id, + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + ) + graph_init_context = DifyGraphInitContext( workflow_id=workflow.id, graph_config=workflow.graph_dict, - run_context=build_dify_run_context( - tenant_id=workflow.tenant_id, - app_id=workflow.app_id, - user_id=user_id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.DEBUGGER, - ), + run_context=run_context, call_depth=0, ) graph_runtime_state = GraphRuntimeState( @@ -313,8 +319,8 @@ class WorkflowEntry: ) # init workflow run state - node_factory = DifyNodeFactory( - graph_init_params=graph_init_params, + node_factory = DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, graph_runtime_state=graph_runtime_state, ) node = node_factory.create_node(node_config) @@ -409,17 +415,18 @@ class WorkflowEntry: variable_pool = VariablePool() add_variables_to_pool(variable_pool, default_system_variables()) - # init graph init params and runtime state - graph_init_params = GraphInitParams( + # init graph context and runtime state + run_context = build_dify_run_context( + tenant_id=tenant_id, + app_id="", + user_id=user_id, + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + ) + graph_init_context = DifyGraphInitContext( workflow_id="", graph_config=graph_dict, - run_context=build_dify_run_context( - tenant_id=tenant_id, - app_id="", - user_id=user_id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.DEBUGGER, - ), + run_context=run_context, call_depth=0, ) graph_runtime_state = GraphRuntimeState( @@ -430,8 +437,8 @@ class WorkflowEntry: # init workflow run state node_config = NodeConfigDictAdapter.validate_python({"id": node_id, "data": node_data}) - node_factory = DifyNodeFactory( - graph_init_params=graph_init_params, + node_factory = DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, graph_runtime_state=graph_runtime_state, ) node = node_factory.create_node(node_config) diff --git a/api/services/workflow_service.py b/api/services/workflow_service.py index 1e3feeed29..c28704e83b 100644 --- a/api/services/workflow_service.py +++ b/api/services/workflow_service.py @@ -5,7 +5,7 @@ import uuid from collections.abc import Callable, Generator, Mapping, Sequence from typing import Any, cast -from graphon.entities import GraphInitParams, WorkflowNodeExecution +from graphon.entities import WorkflowNodeExecution from graphon.entities.graph_config import NodeConfigDict from graphon.entities.pause_reason import HumanInputRequired from graphon.enums import ( @@ -48,7 +48,12 @@ from core.workflow.human_input_compat import ( normalize_human_input_node_data_for_graph, parse_human_input_delivery_methods, ) -from core.workflow.node_factory import LATEST_VERSION, get_node_type_classes_mapping, is_start_node_type +from core.workflow.node_factory import ( + LATEST_VERSION, + DifyGraphInitContext, + get_node_type_classes_mapping, + is_start_node_type, +) from core.workflow.node_runtime import DifyHumanInputNodeRuntime, apply_dify_debug_email_recipient from core.workflow.system_variables import build_bootstrap_variables, build_system_variables, default_system_variables from core.workflow.variable_pool_initializer import add_node_inputs_to_pool, add_variables_to_pool @@ -1132,18 +1137,20 @@ class WorkflowService: node_config: NodeConfigDict, variable_pool: VariablePool, ) -> HumanInputNode: - graph_init_params = GraphInitParams( + run_context = build_dify_run_context( + tenant_id=workflow.tenant_id, + app_id=workflow.app_id, + user_id=account.id, + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + ) + graph_init_context = DifyGraphInitContext( workflow_id=workflow.id, graph_config=workflow.graph_dict, - run_context=build_dify_run_context( - tenant_id=workflow.tenant_id, - app_id=workflow.app_id, - user_id=account.id, - user_from=UserFrom.ACCOUNT, - invoke_from=InvokeFrom.DEBUGGER, - ), + run_context=run_context, call_depth=0, ) + graph_init_params = graph_init_context.to_graph_init_params() graph_runtime_state = GraphRuntimeState( variable_pool=variable_pool, start_at=time.perf_counter(), @@ -1153,7 +1160,7 @@ class WorkflowService: config=node_config, graph_init_params=graph_init_params, graph_runtime_state=graph_runtime_state, - runtime=DifyHumanInputNodeRuntime(graph_init_params.run_context), + runtime=DifyHumanInputNodeRuntime(run_context), ) return node diff --git a/api/tests/unit_tests/core/workflow/test_node_factory.py b/api/tests/unit_tests/core/workflow/test_node_factory.py index bc0b339fec..dfe1a47e37 100644 --- a/api/tests/unit_tests/core/workflow/test_node_factory.py +++ b/api/tests/unit_tests/core/workflow/test_node_factory.py @@ -110,6 +110,34 @@ class TestFetchMemory: ) +class TestDifyGraphInitContext: + def test_to_graph_init_params_preserves_explicit_values(self): + run_context = { + DIFY_RUN_CONTEXT_KEY: DifyRunContext( + tenant_id="tenant-id", + app_id="app-id", + user_id="user-id", + user_from=UserFrom.ACCOUNT, + invoke_from=InvokeFrom.DEBUGGER, + ), + "extra": "value", + } + graph_config = {"nodes": [], "edges": []} + graph_init_context = node_factory.DifyGraphInitContext( + workflow_id="workflow-id", + graph_config=graph_config, + run_context=run_context, + call_depth=2, + ) + + result = graph_init_context.to_graph_init_params() + + assert result.workflow_id == "workflow-id" + assert result.graph_config == graph_config + assert result.run_context == run_context + assert result.call_depth == 2 + + class TestDefaultWorkflowCodeExecutor: def test_execute_delegates_to_code_executor(self, monkeypatch): executor = node_factory.DefaultWorkflowCodeExecutor() @@ -172,6 +200,23 @@ class TestCodeExecutorJinja2TemplateRenderer: class TestDifyNodeFactoryInit: + def test_from_graph_init_context_translates_before_init(self): + graph_init_context = MagicMock() + graph_init_context.to_graph_init_params.return_value = sentinel.graph_init_params + + with patch.object(node_factory.DifyNodeFactory, "__init__", return_value=None) as init: + factory = node_factory.DifyNodeFactory.from_graph_init_context( + graph_init_context=graph_init_context, + graph_runtime_state=sentinel.graph_runtime_state, + ) + + assert isinstance(factory, node_factory.DifyNodeFactory) + graph_init_context.to_graph_init_params.assert_called_once_with() + init.assert_called_once_with( + graph_init_params=sentinel.graph_init_params, + graph_runtime_state=sentinel.graph_runtime_state, + ) + def test_init_builds_default_dependencies(self): graph_init_params = SimpleNamespace(run_context={"context": "value"}) graph_runtime_state = sentinel.graph_runtime_state diff --git a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py index 879c0bb721..6dcaed1143 100644 --- a/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py +++ b/api/tests/unit_tests/core/workflow/test_workflow_entry_helpers.py @@ -349,7 +349,7 @@ class TestWorkflowEntrySingleStepRun: ] with ( - patch.object(workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params), + patch.object(workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context), patch.object( workflow_entry, "GraphRuntimeState", @@ -358,7 +358,7 @@ class TestWorkflowEntrySingleStepRun: patch.object(workflow_entry, "build_dify_run_context", return_value={"_dify": "context"}), patch.object(workflow_entry.time, "perf_counter", return_value=123.0), patch.object(workflow_entry, "resolve_workflow_node_class", return_value=FakeLLMNode), - patch.object(workflow_entry, "DifyNodeFactory") as dify_node_factory, + patch.object(workflow_entry.DifyNodeFactory, "from_graph_init_context") as dify_node_factory, patch.object(workflow_entry, "load_into_variable_pool"), patch.object(workflow_entry.WorkflowEntry, "mapping_user_inputs_to_variable_pool"), patch.object( @@ -412,12 +412,12 @@ class TestWorkflowEntrySingleStepRun: raise NotImplementedError with ( - patch.object(workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params), + patch.object(workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context), patch.object(workflow_entry, "GraphRuntimeState", return_value=sentinel.graph_runtime_state), patch.object(workflow_entry, "build_dify_run_context", return_value={"_dify": "context"}), patch.object(workflow_entry.time, "perf_counter", return_value=123.0), patch.object(workflow_entry, "resolve_workflow_node_class", return_value=FakeNode), - patch.object(workflow_entry, "DifyNodeFactory") as dify_node_factory, + patch.object(workflow_entry.DifyNodeFactory, "from_graph_init_context") as dify_node_factory, patch.object(workflow_entry, "add_node_inputs_to_pool") as add_node_inputs_to_pool, patch.object(workflow_entry, "load_into_variable_pool") as load_into_variable_pool, patch.object( @@ -481,12 +481,12 @@ class TestWorkflowEntrySingleStepRun: return {"question": ["node", "question"]} with ( - patch.object(workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params), + patch.object(workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context), patch.object(workflow_entry, "GraphRuntimeState", return_value=sentinel.graph_runtime_state), patch.object(workflow_entry, "build_dify_run_context", return_value={"_dify": "context"}), patch.object(workflow_entry.time, "perf_counter", return_value=123.0), patch.object(workflow_entry, "resolve_workflow_node_class", return_value=FakeDatasourceNode), - patch.object(workflow_entry, "DifyNodeFactory") as dify_node_factory, + patch.object(workflow_entry.DifyNodeFactory, "from_graph_init_context") as dify_node_factory, patch.object(workflow_entry, "add_node_inputs_to_pool") as add_node_inputs_to_pool, patch.object(workflow_entry, "load_into_variable_pool") as load_into_variable_pool, patch.object( @@ -541,12 +541,12 @@ class TestWorkflowEntrySingleStepRun: return "1" with ( - patch.object(workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params), + patch.object(workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context), patch.object(workflow_entry, "GraphRuntimeState", return_value=sentinel.graph_runtime_state), patch.object(workflow_entry, "build_dify_run_context", return_value={"_dify": "context"}), patch.object(workflow_entry.time, "perf_counter", return_value=123.0), patch.object(workflow_entry, "resolve_workflow_node_class", return_value=FakeNode), - patch.object(workflow_entry, "DifyNodeFactory") as dify_node_factory, + patch.object(workflow_entry.DifyNodeFactory, "from_graph_init_context") as dify_node_factory, patch.object(workflow_entry, "add_node_inputs_to_pool"), patch.object(workflow_entry, "load_into_variable_pool"), patch.object(workflow_entry.WorkflowEntry, "mapping_user_inputs_to_variable_pool"), @@ -651,14 +651,18 @@ class TestWorkflowEntryHelpers: patch.object(workflow_entry, "VariablePool", return_value=sentinel.variable_pool) as variable_pool_cls, patch.object(workflow_entry, "add_variables_to_pool") as add_variables_to_pool, patch.object( - workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params - ) as graph_init_params, + workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context + ) as graph_init_context_cls, patch.object(workflow_entry, "GraphRuntimeState", return_value=sentinel.graph_runtime_state), patch.object( workflow_entry, "build_dify_run_context", return_value={"_dify": "context"} ) as build_dify_run_context, patch.object(workflow_entry.time, "perf_counter", return_value=123.0), - patch.object(workflow_entry, "DifyNodeFactory", return_value=dify_node_factory) as dify_node_factory_cls, + patch.object( + workflow_entry.DifyNodeFactory, + "from_graph_init_context", + return_value=dify_node_factory, + ) as dify_node_factory_cls, patch.object( workflow_entry.WorkflowEntry, "mapping_user_inputs_to_variable_pool", @@ -688,7 +692,7 @@ class TestWorkflowEntryHelpers: user_from=UserFrom.ACCOUNT, invoke_from=InvokeFrom.DEBUGGER, ) - graph_init_params.assert_called_once_with( + graph_init_context_cls.assert_called_once_with( workflow_id="", graph_config=workflow_entry.WorkflowEntry._create_single_node_graph( "node-id", {"type": BuiltinNodeTypes.PARAMETER_EXTRACTOR, "title": "Node"} @@ -697,7 +701,7 @@ class TestWorkflowEntryHelpers: call_depth=0, ) dify_node_factory_cls.assert_called_once_with( - graph_init_params=sentinel.graph_init_params, + graph_init_context=sentinel.graph_init_context, graph_runtime_state=sentinel.graph_runtime_state, ) mapping_user_inputs_to_variable_pool.assert_called_once_with( @@ -734,11 +738,15 @@ class TestWorkflowEntryHelpers: patch.object(workflow_entry, "default_system_variables", return_value=sentinel.system_variables), patch.object(workflow_entry, "VariablePool", return_value=sentinel.variable_pool), patch.object(workflow_entry, "add_variables_to_pool"), - patch.object(workflow_entry, "GraphInitParams", return_value=sentinel.graph_init_params), + patch.object(workflow_entry, "DifyGraphInitContext", return_value=sentinel.graph_init_context), patch.object(workflow_entry, "GraphRuntimeState", return_value=sentinel.graph_runtime_state), patch.object(workflow_entry, "build_dify_run_context", return_value={"_dify": "context"}), patch.object(workflow_entry.time, "perf_counter", return_value=123.0), - patch.object(workflow_entry, "DifyNodeFactory", return_value=dify_node_factory), + patch.object( + workflow_entry.DifyNodeFactory, + "from_graph_init_context", + return_value=dify_node_factory, + ), patch.object( workflow_entry.WorkflowEntry, "mapping_user_inputs_to_variable_pool", diff --git a/api/tests/unit_tests/services/test_workflow_service.py b/api/tests/unit_tests/services/test_workflow_service.py index 1b253eb2f1..76fcb19ab2 100644 --- a/api/tests/unit_tests/services/test_workflow_service.py +++ b/api/tests/unit_tests/services/test_workflow_service.py @@ -2753,9 +2753,9 @@ class TestWorkflowServiceFreeNodeExecution: variable_pool = MagicMock() with ( - patch("services.workflow_service.GraphInitParams") as mock_graph_init_params, + patch("services.workflow_service.DifyGraphInitContext") as mock_graph_init_context_cls, patch("services.workflow_service.GraphRuntimeState"), - patch("services.workflow_service.build_dify_run_context"), + patch("services.workflow_service.build_dify_run_context") as mock_build_dify_run_context, patch("services.workflow_service.DifyHumanInputNodeRuntime") as mock_runtime_cls, patch("services.workflow_service.HumanInputNode") as mock_node_cls, ): @@ -2764,4 +2764,17 @@ class TestWorkflowServiceFreeNodeExecution: ) assert node == mock_node_cls.return_value mock_node_cls.assert_called_once() - mock_runtime_cls.assert_called_once_with(mock_graph_init_params.return_value.run_context) + mock_graph_init_context_cls.assert_called_once_with( + workflow_id="wf-1", + graph_config=workflow.graph_dict, + run_context=mock_build_dify_run_context.return_value, + call_depth=0, + ) + mock_runtime_cls.assert_called_once_with(mock_build_dify_run_context.return_value) + mock_node_cls.assert_called_once_with( + id="n-1", + config=node_config, + graph_init_params=mock_graph_init_context_cls.return_value.to_graph_init_params.return_value, + graph_runtime_state=ANY, + runtime=mock_runtime_cls.return_value, + ) From 1befd2a602c54c5cbfba4e93ecba3a35a53ff104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B9=B3=E8=A1=A1=E4=B8=96=E7=95=8C=E7=9A=84BOY?= <2539888062@qq.com> Date: Thu, 9 Apr 2026 16:01:23 +0800 Subject: [PATCH 4/8] fix(web): resolve Dify compact array types in tool output schema (#34804) --- .../__tests__/output-schema-utils.spec.ts | 17 +++++++++++ .../nodes/tool/output-schema-utils.ts | 30 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/web/app/components/workflow/nodes/tool/__tests__/output-schema-utils.spec.ts b/web/app/components/workflow/nodes/tool/__tests__/output-schema-utils.spec.ts index 4d095ab189..f5179742b2 100644 --- a/web/app/components/workflow/nodes/tool/__tests__/output-schema-utils.spec.ts +++ b/web/app/components/workflow/nodes/tool/__tests__/output-schema-utils.spec.ts @@ -229,6 +229,23 @@ describe('output-schema-utils', () => { }) }) + describe('Dify compact types (workflow-as-tool output_schema)', () => { + it('should resolve array[string] to arrayString (issue #34428)', () => { + const result = resolveVarType({ type: 'array[string]' }) + expect(result.type).toBe(VarType.arrayString) + }) + + it('should resolve Array[string] case-insensitively', () => { + const result = resolveVarType({ type: 'Array[string]' }) + expect(result.type).toBe(VarType.arrayString) + }) + + it('should resolve array[object] to arrayObject', () => { + const result = resolveVarType({ type: 'array[object]' }) + expect(result.type).toBe(VarType.arrayObject) + }) + }) + describe('unknown types', () => { it('should resolve unknown type to any', () => { const result = resolveVarType({ type: 'unknown_type' }) diff --git a/web/app/components/workflow/nodes/tool/output-schema-utils.ts b/web/app/components/workflow/nodes/tool/output-schema-utils.ts index 141c679da0..630673e3e9 100644 --- a/web/app/components/workflow/nodes/tool/output-schema-utils.ts +++ b/web/app/components/workflow/nodes/tool/output-schema-utils.ts @@ -2,6 +2,30 @@ import type { SchemaTypeDefinition } from '@/service/use-common' import { VarType } from '@/app/components/workflow/types' import { getMatchedSchemaType } from '../_base/components/variable/use-match-schema-type' +/** + * Workflow-as-tool and some internal APIs store Dify VarType strings (e.g. `array[string]`) + * in JSON Schema `type` instead of standard `{ type: 'array', items: { type: 'string' } }`. + * Map those compact strings to VarType so downstream (e.g. Code node var picker) does not + * fall back to `any` and get filtered out. + */ +const resolveDifyCompactTypeString = (typeStr: string): VarType | undefined => { + const trimmed = typeStr.trim() + const m = /^array\[(string|number|integer|boolean|object|file|any)\]$/i.exec(trimmed) + if (!m) + return undefined + const inner = m[1].toLowerCase() + const map: Record = { + string: VarType.arrayString, + number: VarType.arrayNumber, + integer: VarType.arrayNumber, + boolean: VarType.arrayBoolean, + object: VarType.arrayObject, + file: VarType.arrayFile, + any: VarType.arrayAny, + } + return map[inner] +} + /** * Normalizes a JSON Schema type to a simple string type. * Handles complex schemas with oneOf, anyOf, allOf. @@ -54,6 +78,12 @@ export const resolveVarType = ( schemaTypeDefinitions?: SchemaTypeDefinition[], ): { type: VarType, schemaType?: string } => { const schemaType = getMatchedSchemaType(schema, schemaTypeDefinitions) + if (schema && typeof schema.type === 'string') { + const compact = resolveDifyCompactTypeString(schema.type) + if (compact !== undefined) + return { type: compact, schemaType } + } + const normalizedType = normalizeJsonSchemaType(schema) switch (normalizedType) { From 03750b76acef2d00478d0b561329287e2e285417 Mon Sep 17 00:00:00 2001 From: Asuka Minato Date: Thu, 9 Apr 2026 17:16:25 +0900 Subject: [PATCH 5/8] ci: bump pyrefly version (#34821) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/pyproject.toml | 2 +- api/uv.lock | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/api/pyproject.toml b/api/pyproject.toml index 2f6581a199..086ce5bb72 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -171,7 +171,7 @@ dev = [ "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", "pytest-xdist>=3.8.0", - "pyrefly>=0.59.1", + "pyrefly>=0.60.0", ] ############################################################ diff --git a/api/uv.lock b/api/uv.lock index b1145eac56..b67646cb71 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1572,7 +1572,7 @@ dev = [ { name = "lxml-stubs", specifier = "~=0.5.1" }, { name = "mypy", specifier = "~=1.20.0" }, { name = "pandas-stubs", specifier = "~=3.0.0" }, - { name = "pyrefly", specifier = ">=0.59.1" }, + { name = "pyrefly", specifier = ">=0.60.0" }, { name = "pytest", specifier = "~=9.0.2" }, { name = "pytest-benchmark", specifier = "~=5.2.3" }, { name = "pytest-cov", specifier = "~=7.1.0" }, @@ -4825,19 +4825,19 @@ wheels = [ [[package]] name = "pyrefly" -version = "0.59.1" +version = "0.60.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d5/ce/7882c2af92b2ff6505fcd3430eff8048ece6c6254cc90bdc76ecee12dfab/pyrefly-0.59.1.tar.gz", hash = "sha256:bf1675b0c38d45df2c8f8618cbdfa261a1b92430d9d31eba16e0282b551e210f", size = 5475432, upload-time = "2026-04-01T22:04:04.11Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/c7/28d14b64888e2d03815627ebff8d57a9f08389c4bbebfe70ae1ed98a1267/pyrefly-0.60.0.tar.gz", hash = "sha256:2499f5b6ff5342e86dfe1cd94bcce133519bbbc93b7ad5636195fea4f0fa3b81", size = 5500389, upload-time = "2026-04-06T19:57:30.643Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/10/04a0e05b08fc855b6fe38c3df549925fc3c2c6e750506870de7335d3e1f7/pyrefly-0.59.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:390db3cd14aa7e0268e847b60cd9ee18b04273eddfa38cf341ed3bb43f3fef2a", size = 12868133, upload-time = "2026-04-01T22:03:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/c7/78/fa7be227c3e3fcacee501c1562278dd026186ffd1b5b5beb51d3941a3aed/pyrefly-0.59.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d246d417b6187c1650d7f855f61c68fbfd6d6155dc846d4e4d273a3e6b5175cb", size = 12379325, upload-time = "2026-04-01T22:03:42.046Z" }, - { url = "https://files.pythonhosted.org/packages/bb/13/6828ce1c98171b5f8388f33c4b0b9ea2ab8c49abe0ef8d793c31e30a05cb/pyrefly-0.59.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:575ac67b04412dc651a7143d27e38a40fbdd3c831c714d5520d0e9d4c8631ab4", size = 35826408, upload-time = "2026-04-01T22:03:45.067Z" }, - { url = "https://files.pythonhosted.org/packages/23/56/79ed8ece9a7ecad0113c394a06a084107db3ad8f1fefe19e7ded43c51245/pyrefly-0.59.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:062e6262ce1064d59dcad81ac0499bb7a3ad501e9bc8a677a50dc630ff0bf862", size = 38532699, upload-time = "2026-04-01T22:03:48.376Z" }, - { url = "https://files.pythonhosted.org/packages/18/7d/ecc025e0f0e3f295b497f523cc19cefaa39e57abede8fc353d29445d174b/pyrefly-0.59.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43ef4247f9e6f734feb93e1f2b75335b943629956e509f545cc9cdcccd76dd20", size = 36743570, upload-time = "2026-04-01T22:03:51.362Z" }, - { url = "https://files.pythonhosted.org/packages/2f/03/b1ce882ebcb87c673165c00451fbe4df17bf96ccfde18c75880dc87c5f5e/pyrefly-0.59.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a2d01723b84d042f4fa6ec871ffd52d0a7e83b0ea791c2e0bb0ff750abce56", size = 41236246, upload-time = "2026-04-01T22:03:54.361Z" }, - { url = "https://files.pythonhosted.org/packages/17/af/5e9c7afd510e7dd64a2204be0ed39e804089cbc4338675a28615c7176acb/pyrefly-0.59.1-py3-none-win32.whl", hash = "sha256:4ea70c780848f8376411e787643ae5d2d09da8a829362332b7b26d15ebcbaf56", size = 11884747, upload-time = "2026-04-01T22:03:56.776Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c1/7db1077627453fd1068f0761f059a9512645c00c4c20acfb9f0c24ac02ec/pyrefly-0.59.1-py3-none-win_amd64.whl", hash = "sha256:67e6a08cfd129a0d2788d5e40a627f9860e0fe91a876238d93d5c63ff4af68ae", size = 12720608, upload-time = "2026-04-01T22:03:59.252Z" }, - { url = "https://files.pythonhosted.org/packages/07/16/4bb6e5fce5a9cf0992932d9435d964c33e507aaaf96fdfbb1be493078a4a/pyrefly-0.59.1-py3-none-win_arm64.whl", hash = "sha256:01179cb215cf079e8223a064f61a074f7079aa97ea705cbbc68af3d6713afd15", size = 12223158, upload-time = "2026-04-01T22:04:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/31/99/6c9984a09220e5eb7dd5c869b7a32d25c3d06b5e8854c6eb679db1145c3e/pyrefly-0.60.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bf1691af0fee69d0c99c3c6e9d26ab6acd3c8afef96416f9ba2e74934833b7b5", size = 12921262, upload-time = "2026-04-06T19:57:00.745Z" }, + { url = "https://files.pythonhosted.org/packages/05/b3/6216aa3c00c88e59a27eb4149851b5affe86eeea6129f4224034a32dddb0/pyrefly-0.60.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e71b70c9b95545cf3b479bc55d1381b531de7b2380eb64411088a1e56b634cb", size = 12424413, upload-time = "2026-04-06T19:57:03.417Z" }, + { url = "https://files.pythonhosted.org/packages/9b/87/eb8dd73abd92a93952ac27a605e463c432fb250fb23186574038c7035594/pyrefly-0.60.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:680ee5f8f98230ea145652d7344708f5375786209c5bf03d8b911fdb0d0d4195", size = 35940884, upload-time = "2026-04-06T19:57:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/0d/34/dc6aeb67b840c745fcee6db358295d554abe6ab555a7eaaf44624bd80bf1/pyrefly-0.60.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d0b20dbbe4aff15b959e8d825b7521a144c4122c11e57022e83b36568c54470", size = 38677220, upload-time = "2026-04-06T19:57:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/66/6b/c863fcf7ef592b7d1db91502acf0d1113be8bed7a2a7143fc6f0dd90616f/pyrefly-0.60.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2911563c8e6b2eaefff68885c94727965469a35375a409235a7a4d2b7157dc15", size = 36907431, upload-time = "2026-04-06T19:57:15.074Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/25ea095ab2ecca8e62884669b11a79f14299db93071685b73a97efbaf4f3/pyrefly-0.60.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0a631d9d04705e303fe156f2e62551611bc7ef8066c34708ceebcfb3088bd55", size = 41447898, upload-time = "2026-04-06T19:57:19.382Z" }, + { url = "https://files.pythonhosted.org/packages/8e/2c/097bdc6e8d40676b28eb03710a4577bc3c7b803cd24693ac02bf15de3d67/pyrefly-0.60.0-py3-none-win32.whl", hash = "sha256:a08d69298da5626cf502d3debbb6944fd13d2f405ea6625363751f1ff570d366", size = 11913434, upload-time = "2026-04-06T19:57:22.887Z" }, + { url = "https://files.pythonhosted.org/packages/0a/d4/8d27fe310e830c8d11ab73db38b93f9fd2e218744b6efb1204401c9a74d5/pyrefly-0.60.0-py3-none-win_amd64.whl", hash = "sha256:56cf30654e708ae1dd635ffefcba4fa4b349dd7004a6ccc5c41e3a9bb944320c", size = 12745033, upload-time = "2026-04-06T19:57:25.517Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ad/8eea1f8fb8209f91f6dbfe48000c9d05fd0cdb1b5b3157283c9b1dada55d/pyrefly-0.60.0-py3-none-win_arm64.whl", hash = "sha256:b6d27fba970f4777063c0227c54167d83bece1804ea34f69e7118e409ba038d2", size = 12246390, upload-time = "2026-04-06T19:57:28.141Z" }, ] [[package]] From d042cbc62e24833026c0d1e8840d7ec99addb65b Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 9 Apr 2026 16:22:09 +0800 Subject: [PATCH 6/8] fix: fix remove_leading_symbols remove [ (#34832) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/tools/utils/text_processing_utils.py | 15 +++++- .../unit_tests/utils/test_text_processing.py | 52 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/api/core/tools/utils/text_processing_utils.py b/api/core/tools/utils/text_processing_utils.py index 4bfaa5e49b..1dd0605f28 100644 --- a/api/core/tools/utils/text_processing_utils.py +++ b/api/core/tools/utils/text_processing_utils.py @@ -19,5 +19,18 @@ def remove_leading_symbols(text: str) -> str: # Match Unicode ranges for punctuation and symbols # FIXME this pattern is confused quick fix for #11868 maybe refactor it later - pattern = r'^[\[\]\u2000-\u2025\u2027-\u206F\u2E00-\u2E7F\u3000-\u300F\u3011-\u303F"#$%&\'()*+,./:;<=>?@^_`~]+' + pattern = re.compile( + r""" + ^ + (?: + [\u2000-\u2025] # General Punctuation: spaces, quotes, dashes + | [\u2027-\u206F] # General Punctuation: ellipsis, underscores, etc. + | [\u2E00-\u2E7F] # Supplemental Punctuation: medieval, ancient marks + | [\u3000-\u300F] # CJK Punctuation: 、。〃「」『》』 (excludes 【】) + | [\u3012-\u303F] # CJK Punctuation: 〖〗〔〕〘〙〚〛〜 etc. + | ["#$%&'()*+,./:;<=>?@^_`~] # ASCII punctuation (excludes []【】) + )+ + """, + re.VERBOSE, + ) return re.sub(pattern, "", text) diff --git a/api/tests/unit_tests/utils/test_text_processing.py b/api/tests/unit_tests/utils/test_text_processing.py index bf61162a66..5f6ccbcdff 100644 --- a/api/tests/unit_tests/utils/test_text_processing.py +++ b/api/tests/unit_tests/utils/test_text_processing.py @@ -19,7 +19,57 @@ from core.tools.utils.text_processing_utils import remove_leading_symbols ("[Google](https://google.com) is a search engine", "[Google](https://google.com) is a search engine"), ("[Example](http://example.com) some text", "[Example](http://example.com) some text"), # Leading symbols before markdown link are removed, including the opening bracket [ - ("@[Test](https://example.com)", "Test](https://example.com)"), + ("@[Test](https://example.com)", "[Test](https://example.com)"), + ("~~标题~~", "标题~~"), + ('""quoted', "quoted"), + ("''test", "test"), + ("##话题", "话题"), + ("$$价格", "价格"), + ("%%百分比", "百分比"), + ("&&与逻辑", "与逻辑"), + ("((括号))", "括号))"), + ("**强调**", "强调**"), + ("++自增", "自增"), + (",,逗号", "逗号"), + ("..省略", "省略"), + ("//注释", "注释"), + ("::范围", "范围"), + (";;分号", "分号"), + ("<<左移", "左移"), + ("==等于", "等于"), + (">>右移", "右移"), + ("??疑问", "疑问"), + ("@@提及", "提及"), + ("^^上标", "上标"), + ("__下划线", "下划线"), + ("``代码", "代码"), + ("~~删除线", "删除线"), + (" 全角空格开头", "全角空格开头"), + ("、顿号开头", "顿号开头"), + ("。句号开头", "句号开头"), + ("「引号」测试", "引号」测试"), + ("『书名号』", "书名号』"), + ("【保留】测试", "【保留】测试"), + ("〖括号〗测试", "括号〗测试"), + ("〔括号〕测试", "括号〕测试"), + ("~~【保留】~~", "【保留】~~"), + ('"[公告]"', '[公告]"'), + ("[公告] 更新", "[公告] 更新"), + ("【通知】重要", "【通知】重要"), + ("[[嵌套]]", "[[嵌套]]"), + ("【【嵌套】】", "【【嵌套】】"), + ("[【混合】]", "[【混合】]"), + ("normal text", "normal text"), + ("123数字", "123数字"), + ("中文开头", "中文开头"), + ("alpha", "alpha"), + ("~", ""), + ("【", "【"), + ("[", "["), + ("~~~", ""), + ("【【【", "【【【"), + ("\t制表符", "\t制表符"), + ("\n换行", "\n换行"), ], ) def test_remove_leading_symbols(input_text, expected_output): From 02c1bfc3e7932884d1b7cdc9b8fab567261c658a Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 9 Apr 2026 16:35:01 +0800 Subject: [PATCH 7/8] chore: install from npm for vinext (#34840) --- pnpm-lock.yaml | 13 ++++++------- pnpm-workspace.yaml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b61ca1b0ee..869b425bb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -514,8 +514,8 @@ catalogs: specifier: 13.0.0 version: 13.0.0 vinext: - specifier: https://pkg.pr.new/vinext@adbf24d - version: 0.0.5 + specifier: 0.0.41 + version: 0.0.41 vite-plugin-inspect: specifier: 12.0.0-beta.1 version: 12.0.0-beta.1 @@ -1150,7 +1150,7 @@ importers: version: 3.19.3 vinext: specifier: 'catalog:' - version: https://pkg.pr.new/vinext@adbf24d(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2) + version: 0.0.41(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2) vite: specifier: npm:@voidzero-dev/vite-plus-core@0.1.16 version: '@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' @@ -8303,9 +8303,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vinext@https://pkg.pr.new/vinext@adbf24d: - resolution: {tarball: https://pkg.pr.new/vinext@adbf24d} - version: 0.0.5 + vinext@0.0.41: + resolution: {integrity: sha512-fpQjNp6cIqjYGH2/kbhN2SdIYHEu79RdlII23SWsY1Qp7LM+je8GfTJH1sxw6dASxPhZKZB/jCmTm5d2/D25zw==} engines: {node: '>=22'} hasBin: true peerDependencies: @@ -16501,7 +16500,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinext@https://pkg.pr.new/vinext@adbf24d(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2): + vinext@0.0.41(@mdx-js/rollup@3.1.1(rollup@4.59.0))(@vitejs/plugin-react@6.0.1(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)))(@vitejs/plugin-rsc@0.5.23(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.5.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.105.4(uglify-js@3.19.3)))(react@19.2.5)(typescript@6.0.2): dependencies: '@unpic/react': 1.0.2(next@16.2.3(@babel/core@7.29.0)(@playwright/test@1.59.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(sass@1.98.0))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@vercel/og': 0.8.6 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f715787766..92c7886245 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -219,7 +219,7 @@ catalog: unist-util-visit: 5.1.0 use-context-selector: 2.0.0 uuid: 13.0.0 - vinext: https://pkg.pr.new/vinext@adbf24d + vinext: 0.0.41 vite: npm:@voidzero-dev/vite-plus-core@0.1.16 vite-plugin-inspect: 12.0.0-beta.1 vite-plus: 0.1.16 From 41eeb1f2e7c490286efbb0304d53cdadacd7210e Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Thu, 9 Apr 2026 18:55:48 +0800 Subject: [PATCH 8/8] fix: fix sqlalchemy.orm.exc.DetachedInstanceError (#34845) --- .../plugin/plugin_auto_upgrade_service.py | 9 ++-- .../test_plugin_auto_upgrade_service.py | 49 +++++++++---------- 2 files changed, 28 insertions(+), 30 deletions(-) diff --git a/api/services/plugin/plugin_auto_upgrade_service.py b/api/services/plugin/plugin_auto_upgrade_service.py index a58bede8db..9bb0ab6ae2 100644 --- a/api/services/plugin/plugin_auto_upgrade_service.py +++ b/api/services/plugin/plugin_auto_upgrade_service.py @@ -1,14 +1,13 @@ from sqlalchemy import select -from sqlalchemy.orm import sessionmaker -from extensions.ext_database import db +from core.db.session_factory import session_factory from models.account import TenantPluginAutoUpgradeStrategy class PluginAutoUpgradeService: @staticmethod def get_strategy(tenant_id: str) -> TenantPluginAutoUpgradeStrategy | None: - with sessionmaker(bind=db.engine).begin() as session: + with session_factory.create_session() as session: return session.scalar( select(TenantPluginAutoUpgradeStrategy) .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) @@ -24,7 +23,7 @@ class PluginAutoUpgradeService: exclude_plugins: list[str], include_plugins: list[str], ) -> bool: - with sessionmaker(bind=db.engine).begin() as session: + with session_factory.create_session() as session: exist_strategy = session.scalar( select(TenantPluginAutoUpgradeStrategy) .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) @@ -51,7 +50,7 @@ class PluginAutoUpgradeService: @staticmethod def exclude_plugin(tenant_id: str, plugin_id: str) -> bool: - with sessionmaker(bind=db.engine).begin() as session: + with session_factory.create_session() as session: exist_strategy = session.scalar( select(TenantPluginAutoUpgradeStrategy) .where(TenantPluginAutoUpgradeStrategy.tenant_id == tenant_id) diff --git a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py index bc2f1c6ecc..021bebceff 100644 --- a/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py +++ b/api/tests/unit_tests/services/plugin/test_plugin_auto_upgrade_service.py @@ -6,23 +6,23 @@ MODULE = "services.plugin.plugin_auto_upgrade_service" def _patched_session(): - """Patch sessionmaker(bind=db.engine).begin() to return a mock session as context manager.""" + """Patch session_factory.create_session() to return a mock session as context manager.""" session = MagicMock() - mock_sessionmaker = MagicMock() - mock_sessionmaker.return_value.begin.return_value.__enter__ = MagicMock(return_value=session) - mock_sessionmaker.return_value.begin.return_value.__exit__ = MagicMock(return_value=False) - patcher = patch(f"{MODULE}.sessionmaker", mock_sessionmaker) - db_patcher = patch(f"{MODULE}.db") - return patcher, db_patcher, session + session.__enter__ = MagicMock(return_value=session) + session.__exit__ = MagicMock(return_value=False) + mock_factory = MagicMock() + mock_factory.create_session.return_value = session + patcher = patch(f"{MODULE}.session_factory", mock_factory) + return patcher, session class TestGetStrategy: def test_returns_strategy_when_found(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() strategy = MagicMock() session.scalar.return_value = strategy - with p1, p2: + with p1: from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService result = PluginAutoUpgradeService.get_strategy("t1") @@ -30,10 +30,10 @@ class TestGetStrategy: assert result is strategy def test_returns_none_when_not_found(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() session.scalar.return_value = None - with p1, p2: + with p1: from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService result = PluginAutoUpgradeService.get_strategy("t1") @@ -43,10 +43,10 @@ class TestGetStrategy: class TestChangeStrategy: def test_creates_new_strategy(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() session.scalar.return_value = None - with p1, p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: strat_cls.return_value = MagicMock() from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService @@ -63,11 +63,11 @@ class TestChangeStrategy: session.add.assert_called_once() def test_updates_existing_strategy(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() existing = MagicMock() session.scalar.return_value = existing - with p1, p2: + with p1: from services.plugin.plugin_auto_upgrade_service import PluginAutoUpgradeService result = PluginAutoUpgradeService.change_strategy( @@ -89,12 +89,11 @@ class TestChangeStrategy: class TestExcludePlugin: def test_creates_default_strategy_when_none_exists(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() session.scalar.return_value = None with ( p1, - p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls, patch(f"{MODULE}.PluginAutoUpgradeService.change_strategy") as cs, @@ -110,13 +109,13 @@ class TestExcludePlugin: cs.assert_called_once() def test_appends_to_exclude_list_in_exclude_mode(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() existing = MagicMock() existing.upgrade_mode = "exclude" existing.exclude_plugins = ["p-existing"] session.scalar.return_value = existing - with p1, p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: strat_cls.UpgradeMode.EXCLUDE = "exclude" strat_cls.UpgradeMode.PARTIAL = "partial" strat_cls.UpgradeMode.ALL = "all" @@ -128,13 +127,13 @@ class TestExcludePlugin: assert existing.exclude_plugins == ["p-existing", "p-new"] def test_removes_from_include_list_in_partial_mode(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() existing = MagicMock() existing.upgrade_mode = "partial" existing.include_plugins = ["p1", "p2"] session.scalar.return_value = existing - with p1, p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: strat_cls.UpgradeMode.EXCLUDE = "exclude" strat_cls.UpgradeMode.PARTIAL = "partial" strat_cls.UpgradeMode.ALL = "all" @@ -146,12 +145,12 @@ class TestExcludePlugin: assert existing.include_plugins == ["p2"] def test_switches_to_exclude_mode_from_all(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() existing = MagicMock() existing.upgrade_mode = "all" session.scalar.return_value = existing - with p1, p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: strat_cls.UpgradeMode.EXCLUDE = "exclude" strat_cls.UpgradeMode.PARTIAL = "partial" strat_cls.UpgradeMode.ALL = "all" @@ -164,13 +163,13 @@ class TestExcludePlugin: assert existing.exclude_plugins == ["p1"] def test_no_duplicate_in_exclude_list(self): - p1, p2, session = _patched_session() + p1, session = _patched_session() existing = MagicMock() existing.upgrade_mode = "exclude" existing.exclude_plugins = ["p1"] session.scalar.return_value = existing - with p1, p2, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: + with p1, patch(f"{MODULE}.select"), patch(f"{MODULE}.TenantPluginAutoUpgradeStrategy") as strat_cls: strat_cls.UpgradeMode.EXCLUDE = "exclude" strat_cls.UpgradeMode.PARTIAL = "partial" strat_cls.UpgradeMode.ALL = "all"