From dcde854c5e5dbad1347424b8a959825d1300e63b Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 24 Dec 2025 14:45:33 +0800 Subject: [PATCH 01/17] chore: some tests (#30078) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../app/log-annotation/index.spec.tsx | 176 ++++++++++++ web/app/components/base/input/index.tsx | 6 +- .../billing/priority-label/index.spec.tsx | 125 ++++++++ .../explore/app-list/index.spec.tsx | 271 ++++++++++++++++++ web/app/components/explore/category.spec.tsx | 79 +++++ web/app/components/explore/index.spec.tsx | 140 +++++++++ .../explore/item-operation/index.spec.tsx | 109 +++++++ .../explore/item-operation/index.tsx | 6 +- .../sidebar/app-nav-item/index.spec.tsx | 99 +++++++ .../components/explore/sidebar/index.spec.tsx | 164 +++++++++++ 10 files changed, 1173 insertions(+), 2 deletions(-) create mode 100644 web/app/components/app/log-annotation/index.spec.tsx create mode 100644 web/app/components/billing/priority-label/index.spec.tsx create mode 100644 web/app/components/explore/app-list/index.spec.tsx create mode 100644 web/app/components/explore/category.spec.tsx create mode 100644 web/app/components/explore/index.spec.tsx create mode 100644 web/app/components/explore/item-operation/index.spec.tsx create mode 100644 web/app/components/explore/sidebar/app-nav-item/index.spec.tsx create mode 100644 web/app/components/explore/sidebar/index.spec.tsx diff --git a/web/app/components/app/log-annotation/index.spec.tsx b/web/app/components/app/log-annotation/index.spec.tsx new file mode 100644 index 00000000000..064092f20ef --- /dev/null +++ b/web/app/components/app/log-annotation/index.spec.tsx @@ -0,0 +1,176 @@ +import type { App, AppIconType } from '@/types/app' +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { useStore as useAppStore } from '@/app/components/app/store' +import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type' +import { AppModeEnum } from '@/types/app' +import LogAnnotation from './index' + +const mockRouterPush = vi.fn() +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: mockRouterPush, + }), +})) + +vi.mock('@/app/components/app/annotation', () => ({ + __esModule: true, + default: ({ appDetail }: { appDetail: App }) => ( +
+ ), +})) + +vi.mock('@/app/components/app/log', () => ({ + __esModule: true, + default: ({ appDetail }: { appDetail: App }) => ( +
+ ), +})) + +vi.mock('@/app/components/app/workflow-log', () => ({ + __esModule: true, + default: ({ appDetail }: { appDetail: App }) => ( +
+ ), +})) + +const createMockApp = (overrides: Partial = {}): App => ({ + id: 'app-123', + name: 'Test App', + description: 'Test app description', + author_name: 'Test Author', + icon_type: 'emoji' as AppIconType, + icon: ':icon:', + icon_background: '#FFEAD5', + icon_url: null, + use_icon_as_answer_icon: false, + mode: AppModeEnum.CHAT, + enable_site: true, + enable_api: true, + api_rpm: 60, + api_rph: 3600, + is_demo: false, + model_config: {} as App['model_config'], + app_model_config: {} as App['app_model_config'], + created_at: Date.now(), + updated_at: Date.now(), + site: { + access_token: 'token', + app_base_url: 'https://example.com', + } as App['site'], + api_base_url: 'https://api.example.com', + tags: [], + access_mode: 'public_access' as App['access_mode'], + ...overrides, +}) + +describe('LogAnnotation', () => { + beforeEach(() => { + vi.clearAllMocks() + useAppStore.setState({ appDetail: createMockApp() }) + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render loading state when app detail is missing', () => { + // Arrange + useAppStore.setState({ appDetail: undefined }) + + // Act + render() + + // Assert + expect(screen.getByRole('status')).toBeInTheDocument() + }) + + it('should render log and annotation tabs for non-completion apps', () => { + // Arrange + useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) }) + + // Act + render() + + // Assert + expect(screen.getByText('appLog.title')).toBeInTheDocument() + expect(screen.getByText('appAnnotation.title')).toBeInTheDocument() + }) + + it('should render only log tab for completion apps', () => { + // Arrange + useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.COMPLETION }) }) + + // Act + render() + + // Assert + expect(screen.getByText('appLog.title')).toBeInTheDocument() + expect(screen.queryByText('appAnnotation.title')).not.toBeInTheDocument() + }) + + it('should hide tabs and render workflow log in workflow mode', () => { + // Arrange + useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.WORKFLOW }) }) + + // Act + render() + + // Assert + expect(screen.queryByText('appLog.title')).not.toBeInTheDocument() + expect(screen.getByTestId('workflow-log')).toBeInTheDocument() + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should render log content when page type is log', () => { + // Arrange + useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) }) + + // Act + render() + + // Assert + expect(screen.getByTestId('log')).toBeInTheDocument() + expect(screen.queryByTestId('annotation')).not.toBeInTheDocument() + }) + + it('should render annotation content when page type is annotation', () => { + // Arrange + useAppStore.setState({ appDetail: createMockApp({ mode: AppModeEnum.CHAT }) }) + + // Act + render() + + // Assert + expect(screen.getByTestId('annotation')).toBeInTheDocument() + expect(screen.queryByTestId('log')).not.toBeInTheDocument() + }) + }) + + // User interaction behavior + describe('User Interactions', () => { + it('should navigate to annotations when switching from log tab', async () => { + // Arrange + const user = userEvent.setup() + + // Act + render() + await user.click(screen.getByText('appAnnotation.title')) + + // Assert + expect(mockRouterPush).toHaveBeenCalledWith('/app/app-123/annotations') + }) + + it('should navigate to logs when switching from annotation tab', async () => { + // Arrange + const user = userEvent.setup() + + // Act + render() + await user.click(screen.getByText('appLog.title')) + + // Assert + expect(mockRouterPush).toHaveBeenCalledWith('/app/app-123/logs') + }) + }) +}) diff --git a/web/app/components/base/input/index.tsx b/web/app/components/base/input/index.tsx index 98529a26bc8..6c6a0c6a750 100644 --- a/web/app/components/base/input/index.tsx +++ b/web/app/components/base/input/index.tsx @@ -110,7 +110,11 @@ const Input = React.forwardRef(({ {...props} /> {showClearIcon && value && !disabled && !destructive && ( -
+
)} diff --git a/web/app/components/billing/priority-label/index.spec.tsx b/web/app/components/billing/priority-label/index.spec.tsx new file mode 100644 index 00000000000..0d176d1611a --- /dev/null +++ b/web/app/components/billing/priority-label/index.spec.tsx @@ -0,0 +1,125 @@ +import type { Mock } from 'vitest' +import { fireEvent, render, screen } from '@testing-library/react' +import { createMockPlan } from '@/__mocks__/provider-context' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '../type' +import PriorityLabel from './index' + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: vi.fn(), +})) + +const useProviderContextMock = useProviderContext as Mock + +const setupPlan = (planType: Plan) => { + useProviderContextMock.mockReturnValue(createMockPlan(planType)) +} + +describe('PriorityLabel', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Rendering: basic label output for sandbox plan. + describe('Rendering', () => { + it('should render the standard priority label when plan is sandbox', () => { + // Arrange + setupPlan(Plan.sandbox) + + // Act + render() + + // Assert + expect(screen.getByText('billing.plansCommon.priority.standard')).toBeInTheDocument() + }) + }) + + // Props: custom class name applied to the label container. + describe('Props', () => { + it('should apply custom className to the label container', () => { + // Arrange + setupPlan(Plan.sandbox) + + // Act + render() + + // Assert + const label = screen.getByText('billing.plansCommon.priority.standard').closest('div') + expect(label).toHaveClass('custom-class') + }) + }) + + // Plan types: label text and icon visibility for different plans. + describe('Plan Types', () => { + it('should render priority label and icon when plan is professional', () => { + // Arrange + setupPlan(Plan.professional) + + // Act + const { container } = render() + + // Assert + expect(screen.getByText('billing.plansCommon.priority.priority')).toBeInTheDocument() + expect(container.querySelector('svg')).toBeInTheDocument() + }) + + it('should render top priority label and icon when plan is team', () => { + // Arrange + setupPlan(Plan.team) + + // Act + const { container } = render() + + // Assert + expect(screen.getByText('billing.plansCommon.priority.top-priority')).toBeInTheDocument() + expect(container.querySelector('svg')).toBeInTheDocument() + }) + + it('should render standard label without icon when plan is sandbox', () => { + // Arrange + setupPlan(Plan.sandbox) + + // Act + const { container } = render() + + // Assert + expect(screen.getByText('billing.plansCommon.priority.standard')).toBeInTheDocument() + expect(container.querySelector('svg')).not.toBeInTheDocument() + }) + }) + + // Edge cases: tooltip content varies by priority level. + describe('Edge Cases', () => { + it('should show the tip text when priority is not top priority', async () => { + // Arrange + setupPlan(Plan.sandbox) + + // Act + render() + const label = screen.getByText('billing.plansCommon.priority.standard').closest('div') + fireEvent.mouseEnter(label as HTMLElement) + + // Assert + expect(await screen.findByText( + 'billing.plansCommon.documentProcessingPriority: billing.plansCommon.priority.standard', + )).toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.documentProcessingPriorityTip')).toBeInTheDocument() + }) + + it('should hide the tip text when priority is top priority', async () => { + // Arrange + setupPlan(Plan.enterprise) + + // Act + render() + const label = screen.getByText('billing.plansCommon.priority.top-priority').closest('div') + fireEvent.mouseEnter(label as HTMLElement) + + // Assert + expect(await screen.findByText( + 'billing.plansCommon.documentProcessingPriority: billing.plansCommon.priority.top-priority', + )).toBeInTheDocument() + expect(screen.queryByText('billing.plansCommon.documentProcessingPriorityTip')).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/explore/app-list/index.spec.tsx b/web/app/components/explore/app-list/index.spec.tsx new file mode 100644 index 00000000000..e73fcdf0ad5 --- /dev/null +++ b/web/app/components/explore/app-list/index.spec.tsx @@ -0,0 +1,271 @@ +import type { Mock } from 'vitest' +import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' +import type { App } from '@/models/explore' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import ExploreContext from '@/context/explore-context' +import { fetchAppDetail } from '@/service/explore' +import { AppModeEnum } from '@/types/app' +import AppList from './index' + +const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}' +let mockTabValue = allCategoriesEn +const mockSetTab = vi.fn() +let mockSWRData: { categories: string[], allList: App[] } = { categories: [], allList: [] } +const mockHandleImportDSL = vi.fn() +const mockHandleImportDSLConfirm = vi.fn() + +vi.mock('@/hooks/use-tab-searchparams', () => ({ + useTabSearchParams: () => [mockTabValue, mockSetTab], +})) + +vi.mock('ahooks', async () => { + const actual = await vi.importActual('ahooks') + const React = await vi.importActual('react') + return { + ...actual, + useDebounceFn: (fn: (...args: unknown[]) => void) => { + const fnRef = React.useRef(fn) + fnRef.current = fn + return { + run: () => setTimeout(() => fnRef.current(), 0), + } + }, + } +}) + +vi.mock('swr', () => ({ + __esModule: true, + default: () => ({ data: mockSWRData }), +})) + +vi.mock('@/service/explore', () => ({ + fetchAppDetail: vi.fn(), + fetchAppList: vi.fn(), +})) + +vi.mock('@/hooks/use-import-dsl', () => ({ + useImportDSL: () => ({ + handleImportDSL: mockHandleImportDSL, + handleImportDSLConfirm: mockHandleImportDSLConfirm, + versions: ['v1'], + isFetching: false, + }), +})) + +vi.mock('@/app/components/explore/create-app-modal', () => ({ + __esModule: true, + default: (props: CreateAppModalProps) => { + if (!props.show) + return null + return ( +
+ + +
+ ) + }, +})) + +vi.mock('@/app/components/app/create-from-dsl-modal/dsl-confirm-modal', () => ({ + __esModule: true, + default: ({ onConfirm, onCancel }: { onConfirm: () => void, onCancel: () => void }) => ( +
+ + +
+ ), +})) + +const createApp = (overrides: Partial = {}): App => ({ + app: { + id: overrides.app?.id ?? 'app-basic-id', + mode: overrides.app?.mode ?? AppModeEnum.CHAT, + icon_type: overrides.app?.icon_type ?? 'emoji', + icon: overrides.app?.icon ?? '😀', + icon_background: overrides.app?.icon_background ?? '#fff', + icon_url: overrides.app?.icon_url ?? '', + name: overrides.app?.name ?? 'Alpha', + description: overrides.app?.description ?? 'Alpha description', + use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false, + }, + app_id: overrides.app_id ?? 'app-1', + description: overrides.description ?? 'Alpha description', + copyright: overrides.copyright ?? '', + privacy_policy: overrides.privacy_policy ?? null, + custom_disclaimer: overrides.custom_disclaimer ?? null, + category: overrides.category ?? 'Writing', + position: overrides.position ?? 1, + is_listed: overrides.is_listed ?? true, + install_count: overrides.install_count ?? 0, + installed: overrides.installed ?? false, + editable: overrides.editable ?? false, + is_agent: overrides.is_agent ?? false, +}) + +const renderWithContext = (hasEditPermission = false, onSuccess?: () => void) => { + return render( + + + , + ) +} + +describe('AppList', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTabValue = allCategoriesEn + mockSWRData = { categories: [], allList: [] } + }) + + // Rendering: show loading when categories are not ready. + describe('Rendering', () => { + it('should render loading when categories are empty', () => { + // Arrange + mockSWRData = { categories: [], allList: [] } + + // Act + renderWithContext() + + // Assert + expect(screen.getByRole('status')).toBeInTheDocument() + }) + + it('should render app cards when data is available', () => { + // Arrange + mockSWRData = { + categories: ['Writing', 'Translate'], + allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })], + } + + // Act + renderWithContext() + + // Assert + expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByText('Beta')).toBeInTheDocument() + }) + }) + + // Props: category selection filters the list. + describe('Props', () => { + it('should filter apps by selected category', () => { + // Arrange + mockTabValue = 'Writing' + mockSWRData = { + categories: ['Writing', 'Translate'], + allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })], + } + + // Act + renderWithContext() + + // Assert + expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.queryByText('Beta')).not.toBeInTheDocument() + }) + }) + + // User interactions: search and create flow. + describe('User Interactions', () => { + it('should filter apps by search keywords', async () => { + // Arrange + mockSWRData = { + categories: ['Writing'], + allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })], + } + renderWithContext() + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + fireEvent.change(input, { target: { value: 'gam' } }) + + // Assert + await waitFor(() => { + expect(screen.queryByText('Alpha')).not.toBeInTheDocument() + expect(screen.getByText('Gamma')).toBeInTheDocument() + }) + }) + + it('should handle create flow and confirm DSL when pending', async () => { + // Arrange + const onSuccess = vi.fn() + mockSWRData = { + categories: ['Writing'], + allList: [createApp()], + }; + (fetchAppDetail as unknown as Mock).mockResolvedValue({ export_data: 'yaml-content' }) + mockHandleImportDSL.mockImplementation(async (_payload: unknown, options: { onSuccess?: () => void, onPending?: () => void }) => { + options.onPending?.() + }) + mockHandleImportDSLConfirm.mockImplementation(async (options: { onSuccess?: () => void }) => { + options.onSuccess?.() + }) + + // Act + renderWithContext(true, onSuccess) + fireEvent.click(screen.getByText('explore.appCard.addToWorkspace')) + fireEvent.click(await screen.findByTestId('confirm-create')) + + // Assert + await waitFor(() => { + expect(fetchAppDetail).toHaveBeenCalledWith('app-basic-id') + }) + expect(mockHandleImportDSL).toHaveBeenCalledTimes(1) + expect(await screen.findByTestId('dsl-confirm-modal')).toBeInTheDocument() + + fireEvent.click(screen.getByTestId('dsl-confirm')) + await waitFor(() => { + expect(mockHandleImportDSLConfirm).toHaveBeenCalledTimes(1) + expect(onSuccess).toHaveBeenCalledTimes(1) + }) + }) + }) + + // Edge cases: handle clearing search keywords. + describe('Edge Cases', () => { + it('should reset search results when clear icon is clicked', async () => { + // Arrange + mockSWRData = { + categories: ['Writing'], + allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })], + } + renderWithContext() + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + fireEvent.change(input, { target: { value: 'gam' } }) + await waitFor(() => { + expect(screen.queryByText('Alpha')).not.toBeInTheDocument() + }) + + fireEvent.click(screen.getByTestId('input-clear')) + + // Assert + await waitFor(() => { + expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByText('Gamma')).toBeInTheDocument() + }) + }) + }) +}) diff --git a/web/app/components/explore/category.spec.tsx b/web/app/components/explore/category.spec.tsx new file mode 100644 index 00000000000..a84b17c844e --- /dev/null +++ b/web/app/components/explore/category.spec.tsx @@ -0,0 +1,79 @@ +import type { AppCategory } from '@/models/explore' +import { fireEvent, render, screen } from '@testing-library/react' +import Category from './category' + +describe('Category', () => { + const allCategoriesEn = 'Recommended' + + const renderComponent = (overrides: Partial> = {}) => { + const props: React.ComponentProps = { + list: ['Writing', 'Recommended'] as AppCategory[], + value: allCategoriesEn, + onChange: vi.fn(), + allCategoriesEn, + ...overrides, + } + return { + props, + ...render(), + } + } + + // Rendering: basic categories and all-categories button. + describe('Rendering', () => { + it('should render all categories item and translated categories', () => { + // Arrange + renderComponent() + + // Assert + expect(screen.getByText('explore.apps.allCategories')).toBeInTheDocument() + expect(screen.getByText('explore.category.Writing')).toBeInTheDocument() + }) + + it('should not render allCategoriesEn again inside the category list', () => { + // Arrange + renderComponent() + + // Assert + const recommendedItems = screen.getAllByText('explore.apps.allCategories') + expect(recommendedItems).toHaveLength(1) + }) + }) + + // Props: clicking items triggers onChange. + describe('Props', () => { + it('should call onChange with category value when category item is clicked', () => { + // Arrange + const { props } = renderComponent() + + // Act + fireEvent.click(screen.getByText('explore.category.Writing')) + + // Assert + expect(props.onChange).toHaveBeenCalledWith('Writing') + }) + + it('should call onChange with allCategoriesEn when all categories is clicked', () => { + // Arrange + const { props } = renderComponent({ value: 'Writing' }) + + // Act + fireEvent.click(screen.getByText('explore.apps.allCategories')) + + // Assert + expect(props.onChange).toHaveBeenCalledWith(allCategoriesEn) + }) + }) + + // Edge cases: handle values not in the list. + describe('Edge Cases', () => { + it('should treat unknown value as all categories selection', () => { + // Arrange + renderComponent({ value: 'Unknown' }) + + // Assert + const allCategoriesItem = screen.getByText('explore.apps.allCategories') + expect(allCategoriesItem.className).toContain('bg-components-main-nav-nav-button-bg-active') + }) + }) +}) diff --git a/web/app/components/explore/index.spec.tsx b/web/app/components/explore/index.spec.tsx new file mode 100644 index 00000000000..8f361ad4717 --- /dev/null +++ b/web/app/components/explore/index.spec.tsx @@ -0,0 +1,140 @@ +import type { Mock } from 'vitest' +import { render, screen, waitFor } from '@testing-library/react' +import { useContext } from 'use-context-selector' +import { useAppContext } from '@/context/app-context' +import ExploreContext from '@/context/explore-context' +import { MediaType } from '@/hooks/use-breakpoints' +import useDocumentTitle from '@/hooks/use-document-title' +import { useMembers } from '@/service/use-common' +import Explore from './index' + +const mockReplace = vi.fn() +const mockPush = vi.fn() +const mockInstalledAppsData = { installed_apps: [] as const } + +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + replace: mockReplace, + push: mockPush, + }), + useSelectedLayoutSegments: () => ['apps'], +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + __esModule: true, + default: () => MediaType.pc, + MediaType: { + mobile: 'mobile', + tablet: 'tablet', + pc: 'pc', + }, +})) + +vi.mock('@/service/use-explore', () => ({ + useGetInstalledApps: () => ({ + isFetching: false, + data: mockInstalledAppsData, + refetch: vi.fn(), + }), + useUninstallApp: () => ({ + mutateAsync: vi.fn(), + }), + useUpdateAppPinStatus: () => ({ + mutateAsync: vi.fn(), + }), +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: vi.fn(), +})) + +vi.mock('@/service/use-common', () => ({ + useMembers: vi.fn(), +})) + +vi.mock('@/hooks/use-document-title', () => ({ + __esModule: true, + default: vi.fn(), +})) + +const ContextReader = () => { + const { hasEditPermission } = useContext(ExploreContext) + return
{hasEditPermission ? 'edit-yes' : 'edit-no'}
+} + +describe('Explore', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Rendering: provides ExploreContext and children. + describe('Rendering', () => { + it('should render children and provide edit permission from members role', async () => { + // Arrange + ; (useAppContext as Mock).mockReturnValue({ + userProfile: { id: 'user-1' }, + isCurrentWorkspaceDatasetOperator: false, + }); + (useMembers as Mock).mockReturnValue({ + data: { + accounts: [{ id: 'user-1', role: 'admin' }], + }, + }) + + // Act + render(( + + + + )) + + // Assert + await waitFor(() => { + expect(screen.getByText('edit-yes')).toBeInTheDocument() + }) + }) + }) + + // Effects: set document title and redirect dataset operators. + describe('Effects', () => { + it('should set document title on render', () => { + // Arrange + ; (useAppContext as Mock).mockReturnValue({ + userProfile: { id: 'user-1' }, + isCurrentWorkspaceDatasetOperator: false, + }); + (useMembers as Mock).mockReturnValue({ data: { accounts: [] } }) + + // Act + render(( + +
child
+
+ )) + + // Assert + expect(useDocumentTitle).toHaveBeenCalledWith('common.menus.explore') + }) + + it('should redirect dataset operators to /datasets', async () => { + // Arrange + ; (useAppContext as Mock).mockReturnValue({ + userProfile: { id: 'user-1' }, + isCurrentWorkspaceDatasetOperator: true, + }); + (useMembers as Mock).mockReturnValue({ data: { accounts: [] } }) + + // Act + render(( + +
child
+
+ )) + + // Assert + await waitFor(() => { + expect(mockReplace).toHaveBeenCalledWith('/datasets') + }) + }) + }) +}) diff --git a/web/app/components/explore/item-operation/index.spec.tsx b/web/app/components/explore/item-operation/index.spec.tsx new file mode 100644 index 00000000000..9084e5564e0 --- /dev/null +++ b/web/app/components/explore/item-operation/index.spec.tsx @@ -0,0 +1,109 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import ItemOperation from './index' + +describe('ItemOperation', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + const renderComponent = (overrides: Partial> = {}) => { + const props: React.ComponentProps = { + isPinned: false, + isShowDelete: true, + togglePin: vi.fn(), + onDelete: vi.fn(), + ...overrides, + } + return { + props, + ...render(), + } + } + + // Rendering: menu items show after opening. + describe('Rendering', () => { + it('should render pin and delete actions when menu is open', async () => { + // Arrange + renderComponent() + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + + // Assert + expect(await screen.findByText('explore.sidebar.action.pin')).toBeInTheDocument() + expect(screen.getByText('explore.sidebar.action.delete')).toBeInTheDocument() + }) + }) + + // Props: render optional rename action and pinned label text. + describe('Props', () => { + it('should render rename action when isShowRenameConversation is true', async () => { + // Arrange + renderComponent({ isShowRenameConversation: true }) + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + + // Assert + expect(await screen.findByText('explore.sidebar.action.rename')).toBeInTheDocument() + }) + + it('should render unpin label when isPinned is true', async () => { + // Arrange + renderComponent({ isPinned: true }) + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + + // Assert + expect(await screen.findByText('explore.sidebar.action.unpin')).toBeInTheDocument() + }) + }) + + // User interactions: clicking action items triggers callbacks. + describe('User Interactions', () => { + it('should call togglePin when clicking pin action', async () => { + // Arrange + const { props } = renderComponent() + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + fireEvent.click(await screen.findByText('explore.sidebar.action.pin')) + + // Assert + expect(props.togglePin).toHaveBeenCalledTimes(1) + }) + + it('should call onDelete when clicking delete action', async () => { + // Arrange + const { props } = renderComponent() + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + fireEvent.click(await screen.findByText('explore.sidebar.action.delete')) + + // Assert + expect(props.onDelete).toHaveBeenCalledTimes(1) + }) + }) + + // Edge cases: menu closes after mouse leave when no hovering state remains. + describe('Edge Cases', () => { + it('should close the menu when mouse leaves the panel and item is not hovering', async () => { + // Arrange + renderComponent() + fireEvent.click(screen.getByTestId('item-operation-trigger')) + const pinText = await screen.findByText('explore.sidebar.action.pin') + const menu = pinText.closest('div')?.parentElement as HTMLElement + + // Act + fireEvent.mouseEnter(menu) + fireEvent.mouseLeave(menu) + + // Assert + await waitFor(() => { + expect(screen.queryByText('explore.sidebar.action.pin')).not.toBeInTheDocument() + }) + }) + }) +}) diff --git a/web/app/components/explore/item-operation/index.tsx b/web/app/components/explore/item-operation/index.tsx index 3703c0d4c09..abbfdd09bd4 100644 --- a/web/app/components/explore/item-operation/index.tsx +++ b/web/app/components/explore/item-operation/index.tsx @@ -53,7 +53,11 @@ const ItemOperation: FC = ({ setOpen(v => !v)} > -
+
+
({ + useRouter: () => ({ + push: mockPush, + }), +})) + +vi.mock('ahooks', async () => { + const actual = await vi.importActual('ahooks') + return { + ...actual, + useHover: () => false, + } +}) + +const baseProps = { + isMobile: false, + name: 'My App', + id: 'app-123', + icon_type: 'emoji' as const, + icon: '🤖', + icon_background: '#fff', + icon_url: '', + isSelected: false, + isPinned: false, + togglePin: vi.fn(), + uninstallable: false, + onDelete: vi.fn(), +} + +describe('AppNavItem', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Rendering: display app name for desktop and hide for mobile. + describe('Rendering', () => { + it('should render name and item operation on desktop', () => { + // Arrange + render() + + // Assert + expect(screen.getByText('My App')).toBeInTheDocument() + expect(screen.getByTestId('item-operation-trigger')).toBeInTheDocument() + }) + + it('should hide name on mobile', () => { + // Arrange + render() + + // Assert + expect(screen.queryByText('My App')).not.toBeInTheDocument() + }) + }) + + // User interactions: navigation and delete flow. + describe('User Interactions', () => { + it('should navigate to installed app when item is clicked', () => { + // Arrange + render() + + // Act + fireEvent.click(screen.getByText('My App')) + + // Assert + expect(mockPush).toHaveBeenCalledWith('/explore/installed/app-123') + }) + + it('should call onDelete with app id when delete action is clicked', async () => { + // Arrange + render() + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + fireEvent.click(await screen.findByText('explore.sidebar.action.delete')) + + // Assert + expect(baseProps.onDelete).toHaveBeenCalledWith('app-123') + }) + }) + + // Edge cases: hide delete when uninstallable or selected. + describe('Edge Cases', () => { + it('should not render delete action when app is uninstallable', () => { + // Arrange + render() + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + + // Assert + expect(screen.queryByText('explore.sidebar.action.delete')).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/explore/sidebar/index.spec.tsx b/web/app/components/explore/sidebar/index.spec.tsx new file mode 100644 index 00000000000..0cbd05aa08b --- /dev/null +++ b/web/app/components/explore/sidebar/index.spec.tsx @@ -0,0 +1,164 @@ +import type { InstalledApp } from '@/models/explore' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import Toast from '@/app/components/base/toast' +import ExploreContext from '@/context/explore-context' +import { MediaType } from '@/hooks/use-breakpoints' +import { AppModeEnum } from '@/types/app' +import SideBar from './index' + +const mockSegments = ['apps'] +const mockPush = vi.fn() +const mockRefetch = vi.fn() +const mockUninstall = vi.fn() +const mockUpdatePinStatus = vi.fn() +let mockIsFetching = false +let mockInstalledApps: InstalledApp[] = [] + +vi.mock('next/navigation', () => ({ + useSelectedLayoutSegments: () => mockSegments, + useRouter: () => ({ + push: mockPush, + }), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + __esModule: true, + default: () => MediaType.pc, + MediaType: { + mobile: 'mobile', + tablet: 'tablet', + pc: 'pc', + }, +})) + +vi.mock('@/service/use-explore', () => ({ + useGetInstalledApps: () => ({ + isFetching: mockIsFetching, + data: { installed_apps: mockInstalledApps }, + refetch: mockRefetch, + }), + useUninstallApp: () => ({ + mutateAsync: mockUninstall, + }), + useUpdateAppPinStatus: () => ({ + mutateAsync: mockUpdatePinStatus, + }), +})) + +const createInstalledApp = (overrides: Partial = {}): InstalledApp => ({ + id: overrides.id ?? 'app-123', + uninstallable: overrides.uninstallable ?? false, + is_pinned: overrides.is_pinned ?? false, + app: { + id: overrides.app?.id ?? 'app-basic-id', + mode: overrides.app?.mode ?? AppModeEnum.CHAT, + icon_type: overrides.app?.icon_type ?? 'emoji', + icon: overrides.app?.icon ?? '🤖', + icon_background: overrides.app?.icon_background ?? '#fff', + icon_url: overrides.app?.icon_url ?? '', + name: overrides.app?.name ?? 'My App', + description: overrides.app?.description ?? 'desc', + use_icon_as_answer_icon: overrides.app?.use_icon_as_answer_icon ?? false, + }, +}) + +const renderWithContext = (installedApps: InstalledApp[] = []) => { + return render( + + + , + ) +} + +describe('SideBar', () => { + beforeEach(() => { + vi.clearAllMocks() + mockIsFetching = false + mockInstalledApps = [] + vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() })) + }) + + // Rendering: show discovery and workspace section. + describe('Rendering', () => { + it('should render workspace items when installed apps exist', () => { + // Arrange + mockInstalledApps = [createInstalledApp()] + + // Act + renderWithContext(mockInstalledApps) + + // Assert + expect(screen.getByText('explore.sidebar.discovery')).toBeInTheDocument() + expect(screen.getByText('explore.sidebar.workspace')).toBeInTheDocument() + expect(screen.getByText('My App')).toBeInTheDocument() + }) + }) + + // Effects: refresh and sync installed apps state. + describe('Effects', () => { + it('should refetch installed apps on mount', () => { + // Arrange + mockInstalledApps = [createInstalledApp()] + + // Act + renderWithContext(mockInstalledApps) + + // Assert + expect(mockRefetch).toHaveBeenCalledTimes(1) + }) + }) + + // User interactions: delete and pin flows. + describe('User Interactions', () => { + it('should uninstall app and show toast when delete is confirmed', async () => { + // Arrange + mockInstalledApps = [createInstalledApp()] + mockUninstall.mockResolvedValue(undefined) + renderWithContext(mockInstalledApps) + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + fireEvent.click(await screen.findByText('explore.sidebar.action.delete')) + fireEvent.click(await screen.findByText('common.operation.confirm')) + + // Assert + await waitFor(() => { + expect(mockUninstall).toHaveBeenCalledWith('app-123') + expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({ + type: 'success', + message: 'common.api.remove', + })) + }) + }) + + it('should update pin status and show toast when pin is clicked', async () => { + // Arrange + mockInstalledApps = [createInstalledApp({ is_pinned: false })] + mockUpdatePinStatus.mockResolvedValue(undefined) + renderWithContext(mockInstalledApps) + + // Act + fireEvent.click(screen.getByTestId('item-operation-trigger')) + fireEvent.click(await screen.findByText('explore.sidebar.action.pin')) + + // Assert + await waitFor(() => { + expect(mockUpdatePinStatus).toHaveBeenCalledWith({ appId: 'app-123', isPinned: true }) + expect(Toast.notify).toHaveBeenCalledWith(expect.objectContaining({ + type: 'success', + message: 'common.api.success', + })) + }) + }) + }) +}) From b2b7e82e281512a7249aabc754dbe4d65f825ad9 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:25:28 +0800 Subject: [PATCH 02/17] refactor(web): migrate log service to TanStack Query (#30065) --- .claude/skills/frontend-testing/SKILL.md | 6 +- .../assets/component-test.template.tsx | 2 +- .../frontend-testing/references/checklist.md | 14 +- .../frontend-testing/references/mocking.md | 25 +- .../frontend-testing/references/workflow.md | 12 +- .github/workflows/web-tests.yml | 2 +- .../components/app/annotation/filter.spec.tsx | 350 +++++++++++-- web/app/components/app/annotation/filter.tsx | 8 +- web/app/components/app/log/filter.tsx | 7 +- web/app/components/app/log/index.tsx | 23 +- web/app/components/app/log/list.tsx | 10 +- .../app/workflow-log/index.spec.tsx | 494 ++++++++++-------- web/app/components/app/workflow-log/index.tsx | 9 +- web/app/components/base/skeleton/index.tsx | 3 +- .../model-parameter-modal/model-display.tsx | 30 +- web/models/log.ts | 15 - web/package.json | 1 + web/service/log.ts | 54 +- web/service/use-log.ts | 89 ++++ web/testing/testing.md | 4 +- 20 files changed, 741 insertions(+), 417 deletions(-) create mode 100644 web/service/use-log.ts diff --git a/.claude/skills/frontend-testing/SKILL.md b/.claude/skills/frontend-testing/SKILL.md index 7475513ba01..65602c92ebb 100644 --- a/.claude/skills/frontend-testing/SKILL.md +++ b/.claude/skills/frontend-testing/SKILL.md @@ -49,10 +49,10 @@ pnpm test pnpm test:watch # Run specific file -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx # Generate coverage report -pnpm test -- --coverage +pnpm test:coverage # Analyze component complexity pnpm analyze-component @@ -155,7 +155,7 @@ describe('ComponentName', () => { For each file: ┌────────────────────────────────────────┐ │ 1. Write test │ - │ 2. Run: pnpm test -- .spec.tsx │ + │ 2. Run: pnpm test .spec.tsx │ │ 3. PASS? → Mark complete, next file │ │ FAIL? → Fix first, then continue │ └────────────────────────────────────────┘ diff --git a/.claude/skills/frontend-testing/assets/component-test.template.tsx b/.claude/skills/frontend-testing/assets/component-test.template.tsx index 92dd797c83d..c39baff9160 100644 --- a/.claude/skills/frontend-testing/assets/component-test.template.tsx +++ b/.claude/skills/frontend-testing/assets/component-test.template.tsx @@ -198,7 +198,7 @@ describe('ComponentName', () => { }) // -------------------------------------------------------------------------- - // Async Operations (if component fetches data - useSWR, useQuery, fetch) + // Async Operations (if component fetches data - useQuery, fetch) // -------------------------------------------------------------------------- // WHY: Async operations have 3 states users experience: loading, success, error describe('Async Operations', () => { diff --git a/.claude/skills/frontend-testing/references/checklist.md b/.claude/skills/frontend-testing/references/checklist.md index aad80b120e3..1ff2b27bbb7 100644 --- a/.claude/skills/frontend-testing/references/checklist.md +++ b/.claude/skills/frontend-testing/references/checklist.md @@ -114,15 +114,15 @@ For the current file being tested: **Run these checks after EACH test file, not just at the end:** -- [ ] Run `pnpm test -- path/to/file.spec.tsx` - **MUST PASS before next file** +- [ ] Run `pnpm test path/to/file.spec.tsx` - **MUST PASS before next file** - [ ] Fix any failures immediately - [ ] Mark file as complete in todo list - [ ] Only then proceed to next file ### After All Files Complete -- [ ] Run full directory test: `pnpm test -- path/to/directory/` -- [ ] Check coverage report: `pnpm test -- --coverage` +- [ ] Run full directory test: `pnpm test path/to/directory/` +- [ ] Check coverage report: `pnpm test:coverage` - [ ] Run `pnpm lint:fix` on all test files - [ ] Run `pnpm type-check:tsgo` @@ -186,16 +186,16 @@ Always test these scenarios: ```bash # Run specific test -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx # Run with coverage -pnpm test -- --coverage path/to/file.spec.tsx +pnpm test:coverage path/to/file.spec.tsx # Watch mode -pnpm test:watch -- path/to/file.spec.tsx +pnpm test:watch path/to/file.spec.tsx # Update snapshots (use sparingly) -pnpm test -- -u path/to/file.spec.tsx +pnpm test -u path/to/file.spec.tsx # Analyze component pnpm analyze-component path/to/component.tsx diff --git a/.claude/skills/frontend-testing/references/mocking.md b/.claude/skills/frontend-testing/references/mocking.md index 51920ebc64a..23889c8d3d6 100644 --- a/.claude/skills/frontend-testing/references/mocking.md +++ b/.claude/skills/frontend-testing/references/mocking.md @@ -242,32 +242,9 @@ describe('Component with Context', () => { }) ``` -### 7. SWR / React Query +### 7. React Query ```typescript -// SWR -vi.mock('swr', () => ({ - __esModule: true, - default: vi.fn(), -})) - -import useSWR from 'swr' -const mockedUseSWR = vi.mocked(useSWR) - -describe('Component with SWR', () => { - it('should show loading state', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - error: undefined, - isLoading: true, - }) - - render() - expect(screen.getByText(/loading/i)).toBeInTheDocument() - }) -}) - -// React Query import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const createTestQueryClient = () => new QueryClient({ diff --git a/.claude/skills/frontend-testing/references/workflow.md b/.claude/skills/frontend-testing/references/workflow.md index b0f2994bded..009c3e013be 100644 --- a/.claude/skills/frontend-testing/references/workflow.md +++ b/.claude/skills/frontend-testing/references/workflow.md @@ -35,7 +35,7 @@ When testing a **single component, hook, or utility**: 2. Run `pnpm analyze-component ` (if available) 3. Check complexity score and features detected 4. Write the test file -5. Run test: `pnpm test -- .spec.tsx` +5. Run test: `pnpm test .spec.tsx` 6. Fix any failures 7. Verify coverage meets goals (100% function, >95% branch) ``` @@ -80,7 +80,7 @@ Process files in this recommended order: ``` ┌─────────────────────────────────────────────┐ │ 1. Write test file │ -│ 2. Run: pnpm test -- .spec.tsx │ +│ 2. Run: pnpm test .spec.tsx │ │ 3. If FAIL → Fix immediately, re-run │ │ 4. If PASS → Mark complete in todo list │ │ 5. ONLY THEN proceed to next file │ @@ -95,10 +95,10 @@ After all individual tests pass: ```bash # Run all tests in the directory together -pnpm test -- path/to/directory/ +pnpm test path/to/directory/ # Check coverage -pnpm test -- --coverage path/to/directory/ +pnpm test:coverage path/to/directory/ ``` ## Component Complexity Guidelines @@ -201,9 +201,9 @@ Run pnpm test ← Multiple failures, hard to debug ``` # GOOD: Incremental with verification Write component-a.spec.tsx -Run pnpm test -- component-a.spec.tsx ✅ +Run pnpm test component-a.spec.tsx ✅ Write component-b.spec.tsx -Run pnpm test -- component-b.spec.tsx ✅ +Run pnpm test component-b.spec.tsx ✅ ...continue... ``` diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8eba0f084b2..adf52a13620 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -42,7 +42,7 @@ jobs: run: pnpm run check:i18n-types - name: Run tests - run: pnpm test --coverage + run: pnpm test:coverage - name: Coverage Summary if: always() diff --git a/web/app/components/app/annotation/filter.spec.tsx b/web/app/components/app/annotation/filter.spec.tsx index 9b733a8c105..7bb39bd4442 100644 --- a/web/app/components/app/annotation/filter.spec.tsx +++ b/web/app/components/app/annotation/filter.spec.tsx @@ -1,72 +1,332 @@ +import type { UseQueryResult } from '@tanstack/react-query' import type { Mock } from 'vitest' import type { QueryParam } from './filter' +import type { AnnotationsCountResponse } from '@/models/log' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import useSWR from 'swr' +import * as useLogModule from '@/service/use-log' import Filter from './filter' -vi.mock('swr', () => ({ - __esModule: true, - default: vi.fn(), -})) +vi.mock('@/service/use-log') -vi.mock('@/service/log', () => ({ - fetchAnnotationsCount: vi.fn(), -})) +const mockUseAnnotationsCount = useLogModule.useAnnotationsCount as Mock -const mockUseSWR = useSWR as unknown as Mock +// ============================================================================ +// Test Utilities +// ============================================================================ + +const createQueryClient = () => new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + +const renderWithQueryClient = (ui: React.ReactElement) => { + const queryClient = createQueryClient() + return render( + + {ui} + , + ) +} + +// ============================================================================ +// Mock Return Value Factory +// ============================================================================ + +type MockQueryResult = Pick, 'data' | 'isLoading' | 'error' | 'refetch'> + +const createMockQueryResult = ( + overrides: Partial> = {}, +): MockQueryResult => ({ + data: undefined, + isLoading: false, + error: null, + refetch: vi.fn(), + ...overrides, +}) + +// ============================================================================ +// Tests +// ============================================================================ describe('Filter', () => { const appId = 'app-1' const childContent = 'child-content' + const defaultQueryParams: QueryParam = { keyword: '' } beforeEach(() => { vi.clearAllMocks() }) - it('should render nothing until annotation count is fetched', () => { - mockUseSWR.mockReturnValue({ data: undefined }) + // -------------------------------------------------------------------------- + // Rendering Tests (REQUIRED) + // -------------------------------------------------------------------------- + describe('Rendering', () => { + it('should render nothing when data is loading', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ isLoading: true }), + ) - const { container } = render( - -
{childContent}
-
, - ) + // Act + const { container } = renderWithQueryClient( + +
{childContent}
+
, + ) - expect(container.firstChild).toBeNull() - expect(mockUseSWR).toHaveBeenCalledWith( - { url: `/apps/${appId}/annotations/count` }, - expect.any(Function), - ) + // Assert + expect(container.firstChild).toBeNull() + }) + + it('should render nothing when data is undefined', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ data: undefined, isLoading: false }), + ) + + // Act + const { container } = renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(container.firstChild).toBeNull() + }) + + it('should render filter and children when data is available', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + expect(screen.getByText(childContent)).toBeInTheDocument() + }) }) - it('should propagate keyword changes and clearing behavior', () => { - mockUseSWR.mockReturnValue({ data: { total: 20 } }) - const queryParams: QueryParam = { keyword: 'prefill' } - const setQueryParams = vi.fn() + // -------------------------------------------------------------------------- + // Props Tests (REQUIRED) + // -------------------------------------------------------------------------- + describe('Props', () => { + it('should call useAnnotationsCount with appId', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 10 }, + isLoading: false, + }), + ) - const { container } = render( - -
{childContent}
-
, - ) + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) - const input = screen.getByPlaceholderText('common.operation.search') as HTMLInputElement - fireEvent.change(input, { target: { value: 'updated' } }) - expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: 'updated' }) + // Assert + expect(mockUseAnnotationsCount).toHaveBeenCalledWith(appId) + }) - const clearButton = input.parentElement?.querySelector('div.cursor-pointer') as HTMLElement - fireEvent.click(clearButton) - expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' }) + it('should display keyword value in input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 10 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: 'test-keyword' } - expect(container).toHaveTextContent(childContent) + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toHaveValue('test-keyword') + }) + }) + + // -------------------------------------------------------------------------- + // User Interactions + // -------------------------------------------------------------------------- + describe('User Interactions', () => { + it('should call setQueryParams when typing in search input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: '' } + const setQueryParams = vi.fn() + + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + fireEvent.change(input, { target: { value: 'updated' } }) + + // Assert + expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: 'updated' }) + }) + + it('should call setQueryParams with empty keyword when clearing input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: 'prefill' } + const setQueryParams = vi.fn() + + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + const clearButton = input.parentElement?.querySelector('div.cursor-pointer') + if (clearButton) + fireEvent.click(clearButton) + + // Assert + expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' }) + }) + }) + + // -------------------------------------------------------------------------- + // Edge Cases (REQUIRED) + // -------------------------------------------------------------------------- + describe('Edge Cases', () => { + it('should handle empty keyword in queryParams', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 5 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toHaveValue('') + }) + + it('should handle undefined keyword in queryParams', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 5 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + }) + + it('should handle zero count', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 0 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert - should still render when count is 0 + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + }) }) }) diff --git a/web/app/components/app/annotation/filter.tsx b/web/app/components/app/annotation/filter.tsx index 76f33d2f1b4..b64a033793f 100644 --- a/web/app/components/app/annotation/filter.tsx +++ b/web/app/components/app/annotation/filter.tsx @@ -2,9 +2,8 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Input from '@/app/components/base/input' -import { fetchAnnotationsCount } from '@/service/log' +import { useAnnotationsCount } from '@/service/use-log' export type QueryParam = { keyword?: string @@ -23,10 +22,9 @@ const Filter: FC = ({ setQueryParams, children, }) => { - // TODO: change fetch list api - const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount) + const { data, isLoading } = useAnnotationsCount(appId) const { t } = useTranslation() - if (!data) + if (isLoading || !data) return null return (
diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index 8984ff3494b..4a0103449f8 100644 --- a/web/app/components/app/log/filter.tsx +++ b/web/app/components/app/log/filter.tsx @@ -6,11 +6,10 @@ import dayjs from 'dayjs' import quarterOfYear from 'dayjs/plugin/quarterOfYear' import * as React from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Chip from '@/app/components/base/chip' import Input from '@/app/components/base/input' import Sort from '@/app/components/base/sort' -import { fetchAnnotationsCount } from '@/service/log' +import { useAnnotationsCount } from '@/service/use-log' dayjs.extend(quarterOfYear) @@ -36,9 +35,9 @@ type IFilterProps = { } const Filter: FC = ({ isChatMode, appId, queryParams, setQueryParams }: IFilterProps) => { - const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount) + const { data, isLoading } = useAnnotationsCount(appId) const { t } = useTranslation() - if (!data) + if (isLoading || !data) return null return (
diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index 183826464ff..0e6e4bba2d6 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -8,11 +8,10 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import { APP_PAGE_LIMIT } from '@/config' -import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' +import { useChatConversations, useCompletionConversations } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import EmptyElement from './empty-element' import Filter, { TIME_PERIOD_MAPPING } from './filter' @@ -88,19 +87,15 @@ const Logs: FC = ({ appDetail }) => { } // When the details are obtained, proceed to the next request - const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode - ? { - url: `/apps/${appDetail.id}/chat-conversations`, - params: query, - } - : null, fetchChatConversations) + const { data: chatConversations, refetch: mutateChatList } = useChatConversations({ + appId: isChatMode ? appDetail.id : '', + params: query, + }) - const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode - ? { - url: `/apps/${appDetail.id}/completion-conversations`, - params: query, - } - : null, fetchCompletionConversations) + const { data: completionConversations, refetch: mutateCompletionList } = useCompletionConversations({ + appId: !isChatMode ? appDetail.id : '', + params: query, + }) const total = isChatMode ? chatConversations?.total : completionConversations?.total diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 06cd20b323d..2b13a09b3a9 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -17,7 +17,6 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import { createContext, useContext } from 'use-context-selector' import { useShallow } from 'zustand/react/shallow' import ModelInfo from '@/app/components/app/log/model-info' @@ -38,7 +37,8 @@ import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' -import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' +import { fetchChatMessages, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' +import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' import PromptLogModal from '../../base/prompt-log-modal' @@ -825,8 +825,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { */ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { // Text Generator App Session Details Including Message List - const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` }) - const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail) + const { data: conversationDetail, refetch: conversationDetailMutate } = useCompletionConversationDetail(appId, conversationId) const { notify } = useContext(ToastContext) const { t } = useTranslation() @@ -875,8 +874,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st * Chat App Conversation Detail Component */ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { - const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` } - const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail) + const { data: conversationDetail } = useChatConversationDetail(appId, conversationId) const { notify } = useContext(ToastContext) const { t } = useTranslation() diff --git a/web/app/components/app/workflow-log/index.spec.tsx b/web/app/components/app/workflow-log/index.spec.tsx index 8d87a6426b9..d689758b306 100644 --- a/web/app/components/app/workflow-log/index.spec.tsx +++ b/web/app/components/app/workflow-log/index.spec.tsx @@ -1,9 +1,9 @@ -import type { MockedFunction } from 'vitest' +import type { UseQueryResult } from '@tanstack/react-query' /** * Logs Container Component Tests * * Tests the main Logs container component which: - * - Fetches workflow logs via useSWR + * - Fetches workflow logs via TanStack Query * - Manages query parameters (status, period, keyword) * - Handles pagination * - Renders Filter, List, and Empty states @@ -15,14 +15,16 @@ import type { MockedFunction } from 'vitest' * - trigger-by-display.spec.tsx */ +import type { MockedFunction } from 'vitest' import type { ILogsProps } from './index' import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' import type { App, AppIconType, AppModeEnum } from '@/types/app' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import useSWR from 'swr' import { APP_PAGE_LIMIT } from '@/config' import { WorkflowRunTriggeredFrom } from '@/models/log' +import * as useLogModule from '@/service/use-log' import { TIME_PERIOD_MAPPING } from './filter' import Logs from './index' @@ -30,7 +32,7 @@ import Logs from './index' // Mocks // ============================================================================ -vi.mock('swr') +vi.mock('@/service/use-log') vi.mock('ahooks', () => ({ useDebounce: (value: T) => value, @@ -72,10 +74,6 @@ vi.mock('@/app/components/base/amplitude/utils', () => ({ trackEvent: (...args: unknown[]) => mockTrackEvent(...args), })) -vi.mock('@/service/log', () => ({ - fetchWorkflowLogs: vi.fn(), -})) - vi.mock('@/hooks/use-theme', () => ({ __esModule: true, default: () => { @@ -89,38 +87,76 @@ vi.mock('@/context/app-context', () => ({ }), })) -// Mock useTimestamp -vi.mock('@/hooks/use-timestamp', () => ({ - __esModule: true, - default: () => ({ - formatTime: (timestamp: number, _format: string) => `formatted-${timestamp}`, - }), -})) - -// Mock useBreakpoints -vi.mock('@/hooks/use-breakpoints', () => ({ - __esModule: true, - default: () => 'pc', - MediaType: { - mobile: 'mobile', - pc: 'pc', - }, -})) - -// Mock BlockIcon -vi.mock('@/app/components/workflow/block-icon', () => ({ - __esModule: true, - default: () =>
BlockIcon
, -})) - // Mock WorkflowContextProvider vi.mock('@/app/components/workflow/context', () => ({ WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => ( -
{children}
+ <>{children} ), })) -const mockedUseSWR = useSWR as unknown as MockedFunction +const mockedUseWorkflowLogs = useLogModule.useWorkflowLogs as MockedFunction + +// ============================================================================ +// Test Utilities +// ============================================================================ + +const createQueryClient = () => new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + +const renderWithQueryClient = (ui: React.ReactElement) => { + const queryClient = createQueryClient() + return render( + + {ui} + , + ) +} + +// ============================================================================ +// Mock Return Value Factory +// ============================================================================ + +const createMockQueryResult = ( + overrides: { data?: T, isLoading?: boolean, error?: Error | null } = {}, +): UseQueryResult => { + const isLoading = overrides.isLoading ?? false + const error = overrides.error ?? null + const data = overrides.data + + return { + data, + isLoading, + error, + refetch: vi.fn(), + isError: !!error, + isPending: isLoading, + isSuccess: !isLoading && !error && data !== undefined, + isFetching: isLoading, + isRefetching: false, + isLoadingError: false, + isRefetchError: false, + isInitialLoading: isLoading, + isPaused: false, + isEnabled: true, + status: isLoading ? 'pending' : error ? 'error' : 'success', + fetchStatus: isLoading ? 'fetching' : 'idle', + dataUpdatedAt: Date.now(), + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isFetched: !isLoading, + isFetchedAfterMount: !isLoading, + isPlaceholderData: false, + isStale: false, + promise: Promise.resolve(data as T), + } as UseQueryResult +} // ============================================================================ // Test Data Factories @@ -195,6 +231,20 @@ const createMockLogsResponse = ( page: 1, }) +// ============================================================================ +// Type-safe Mock Helper +// ============================================================================ + +type WorkflowLogsParams = { + appId: string + params?: Record +} + +const getMockCallParams = (): WorkflowLogsParams | undefined => { + const lastCall = mockedUseWorkflowLogs.mock.calls.at(-1) + return lastCall?.[0] +} + // ============================================================================ // Tests // ============================================================================ @@ -213,45 +263,48 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Rendering', () => { it('should render without crashing', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.workflowTitle')).toBeInTheDocument() }) it('should render title and subtitle', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.workflowTitle')).toBeInTheDocument() expect(screen.getByText('appLog.workflowSubtitle')).toBeInTheDocument() }) it('should render Filter component', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) }) @@ -261,30 +314,33 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Loading State', () => { it('should show loading spinner when data is undefined', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - mutate: vi.fn(), - isValidating: true, - isLoading: true, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: undefined, + isLoading: true, + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() + // Assert expect(container.querySelector('.spin-animation')).toBeInTheDocument() }) it('should not show loading spinner when data is available', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() + // Assert expect(container.querySelector('.spin-animation')).not.toBeInTheDocument() }) }) @@ -294,16 +350,17 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Empty State', () => { it('should render empty element when total is 0', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.table.empty.element.title')).toBeInTheDocument() expect(screen.queryByRole('table')).not.toBeInTheDocument() }) @@ -313,20 +370,21 @@ describe('Logs Container', () => { // Data Fetching Tests // -------------------------------------------------------------------------- describe('Data Fetching', () => { - it('should call useSWR with correct URL and default params', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + it('should call useWorkflowLogs with correct appId and default params', () => { + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { url: string, params: Record } - expect(keyArg).toMatchObject({ - url: `/apps/${defaultProps.appDetail.id}/workflow-app-logs`, + // Assert + const callArg = getMockCallParams() + expect(callArg).toMatchObject({ + appId: defaultProps.appDetail.id, params: expect.objectContaining({ page: 1, detail: true, @@ -336,34 +394,36 @@ describe('Logs Container', () => { }) it('should include date filters for non-allTime periods', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } - expect(keyArg?.params).toHaveProperty('created_at__after') - expect(keyArg?.params).toHaveProperty('created_at__before') + // Assert + const callArg = getMockCallParams() + expect(callArg?.params).toHaveProperty('created_at__after') + expect(callArg?.params).toHaveProperty('created_at__before') }) it('should not include status param when status is all', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } - expect(keyArg?.params).not.toHaveProperty('status') + // Assert + const callArg = getMockCallParams() + expect(callArg?.params).not.toHaveProperty('status') }) }) @@ -372,24 +432,23 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Filter Integration', () => { it('should update query when selecting status filter', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() - // Click status filter + // Act await user.click(screen.getByText('All')) await user.click(await screen.findByText('Success')) - // Check that useSWR was called with updated params + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).toMatchObject({ status: 'succeeded', }) @@ -397,46 +456,46 @@ describe('Logs Container', () => { }) it('should update query when selecting period filter', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() - // Click period filter + // Act await user.click(screen.getByText('appLog.filter.period.last7days')) await user.click(await screen.findByText('appLog.filter.period.allTime')) - // When period is allTime (9), date filters should be removed + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).not.toHaveProperty('created_at__after') expect(lastCall?.params).not.toHaveProperty('created_at__before') }) }) it('should update query when typing keyword', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() + // Act const searchInput = screen.getByPlaceholderText('common.operation.search') await user.type(searchInput, 'test-keyword') + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).toMatchObject({ keyword: 'test-keyword', }) @@ -449,36 +508,35 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Pagination', () => { it('should not render pagination when total is less than limit', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - render() + // Act + renderWithQueryClient() - // Pagination component should not be rendered + // Assert expect(screen.queryByRole('navigation')).not.toBeInTheDocument() }) it('should render pagination when total exceeds limit', () => { + // Arrange const logs = Array.from({ length: APP_PAGE_LIMIT }, (_, i) => createMockWorkflowLog({ id: `log-${i}` })) - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse(logs, APP_PAGE_LIMIT + 10), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse(logs, APP_PAGE_LIMIT + 10), + }), + ) - render() + // Act + renderWithQueryClient() - // Should show pagination - checking for any pagination-related element - // The Pagination component renders page controls + // Assert expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -488,37 +546,39 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('List Rendering', () => { it('should render List component when data is available', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByRole('table')).toBeInTheDocument() }) it('should display log data in table', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([ - createMockWorkflowLog({ - workflow_run: createMockWorkflowRun({ - status: 'succeeded', - total_tokens: 500, + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([ + createMockWorkflowLog({ + workflow_run: createMockWorkflowRun({ + status: 'succeeded', + total_tokens: 500, + }), }), - }), - ], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + ], 1), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('Success')).toBeInTheDocument() expect(screen.getByText('500')).toBeInTheDocument() }) @@ -541,52 +601,54 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle different app modes', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum }) - render() + // Act + renderWithQueryClient() - // Should render without trigger column + // Assert expect(screen.queryByText('appLog.table.header.triggered_from')).not.toBeInTheDocument() }) - it('should handle error state from useSWR', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: new Error('Failed to fetch'), - }) + it('should handle error state from useWorkflowLogs', () => { + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: undefined, + error: new Error('Failed to fetch'), + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() - // Should show loading state when data is undefined + // Assert - should show loading state when data is undefined expect(container.querySelector('.spin-animation')).toBeInTheDocument() }) it('should handle app with different ID', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) const customApp = createMockApp({ id: 'custom-app-123' }) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { url: string } - expect(keyArg?.url).toBe('/apps/custom-app-123/workflow-app-logs') + // Assert + const callArg = getMockCallParams() + expect(callArg?.appId).toBe('custom-app-123') }) }) }) diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 1390f2d4357..12438d6d17c 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -9,13 +9,12 @@ import { omit } from 'lodash-es' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import EmptyElement from '@/app/components/app/log/empty-element' import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import { APP_PAGE_LIMIT } from '@/config' import { useAppContext } from '@/context/app-context' -import { fetchWorkflowLogs } from '@/service/log' +import { useWorkflowLogs } from '@/service/use-log' import Filter, { TIME_PERIOD_MAPPING } from './filter' import List from './list' @@ -55,10 +54,10 @@ const Logs: FC = ({ appDetail }) => { ...omit(debouncedQueryParams, ['period', 'status']), } - const { data: workflowLogs, mutate } = useSWR({ - url: `/apps/${appDetail.id}/workflow-app-logs`, + const { data: workflowLogs, refetch: mutate } = useWorkflowLogs({ + appId: appDetail.id, params: query, - }, fetchWorkflowLogs) + }) const total = workflowLogs?.total return ( diff --git a/web/app/components/base/skeleton/index.tsx b/web/app/components/base/skeleton/index.tsx index 9cd7e3f09c6..cbb5a3d7c31 100644 --- a/web/app/components/base/skeleton/index.tsx +++ b/web/app/components/base/skeleton/index.tsx @@ -36,7 +36,8 @@ export const SkeletonPoint: FC = (props) => {
·
) } -/** Usage +/** + * Usage * * * diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx index e9e8ec7525e..d8a4fabac0b 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx @@ -6,20 +6,22 @@ type ModelDisplayProps = { } const ModelDisplay = ({ currentModel, modelId }: ModelDisplayProps) => { - return currentModel ? ( - - ) : ( -
-
- {modelId} -
-
- ) + return currentModel + ? ( + + ) + : ( +
+
+ {modelId} +
+
+ ) } export default ModelDisplay diff --git a/web/models/log.ts b/web/models/log.ts index 8c022ee6b24..d15d6d66883 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -6,21 +6,6 @@ import type { } from '@/app/components/workflow/types' import type { VisionFile } from '@/types/app' -// Log type contains key:string conversation_id:string created_at:string question:string answer:string -export type Conversation = { - id: string - key: string - conversationId: string - question: string - answer: string - userRate: number - adminRate: number -} - -export type ConversationListResponse = { - logs: Conversation[] -} - export const CompletionParams = ['temperature', 'top_p', 'presence_penalty', 'max_token', 'stop', 'frequency_penalty'] as const export type CompletionParamType = typeof CompletionParams[number] diff --git a/web/package.json b/web/package.json index ce2e59e022f..baecd2c5c75 100644 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "gen:i18n-types": "node ./i18n-config/generate-i18n-types.js", "check:i18n-types": "node ./i18n-config/check-i18n-sync.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", "analyze-component": "node testing/analyze-component.js", "storybook": "storybook dev -p 6006", diff --git a/web/service/log.ts b/web/service/log.ts index aa0be7ac3b7..a540cea22c9 100644 --- a/web/service/log.ts +++ b/web/service/log.ts @@ -1,80 +1,38 @@ -import type { Fetcher } from 'swr' import type { AgentLogDetailRequest, AgentLogDetailResponse, - AnnotationsCountResponse, - ChatConversationFullDetailResponse, - ChatConversationsRequest, - ChatConversationsResponse, ChatMessagesRequest, ChatMessagesResponse, - CompletionConversationFullDetailResponse, - CompletionConversationsRequest, - CompletionConversationsResponse, - ConversationListResponse, LogMessageAnnotationsRequest, LogMessageAnnotationsResponse, LogMessageFeedbacksRequest, LogMessageFeedbacksResponse, - WorkflowLogsResponse, WorkflowRunDetailResponse, } from '@/models/log' import type { NodeTracingListResponse } from '@/types/workflow' import { get, post } from './base' -export const fetchConversationList: Fetcher }> = ({ appId, params }) => { - return get(`/console/api/apps/${appId}/messages`, params) -} - -// (Text Generation Application) Session List -export const fetchCompletionConversations: Fetcher = ({ url, params }) => { - return get(url, { params }) -} - -// (Text Generation Application) Session Detail -export const fetchCompletionConversationDetail: Fetcher = ({ url }) => { - return get(url, {}) -} - -// (Chat Application) Session List -export const fetchChatConversations: Fetcher = ({ url, params }) => { - return get(url, { params }) -} - -// (Chat Application) Session Detail -export const fetchChatConversationDetail: Fetcher = ({ url }) => { - return get(url, {}) -} - // (Chat Application) Message list in one session -export const fetchChatMessages: Fetcher = ({ url, params }) => { +export const fetchChatMessages = ({ url, params }: { url: string, params: ChatMessagesRequest }): Promise => { return get(url, { params }) } -export const updateLogMessageFeedbacks: Fetcher = ({ url, body }) => { +export const updateLogMessageFeedbacks = ({ url, body }: { url: string, body: LogMessageFeedbacksRequest }): Promise => { return post(url, { body }) } -export const updateLogMessageAnnotations: Fetcher = ({ url, body }) => { +export const updateLogMessageAnnotations = ({ url, body }: { url: string, body: LogMessageAnnotationsRequest }): Promise => { return post(url, { body }) } -export const fetchAnnotationsCount: Fetcher = ({ url }) => { - return get(url) -} - -export const fetchWorkflowLogs: Fetcher }> = ({ url, params }) => { - return get(url, { params }) -} - -export const fetchRunDetail = (url: string) => { +export const fetchRunDetail = (url: string): Promise => { return get(url) } -export const fetchTracingList: Fetcher = ({ url }) => { +export const fetchTracingList = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchAgentLogDetail = ({ appID, params }: { appID: string, params: AgentLogDetailRequest }) => { +export const fetchAgentLogDetail = ({ appID, params }: { appID: string, params: AgentLogDetailRequest }): Promise => { return get(`/apps/${appID}/agent/logs`, { params }) } diff --git a/web/service/use-log.ts b/web/service/use-log.ts new file mode 100644 index 00000000000..b120adda2fd --- /dev/null +++ b/web/service/use-log.ts @@ -0,0 +1,89 @@ +import type { + AnnotationsCountResponse, + ChatConversationFullDetailResponse, + ChatConversationsRequest, + ChatConversationsResponse, + CompletionConversationFullDetailResponse, + CompletionConversationsRequest, + CompletionConversationsResponse, + WorkflowLogsResponse, +} from '@/models/log' +import { useQuery } from '@tanstack/react-query' +import { get } from './base' + +const NAME_SPACE = 'log' + +// ============ Annotations Count ============ + +export const useAnnotationsCount = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'annotations-count', appId], + queryFn: () => get(`/apps/${appId}/annotations/count`), + enabled: !!appId, + }) +} + +// ============ Chat Conversations ============ + +type ChatConversationsParams = { + appId: string + params?: Partial +} + +export const useChatConversations = ({ appId, params }: ChatConversationsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'chat-conversations', appId, params], + queryFn: () => get(`/apps/${appId}/chat-conversations`, { params }), + enabled: !!appId, + }) +} + +// ============ Completion Conversations ============ + +type CompletionConversationsParams = { + appId: string + params?: Partial +} + +export const useCompletionConversations = ({ appId, params }: CompletionConversationsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'completion-conversations', appId, params], + queryFn: () => get(`/apps/${appId}/completion-conversations`, { params }), + enabled: !!appId, + }) +} + +// ============ Chat Conversation Detail ============ + +export const useChatConversationDetail = (appId?: string, conversationId?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'chat-conversation-detail', appId, conversationId], + queryFn: () => get(`/apps/${appId}/chat-conversations/${conversationId}`), + enabled: !!appId && !!conversationId, + }) +} + +// ============ Completion Conversation Detail ============ + +export const useCompletionConversationDetail = (appId?: string, conversationId?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'completion-conversation-detail', appId, conversationId], + queryFn: () => get(`/apps/${appId}/completion-conversations/${conversationId}`), + enabled: !!appId && !!conversationId, + }) +} + +// ============ Workflow Logs ============ + +type WorkflowLogsParams = { + appId: string + params?: Record +} + +export const useWorkflowLogs = ({ appId, params }: WorkflowLogsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'workflow-logs', appId, params], + queryFn: () => get(`/apps/${appId}/workflow-app-logs`, { params }), + enabled: !!appId, + }) +} diff --git a/web/testing/testing.md b/web/testing/testing.md index a2c8399d452..08fc716cf3f 100644 --- a/web/testing/testing.md +++ b/web/testing/testing.md @@ -21,10 +21,10 @@ pnpm test pnpm test:watch # Generate coverage report -pnpm test -- --coverage +pnpm test:coverage # Run specific file -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx ``` ## Project Test Setup From 0f41924db48596058edc1a2ebbe49eac23433c9e Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 24 Dec 2025 16:17:59 +0800 Subject: [PATCH 03/17] chore: some tests (#30084) Co-authored-by: yyh --- .../base/feature-panel/index.spec.tsx | 71 +++++ .../base/feature-panel/index.tsx | 4 +- .../apps-full-in-dialog/index.spec.tsx | 274 ++++++++++++++++++ .../billing/pricing/assets/index.spec.tsx | 64 ++++ .../billing/pricing/footer.spec.tsx | 68 +++++ .../billing/pricing/header.spec.tsx | 72 +++++ .../components/billing/pricing/index.spec.tsx | 119 ++++++++ .../pricing/plan-switcher/index.spec.tsx | 109 +++++++ .../plan-range-switcher.spec.tsx | 81 ++++++ .../pricing/plan-switcher/tab.spec.tsx | 95 ++++++ .../components/billing/progress-bar/index.tsx | 1 + 11 files changed, 956 insertions(+), 2 deletions(-) create mode 100644 web/app/components/app/configuration/base/feature-panel/index.spec.tsx create mode 100644 web/app/components/billing/apps-full-in-dialog/index.spec.tsx create mode 100644 web/app/components/billing/pricing/assets/index.spec.tsx create mode 100644 web/app/components/billing/pricing/footer.spec.tsx create mode 100644 web/app/components/billing/pricing/header.spec.tsx create mode 100644 web/app/components/billing/pricing/index.spec.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/index.spec.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx create mode 100644 web/app/components/billing/pricing/plan-switcher/tab.spec.tsx diff --git a/web/app/components/app/configuration/base/feature-panel/index.spec.tsx b/web/app/components/app/configuration/base/feature-panel/index.spec.tsx new file mode 100644 index 00000000000..7e1b6613992 --- /dev/null +++ b/web/app/components/app/configuration/base/feature-panel/index.spec.tsx @@ -0,0 +1,71 @@ +import { render, screen } from '@testing-library/react' +import FeaturePanel from './index' + +describe('FeaturePanel', () => { + // Rendering behavior for standard layout. + describe('Rendering', () => { + it('should render the title and children when provided', () => { + // Arrange + render( + +
Panel Body
+
, + ) + + // Assert + expect(screen.getByText('Panel Title')).toBeInTheDocument() + expect(screen.getByText('Panel Body')).toBeInTheDocument() + }) + }) + + // Prop-driven presentation details like icons, actions, and spacing. + describe('Props', () => { + it('should render header icon and right slot and apply header border', () => { + // Arrange + render( + Icon} + headerRight={} + hasHeaderBottomBorder={true} + />, + ) + + // Assert + expect(screen.getByText('Icon')).toBeInTheDocument() + expect(screen.getByText('Action')).toBeInTheDocument() + const header = screen.getByTestId('feature-panel-header') + expect(header).toHaveClass('border-b') + }) + + it('should apply custom className and remove padding when noBodySpacing is true', () => { + // Arrange + const { container } = render( + +
Body
+
, + ) + + // Assert + const root = container.firstElementChild as HTMLElement + expect(root).toHaveClass('custom-panel') + expect(root).toHaveClass('pb-0') + const body = screen.getByTestId('feature-panel-body') + expect(body).not.toHaveClass('mt-1') + expect(body).not.toHaveClass('px-3') + }) + }) + + // Edge cases when optional content is missing. + describe('Edge Cases', () => { + it('should not render the body wrapper when children is undefined', () => { + // Arrange + render() + + // Assert + expect(screen.queryByText('No Body')).toBeInTheDocument() + expect(screen.queryByText('Panel Body')).not.toBeInTheDocument() + expect(screen.queryByTestId('feature-panel-body')).not.toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/app/configuration/base/feature-panel/index.tsx b/web/app/components/app/configuration/base/feature-panel/index.tsx index 20c4a8dc177..06ae2ab10aa 100644 --- a/web/app/components/app/configuration/base/feature-panel/index.tsx +++ b/web/app/components/app/configuration/base/feature-panel/index.tsx @@ -25,7 +25,7 @@ const FeaturePanel: FC = ({ return (
{/* Header */} -
+
{headerIcon &&
{headerIcon}
} @@ -38,7 +38,7 @@ const FeaturePanel: FC = ({
{/* Body */} {children && ( -
+
{children}
)} diff --git a/web/app/components/billing/apps-full-in-dialog/index.spec.tsx b/web/app/components/billing/apps-full-in-dialog/index.spec.tsx new file mode 100644 index 00000000000..a11b582b0f6 --- /dev/null +++ b/web/app/components/billing/apps-full-in-dialog/index.spec.tsx @@ -0,0 +1,274 @@ +import type { Mock } from 'vitest' +import type { UsagePlanInfo } from '@/app/components/billing/type' +import type { AppContextValue } from '@/context/app-context' +import type { ProviderContextState } from '@/context/provider-context' +import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' +import { render, screen } from '@testing-library/react' +import { Plan } from '@/app/components/billing/type' +import { mailToSupport } from '@/app/components/header/utils/util' +import { useAppContext } from '@/context/app-context' +import { baseProviderContextValue, useProviderContext } from '@/context/provider-context' +import AppsFull from './index' + +vi.mock('@/context/app-context', () => ({ + useAppContext: vi.fn(), +})) + +vi.mock('@/context/provider-context', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useProviderContext: vi.fn(), + } +}) + +vi.mock('@/context/modal-context', () => ({ + useModalContext: () => ({ + setShowPricingModal: vi.fn(), + }), +})) + +vi.mock('@/app/components/header/utils/util', () => ({ + mailToSupport: vi.fn(), +})) + +const buildUsage = (overrides: Partial = {}): UsagePlanInfo => ({ + buildApps: 0, + teamMembers: 0, + annotatedResponse: 0, + documentsUploadQuota: 0, + apiRateLimit: 0, + triggerEvents: 0, + vectorSpace: 0, + ...overrides, +}) + +const buildProviderContext = (overrides: Partial = {}): ProviderContextState => ({ + ...baseProviderContextValue, + plan: { + ...baseProviderContextValue.plan, + type: Plan.sandbox, + usage: buildUsage({ buildApps: 2 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + ...overrides, +}) + +const buildAppContext = (overrides: Partial = {}): AppContextValue => { + const userProfile: UserProfileResponse = { + id: 'user-id', + name: 'Test User', + email: 'user@example.com', + avatar: '', + avatar_url: '', + is_password_set: false, + } + const currentWorkspace: ICurrentWorkspace = { + id: 'workspace-id', + name: 'Workspace', + plan: '', + status: '', + created_at: 0, + role: 'normal', + providers: [], + } + const langGeniusVersionInfo: LangGeniusVersionResponse = { + current_env: '', + current_version: '1.0.0', + latest_version: '', + release_date: '', + release_notes: '', + version: '', + can_auto_update: false, + } + const base: Omit = { + userProfile, + currentWorkspace, + isCurrentWorkspaceManager: false, + isCurrentWorkspaceOwner: false, + isCurrentWorkspaceEditor: false, + isCurrentWorkspaceDatasetOperator: false, + mutateUserProfile: vi.fn(), + mutateCurrentWorkspace: vi.fn(), + langGeniusVersionInfo, + isLoadingCurrentWorkspace: false, + } + const useSelector: AppContextValue['useSelector'] = selector => selector({ ...base, useSelector }) + return { + ...base, + useSelector, + ...overrides, + } +} + +describe('AppsFull', () => { + beforeEach(() => { + vi.clearAllMocks() + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext()) + ;(useAppContext as Mock).mockReturnValue(buildAppContext()) + ;(mailToSupport as Mock).mockReturnValue('mailto:support@example.com') + }) + + // Rendering behavior for non-team plans. + describe('Rendering', () => { + it('should render the sandbox messaging and upgrade button', () => { + // Act + render() + + // Assert + expect(screen.getByText('billing.apps.fullTip1')).toBeInTheDocument() + expect(screen.getByText('billing.apps.fullTip1des')).toBeInTheDocument() + expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument() + expect(screen.getByText('2/10')).toBeInTheDocument() + }) + }) + + // Prop-driven behavior for team plans and contact CTA. + describe('Props', () => { + it('should render team messaging and contact button for non-sandbox plans', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.team, + usage: buildUsage({ buildApps: 8 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + render() + + // Assert + expect(screen.getByText('billing.apps.fullTip2')).toBeInTheDocument() + expect(screen.getByText('billing.apps.fullTip2des')).toBeInTheDocument() + expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() + expect(screen.getByRole('link', { name: 'billing.apps.contactUs' })).toHaveAttribute('href', 'mailto:support@example.com') + expect(mailToSupport).toHaveBeenCalledWith('user@example.com', Plan.team, '1.0.0') + }) + + it('should render upgrade button for professional plans', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.professional, + usage: buildUsage({ buildApps: 4 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + + // Act + render() + + // Assert + expect(screen.getByText('billing.apps.fullTip1')).toBeInTheDocument() + expect(screen.getByText('billing.upgradeBtn.encourageShort')).toBeInTheDocument() + expect(screen.queryByText('billing.apps.contactUs')).not.toBeInTheDocument() + }) + + it('should render contact button for enterprise plans', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.enterprise, + usage: buildUsage({ buildApps: 9 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + + // Act + render() + + // Assert + expect(screen.getByText('billing.apps.fullTip1')).toBeInTheDocument() + expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument() + expect(screen.getByRole('link', { name: 'billing.apps.contactUs' })).toHaveAttribute('href', 'mailto:support@example.com') + expect(mailToSupport).toHaveBeenCalledWith('user@example.com', Plan.enterprise, '1.0.0') + }) + }) + + // Edge cases for progress color thresholds. + describe('Edge Cases', () => { + it('should use the success color when usage is below 50%', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.sandbox, + usage: buildUsage({ buildApps: 2 }), + total: buildUsage({ buildApps: 5 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + + // Act + render() + + // Assert + expect(screen.getByTestId('billing-progress-bar')).toHaveClass('bg-components-progress-bar-progress-solid') + }) + + it('should use the warning color when usage is between 50% and 80%', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.sandbox, + usage: buildUsage({ buildApps: 6 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + + // Act + render() + + // Assert + expect(screen.getByTestId('billing-progress-bar')).toHaveClass('bg-components-progress-warning-progress') + }) + + it('should use the error color when usage is 80% or higher', () => { + // Arrange + ;(useProviderContext as Mock).mockReturnValue(buildProviderContext({ + plan: { + ...baseProviderContextValue.plan, + type: Plan.sandbox, + usage: buildUsage({ buildApps: 8 }), + total: buildUsage({ buildApps: 10 }), + reset: { + apiRateLimit: null, + triggerEvents: null, + }, + }, + })) + + // Act + render() + + // Assert + expect(screen.getByTestId('billing-progress-bar')).toHaveClass('bg-components-progress-error-progress') + }) + }) +}) diff --git a/web/app/components/billing/pricing/assets/index.spec.tsx b/web/app/components/billing/pricing/assets/index.spec.tsx new file mode 100644 index 00000000000..7980f9a1822 --- /dev/null +++ b/web/app/components/billing/pricing/assets/index.spec.tsx @@ -0,0 +1,64 @@ +import { render } from '@testing-library/react' +import { + Cloud, + Community, + Enterprise, + EnterpriseNoise, + NoiseBottom, + NoiseTop, + Premium, + PremiumNoise, + Professional, + Sandbox, + SelfHosted, + Team, +} from './index' + +describe('Pricing Assets', () => { + // Rendering: each asset should render an svg. + describe('Rendering', () => { + it('should render static assets without crashing', () => { + // Arrange + const assets = [ + , + , + , + , + , + , + , + , + , + , + ] + + // Act / Assert + assets.forEach((asset) => { + const { container, unmount } = render(asset) + expect(container.querySelector('svg')).toBeInTheDocument() + unmount() + }) + }) + }) + + // Props: active state should change fill color for selectable assets. + describe('Props', () => { + it('should render active state for Cloud', () => { + // Arrange + const { container } = render() + + // Assert + const rects = Array.from(container.querySelectorAll('rect')) + expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-saas-dify-blue-accessible)')).toBe(true) + }) + + it('should render inactive state for SelfHosted', () => { + // Arrange + const { container } = render() + + // Assert + const rects = Array.from(container.querySelectorAll('rect')) + expect(rects.some(rect => rect.getAttribute('fill') === 'var(--color-text-primary)')).toBe(true) + }) + }) +}) diff --git a/web/app/components/billing/pricing/footer.spec.tsx b/web/app/components/billing/pricing/footer.spec.tsx new file mode 100644 index 00000000000..f8e7965f5e1 --- /dev/null +++ b/web/app/components/billing/pricing/footer.spec.tsx @@ -0,0 +1,68 @@ +import { render, screen } from '@testing-library/react' +import * as React from 'react' +import { CategoryEnum } from '.' +import Footer from './footer' + +let mockTranslations: Record = {} + +vi.mock('next/link', () => ({ + default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => ( + + {children} + + ), +})) + +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string) => mockTranslations[key] ?? key, + }), + } +}) + +describe('Footer', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTranslations = {} + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render tax tips and comparison link when in cloud category', () => { + // Arrange + render(
) + + // Assert + expect(screen.getByText('billing.plansCommon.taxTip')).toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.taxTipSecond')).toBeInTheDocument() + expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', 'https://dify.ai/pricing#plans-and-features') + expect(screen.getByText('billing.plansCommon.comparePlanAndFeatures')).toBeInTheDocument() + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should hide tax tips when category is self-hosted', () => { + // Arrange + render(
) + + // Assert + expect(screen.queryByText('billing.plansCommon.taxTip')).not.toBeInTheDocument() + expect(screen.queryByText('billing.plansCommon.taxTipSecond')).not.toBeInTheDocument() + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should render link even when pricing URL is empty', () => { + // Arrange + render(
) + + // Assert + expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', '') + }) + }) +}) diff --git a/web/app/components/billing/pricing/header.spec.tsx b/web/app/components/billing/pricing/header.spec.tsx new file mode 100644 index 00000000000..0395e5dd48a --- /dev/null +++ b/web/app/components/billing/pricing/header.spec.tsx @@ -0,0 +1,72 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import Header from './header' + +let mockTranslations: Record = {} + +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string) => mockTranslations[key] ?? key, + }), + } +}) + +describe('Header', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTranslations = {} + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render title and description translations', () => { + // Arrange + const handleClose = vi.fn() + + // Act + render(
) + + // Assert + expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.title.description')).toBeInTheDocument() + expect(screen.getByRole('button')).toBeInTheDocument() + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should invoke onClose when close button is clicked', () => { + // Arrange + const handleClose = vi.fn() + render(
) + + // Act + fireEvent.click(screen.getByRole('button')) + + // Assert + expect(handleClose).toHaveBeenCalledTimes(1) + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should render structure when translations are empty strings', () => { + // Arrange + mockTranslations = { + 'billing.plansCommon.title.plans': '', + 'billing.plansCommon.title.description': '', + } + + // Act + const { container } = render(
) + + // Assert + expect(container.querySelector('span')).toBeInTheDocument() + expect(container.querySelector('p')).toBeInTheDocument() + expect(screen.getByRole('button')).toBeInTheDocument() + }) + }) +}) diff --git a/web/app/components/billing/pricing/index.spec.tsx b/web/app/components/billing/pricing/index.spec.tsx new file mode 100644 index 00000000000..141c2d9c963 --- /dev/null +++ b/web/app/components/billing/pricing/index.spec.tsx @@ -0,0 +1,119 @@ +import type { Mock } from 'vitest' +import type { UsagePlanInfo } from '../type' +import { fireEvent, render, screen } from '@testing-library/react' +import { useKeyPress } from 'ahooks' +import * as React from 'react' +import { useAppContext } from '@/context/app-context' +import { useGetPricingPageLanguage } from '@/context/i18n' +import { useProviderContext } from '@/context/provider-context' +import { Plan } from '../type' +import Pricing from './index' + +let mockTranslations: Record = {} +let mockLanguage: string | null = 'en' + +vi.mock('next/link', () => ({ + default: ({ children, href, className, target }: { children: React.ReactNode, href: string, className?: string, target?: string }) => ( + + {children} + + ), +})) + +vi.mock('ahooks', () => ({ + useKeyPress: vi.fn(), +})) + +vi.mock('@/context/app-context', () => ({ + useAppContext: vi.fn(), +})) + +vi.mock('@/context/provider-context', () => ({ + useProviderContext: vi.fn(), +})) + +vi.mock('@/context/i18n', () => ({ + useGetPricingPageLanguage: vi.fn(), +})) + +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string, options?: { returnObjects?: boolean }) => { + if (options?.returnObjects) + return mockTranslations[key] ?? [] + return mockTranslations[key] ?? key + }, + }), + Trans: ({ i18nKey }: { i18nKey: string }) => {i18nKey}, + } +}) + +const buildUsage = (): UsagePlanInfo => ({ + buildApps: 0, + teamMembers: 0, + annotatedResponse: 0, + documentsUploadQuota: 0, + apiRateLimit: 0, + triggerEvents: 0, + vectorSpace: 0, +}) + +describe('Pricing', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTranslations = {} + mockLanguage = 'en' + ;(useAppContext as Mock).mockReturnValue({ isCurrentWorkspaceManager: true }) + ;(useProviderContext as Mock).mockReturnValue({ + plan: { + type: Plan.sandbox, + usage: buildUsage(), + total: buildUsage(), + }, + }) + ;(useGetPricingPageLanguage as Mock).mockImplementation(() => mockLanguage) + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render pricing header and localized footer link', () => { + // Arrange + render() + + // Assert + expect(screen.getByText('billing.plansCommon.title.plans')).toBeInTheDocument() + expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', 'https://dify.ai/en/pricing#plans-and-features') + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should register esc key handler and allow switching categories', () => { + // Arrange + const handleCancel = vi.fn() + render() + + // Act + fireEvent.click(screen.getByText('billing.plansCommon.self')) + + // Assert + expect(useKeyPress).toHaveBeenCalledWith(['esc'], handleCancel) + expect(screen.queryByRole('switch')).not.toBeInTheDocument() + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should fall back to default pricing URL when language is empty', () => { + // Arrange + mockLanguage = '' + render() + + // Assert + expect(screen.getByTestId('pricing-link')).toHaveAttribute('href', 'https://dify.ai/pricing#plans-and-features') + }) + }) +}) diff --git a/web/app/components/billing/pricing/plan-switcher/index.spec.tsx b/web/app/components/billing/pricing/plan-switcher/index.spec.tsx new file mode 100644 index 00000000000..641d359bfdf --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/index.spec.tsx @@ -0,0 +1,109 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import { CategoryEnum } from '../index' +import PlanSwitcher from './index' +import { PlanRange } from './plan-range-switcher' + +let mockTranslations: Record = {} + +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string) => mockTranslations[key] ?? key, + }), + } +}) + +describe('PlanSwitcher', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTranslations = {} + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render category tabs and plan range switcher for cloud', () => { + // Arrange + render( + , + ) + + // Assert + expect(screen.getByText('billing.plansCommon.cloud')).toBeInTheDocument() + expect(screen.getByText('billing.plansCommon.self')).toBeInTheDocument() + expect(screen.getByRole('switch')).toBeInTheDocument() + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should call onChangeCategory when selecting a tab', () => { + // Arrange + const handleChangeCategory = vi.fn() + render( + , + ) + + // Act + fireEvent.click(screen.getByText('billing.plansCommon.self')) + + // Assert + expect(handleChangeCategory).toHaveBeenCalledTimes(1) + expect(handleChangeCategory).toHaveBeenCalledWith(CategoryEnum.SELF) + }) + + it('should hide plan range switcher when category is self-hosted', () => { + // Arrange + render( + , + ) + + // Assert + expect(screen.queryByRole('switch')).not.toBeInTheDocument() + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should render tabs when translation strings are empty', () => { + // Arrange + mockTranslations = { + 'billing.plansCommon.cloud': '', + 'billing.plansCommon.self': '', + } + + // Act + const { container } = render( + , + ) + + // Assert + const labels = container.querySelectorAll('span') + expect(labels).toHaveLength(2) + expect(labels[0]?.textContent).toBe('') + expect(labels[1]?.textContent).toBe('') + }) + }) +}) diff --git a/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx new file mode 100644 index 00000000000..0b4c00603cb --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/plan-range-switcher.spec.tsx @@ -0,0 +1,81 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import PlanRangeSwitcher, { PlanRange } from './plan-range-switcher' + +let mockTranslations: Record = {} + +vi.mock('react-i18next', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string) => mockTranslations[key] ?? key, + }), + } +}) + +describe('PlanRangeSwitcher', () => { + beforeEach(() => { + vi.clearAllMocks() + mockTranslations = {} + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render the annual billing label', () => { + // Arrange + render() + + // Assert + expect(screen.getByText('billing.plansCommon.annualBilling')).toBeInTheDocument() + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false') + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should switch to yearly when toggled from monthly', () => { + // Arrange + const handleChange = vi.fn() + render() + + // Act + fireEvent.click(screen.getByRole('switch')) + + // Assert + expect(handleChange).toHaveBeenCalledTimes(1) + expect(handleChange).toHaveBeenCalledWith(PlanRange.yearly) + }) + + it('should switch to monthly when toggled from yearly', () => { + // Arrange + const handleChange = vi.fn() + render() + + // Act + fireEvent.click(screen.getByRole('switch')) + + // Assert + expect(handleChange).toHaveBeenCalledTimes(1) + expect(handleChange).toHaveBeenCalledWith(PlanRange.monthly) + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should render when the translation string is empty', () => { + // Arrange + mockTranslations = { + 'billing.plansCommon.annualBilling': '', + } + + // Act + const { container } = render() + + // Assert + const label = container.querySelector('span') + expect(label).toBeInTheDocument() + expect(label?.textContent).toBe('') + }) + }) +}) diff --git a/web/app/components/billing/pricing/plan-switcher/tab.spec.tsx b/web/app/components/billing/pricing/plan-switcher/tab.spec.tsx new file mode 100644 index 00000000000..5c335e0dd1f --- /dev/null +++ b/web/app/components/billing/pricing/plan-switcher/tab.spec.tsx @@ -0,0 +1,95 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import Tab from './tab' + +const Icon = ({ isActive }: { isActive: boolean }) => ( + +) + +describe('PlanSwitcherTab', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + // Rendering behavior + describe('Rendering', () => { + it('should render label and icon', () => { + // Arrange + render( + , + ) + + // Assert + expect(screen.getByText('Cloud')).toBeInTheDocument() + expect(screen.getByTestId('tab-icon')).toHaveAttribute('data-active', 'false') + }) + }) + + // Prop-driven behavior + describe('Props', () => { + it('should call onClick with the provided value', () => { + // Arrange + const handleClick = vi.fn() + render( + , + ) + + // Act + fireEvent.click(screen.getByText('Self')) + + // Assert + expect(handleClick).toHaveBeenCalledTimes(1) + expect(handleClick).toHaveBeenCalledWith('self') + }) + + it('should apply active text class when isActive is true', () => { + // Arrange + render( + , + ) + + // Assert + expect(screen.getByText('Cloud')).toHaveClass('text-saas-dify-blue-accessible') + expect(screen.getByTestId('tab-icon')).toHaveAttribute('data-active', 'true') + }) + }) + + // Edge case rendering behavior + describe('Edge Cases', () => { + it('should render when label is empty', () => { + // Arrange + const { container } = render( + , + ) + + // Assert + const label = container.querySelector('span') + expect(label).toBeInTheDocument() + expect(label?.textContent).toBe('') + }) + }) +}) diff --git a/web/app/components/billing/progress-bar/index.tsx b/web/app/components/billing/progress-bar/index.tsx index 383b516b61a..c41fc53310d 100644 --- a/web/app/components/billing/progress-bar/index.tsx +++ b/web/app/components/billing/progress-bar/index.tsx @@ -12,6 +12,7 @@ const ProgressBar = ({ return (
Date: Wed, 24 Dec 2025 16:31:16 +0800 Subject: [PATCH 04/17] chore: fix type check for i18n (#30058) Co-authored-by: Claude Opus 4.5 Co-authored-by: yyh --- .../translate-i18n-base-on-english.yml | 12 +- .github/workflows/web-tests.yml | 3 - .../[appId]/overview/card-view.tsx | 2 +- .../overview/long-time-range-picker.tsx | 2 +- .../time-range-picker/range-selector.tsx | 2 +- .../[datasetId]/settings/page.tsx | 6 +- .../app-sidebar/dataset-info/index.tsx | 2 +- .../app-sidebar/dataset-sidebar-dropdown.tsx | 2 +- .../app/annotation/header-opts/index.spec.tsx | 7 +- .../components/app/app-publisher/index.tsx | 2 +- .../config-var/config-modal/index.tsx | 4 +- .../app/configuration/config-var/index.tsx | 2 +- .../config-var/select-type-item/index.tsx | 2 +- .../config/automatic/get-automatic-res.tsx | 4 +- .../dataset-config/settings-modal/index.tsx | 4 +- web/app/components/app/log/filter.tsx | 2 +- .../components/app/workflow-log/filter.tsx | 2 +- web/app/components/base/block-input/index.tsx | 2 +- .../base/chat/embedded-chatbot/hooks.tsx | 3 +- .../base/date-and-time-picker/hooks.ts | 4 +- .../base/encrypted-bottom/index.tsx | 4 +- .../text-to-speech/param-config-content.tsx | 4 +- .../components/base/file-uploader/hooks.ts | 4 +- .../field/input-type-select/hooks.tsx | 2 +- .../components/base/image-uploader/hooks.ts | 4 +- web/app/components/base/tag-input/index.tsx | 2 +- .../pricing/plans/cloud-plan-item/index.tsx | 4 +- .../plans/self-hosted-plan-item/button.tsx | 2 +- .../plans/self-hosted-plan-item/index.tsx | 8 +- .../self-hosted-plan-item/list/index.tsx | 4 +- .../billing/priority-label/index.tsx | 4 +- .../components/billing/upgrade-btn/index.tsx | 6 +- .../custom/custom-web-app-brand/index.tsx | 2 +- .../common/image-uploader/hooks/use-upload.ts | 4 +- .../common/retrieval-method-info/index.tsx | 4 +- .../list/template-card/content.tsx | 2 +- .../create/embedding-process/index.tsx | 2 +- .../datasets/create/file-uploader/index.tsx | 2 +- .../datasets/create/top-bar/index.tsx | 2 +- .../data-source/local-file/index.tsx | 2 +- .../embedding-process/rule-detail.tsx | 2 +- .../detail/batch-modal/csv-uploader.tsx | 2 +- .../documents/detail/embedding/index.tsx | 2 +- .../components/query-input/index.tsx | 2 +- .../datasets/list/dataset-card/index.tsx | 8 +- web/app/components/explore/category.tsx | 2 +- .../goto-anything/actions/commands/theme.tsx | 6 +- .../goto-anything/command-selector.tsx | 4 +- web/app/components/goto-anything/index.tsx | 4 +- .../account-setting/language-page/index.tsx | 3 +- .../invite-modal/role-selector.tsx | 2 +- .../members-page/operation/index.tsx | 4 +- .../presets-parameter.tsx | 2 +- .../plugins/base/deprecation-notice.tsx | 4 +- web/app/components/plugins/card/index.tsx | 7 +- web/app/components/plugins/hooks.ts | 6 +- .../plugins/marketplace/description/index.tsx | 5 +- .../components/plugins/marketplace/index.tsx | 3 +- .../plugins/marketplace/list/card-wrapper.tsx | 3 +- .../plugins/marketplace/list/index.tsx | 3 +- .../marketplace/list/list-with-collection.tsx | 3 +- .../plugins/marketplace/list/list-wrapper.tsx | 3 +- .../subscription-list/create/common-modal.tsx | 2 +- .../subscription-list/create/index.tsx | 2 +- web/app/components/plugins/types.ts | 6 +- .../hooks/use-available-nodes-meta-data.ts | 4 +- .../hooks/use-pipeline-template.ts | 2 +- .../get-schema.tsx | 2 +- .../edit-custom-collection-modal/index.tsx | 2 +- .../edit-custom-collection-modal/test-api.tsx | 2 +- web/app/components/tools/provider/empty.tsx | 6 +- .../hooks/use-available-nodes-meta-data.ts | 4 +- .../hooks/use-workflow-template.ts | 6 +- .../workflow/block-selector/blocks.tsx | 2 +- .../block-selector/featured-tools.tsx | 5 +- .../block-selector/featured-triggers.tsx | 5 +- .../workflow/block-selector/hooks.ts | 4 +- .../workflow/block-selector/start-blocks.tsx | 8 +- .../workflow/hooks/use-checklist.ts | 6 +- .../components/variable/output-var-list.tsx | 2 +- .../_base/components/variable/var-list.tsx | 2 +- .../components/operation-selector.tsx | 6 +- .../workflow/nodes/assigner/node.tsx | 2 +- .../components/condition-files-list-value.tsx | 8 +- .../condition-list/condition-item.tsx | 4 +- .../condition-list/condition-operator.tsx | 4 +- .../if-else/components/condition-value.tsx | 4 +- .../nodes/iteration/use-interactions.ts | 2 +- .../components/dataset-item.tsx | 2 +- .../condition-list/condition-operator.tsx | 4 +- .../components/filter-condition.tsx | 4 +- .../llm/components/config-prompt-item.tsx | 2 +- .../components/condition-files-list-value.tsx | 8 +- .../condition-list/condition-item.tsx | 4 +- .../condition-list/condition-operator.tsx | 4 +- .../nodes/loop/components/condition-value.tsx | 4 +- .../loop/components/loop-variables/item.tsx | 2 +- .../components/extract-parameter/update.tsx | 2 +- .../nodes/start/components/var-list.tsx | 2 +- .../workflow/nodes/start/use-config.ts | 2 +- .../components/mode-switcher.tsx | 4 +- .../nodes/trigger-webhook/use-config.ts | 4 +- .../components/node-variable-item.tsx | 2 +- .../components/var-group-item.tsx | 2 +- .../components/variable-modal.tsx | 2 +- .../panel/env-panel/variable-modal.tsx | 2 +- .../workflow/run/loop-result-panel.tsx | 2 +- .../forgot-password/ForgotPasswordForm.tsx | 2 +- web/app/install/installForm.tsx | 4 +- web/app/signin/invite-settings/page.tsx | 3 +- web/hooks/use-format-time-from-now.ts | 26 +-- web/hooks/use-knowledge.ts | 4 +- web/hooks/use-metadata.ts | 12 +- web/i18n-config/README.md | 30 +--- web/i18n-config/auto-gen-i18n.js | 6 +- web/i18n-config/check-i18n-sync.js | 134 --------------- web/i18n-config/check-i18n.js | 6 +- web/i18n-config/generate-i18n-types.js | 149 ---------------- web/i18n-config/i18next-config.ts | 128 +++++++------- web/i18n-config/index.ts | 5 +- web/i18n-config/language.ts | 66 ++++---- web/i18n-config/languages.json | 158 ----------------- web/i18n-config/languages.ts | 160 ++++++++++++++++++ web/i18n-config/server.ts | 11 +- web/i18n/en-US/app-api.ts | 1 + web/i18n/en-US/app-debug.ts | 3 + web/i18n/en-US/app.ts | 6 + web/i18n/en-US/common.ts | 6 + web/i18n/en-US/dataset.ts | 1 + web/i18n/en-US/login.ts | 1 + web/i18n/en-US/plugin-trigger.ts | 1 + web/i18n/en-US/tools.ts | 1 + web/i18n/en-US/workflow.ts | 3 + web/i18n/ja-JP/app-api.ts | 1 + web/i18n/ja-JP/app-debug.ts | 3 + web/i18n/ja-JP/app.ts | 6 + web/i18n/ja-JP/common.ts | 6 + web/i18n/ja-JP/dataset.ts | 1 + web/i18n/ja-JP/login.ts | 1 + web/i18n/ja-JP/plugin-trigger.ts | 1 + web/i18n/ja-JP/tools.ts | 1 + web/i18n/ja-JP/workflow.ts | 3 + web/i18n/zh-Hans/app-api.ts | 1 + web/i18n/zh-Hans/app-debug.ts | 3 + web/i18n/zh-Hans/app.ts | 6 + web/i18n/zh-Hans/common.ts | 6 + web/i18n/zh-Hans/dataset.ts | 1 + web/i18n/zh-Hans/login.ts | 1 + web/i18n/zh-Hans/plugin-trigger.ts | 1 + web/i18n/zh-Hans/tools.ts | 1 + web/i18n/zh-Hans/workflow.ts | 3 + web/package.json | 8 +- web/pnpm-lock.yaml | 106 +----------- web/types/i18n.d.ts | 84 +-------- web/utils/format.ts | 27 +-- 155 files changed, 560 insertions(+), 1015 deletions(-) delete mode 100644 web/i18n-config/check-i18n-sync.js delete mode 100644 web/i18n-config/generate-i18n-types.js delete mode 100644 web/i18n-config/languages.json create mode 100644 web/i18n-config/languages.ts diff --git a/.github/workflows/translate-i18n-base-on-english.yml b/.github/workflows/translate-i18n-base-on-english.yml index 8bb82d5d44c..87e24a4f90d 100644 --- a/.github/workflows/translate-i18n-base-on-english.yml +++ b/.github/workflows/translate-i18n-base-on-english.yml @@ -1,4 +1,4 @@ -name: Check i18n Files and Create PR +name: Translate i18n Files Based on English on: push: @@ -67,25 +67,19 @@ jobs: working-directory: ./web run: pnpm run auto-gen-i18n ${{ env.FILE_ARGS }} - - name: Generate i18n type definitions - if: env.FILES_CHANGED == 'true' - working-directory: ./web - run: pnpm run gen:i18n-types - - name: Create Pull Request if: env.FILES_CHANGED == 'true' uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: 'chore(i18n): update translations based on en-US changes' - title: 'chore(i18n): translate i18n files and update type definitions' + title: 'chore(i18n): translate i18n files based on en-US changes' body: | - This PR was automatically created to update i18n files and TypeScript type definitions based on changes in en-US locale. + This PR was automatically created to update i18n translation files based on changes in en-US locale. **Triggered by:** ${{ github.sha }} **Changes included:** - Updated translation files for all locales - - Regenerated TypeScript type definitions for type safety branch: chore/automated-i18n-updates-${{ github.sha }} delete-branch: true diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index adf52a13620..1a8925e38de 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -38,9 +38,6 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Check i18n types synchronization - run: pnpm run check:i18n-types - - name: Run tests run: pnpm test:coverage diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx index e9877f17157..34bb0f82f8c 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view.tsx @@ -104,7 +104,7 @@ const CardView: FC = ({ appId, isInPanel, className }) => { notify({ type, - message: t(`common.actionMsg.${message}`), + message: t(`common.actionMsg.${message}` as any) as string, }) } diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx index 557b723259a..55ed906a45b 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/long-time-range-picker.tsx @@ -53,7 +53,7 @@ const LongTimeRangePicker: FC = ({ return ( ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} + items={Object.entries(periodMapping).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))} className="mt-0 !w-40" notClearable={true} onSelect={handleSelect} diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index be7181c7593..88cb79ce0d4 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -66,7 +66,7 @@ const RangeSelector: FC = ({ }, []) return ( ({ ...v, name: t(`appLog.filter.period.${v.name}`) }))} + items={ranges.map(v => ({ ...v, name: t(`appLog.filter.period.${v.name}` as any) as string }))} className="mt-0 !w-40" notClearable={true} onSelect={handleSelectRange} diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx index 9dfeaef5280..aa64df34497 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/settings/page.tsx @@ -1,15 +1,15 @@ import * as React from 'react' import Form from '@/app/components/datasets/settings/form' -import { getLocaleOnServer, useTranslation as translate } from '@/i18n-config/server' +import { getLocaleOnServer, getTranslation } from '@/i18n-config/server' const Settings = async () => { const locale = await getLocaleOnServer() - const { t } = await translate(locale, 'dataset-settings') + const { t } = await getTranslation(locale, 'dataset-settings') return (
-
{t('title')}
+
{t('title') as any}
{t('desc')}
diff --git a/web/app/components/app-sidebar/dataset-info/index.tsx b/web/app/components/app-sidebar/dataset-info/index.tsx index ce409ff13a8..39a11150626 100644 --- a/web/app/components/app-sidebar/dataset-info/index.tsx +++ b/web/app/components/app-sidebar/dataset-info/index.tsx @@ -73,7 +73,7 @@ const DatasetInfo: FC = ({ {isExternalProvider && t('dataset.externalTag')} {!isExternalProvider && isPipelinePublished && dataset.doc_form && dataset.indexing_technique && (
- {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string} {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}
)} diff --git a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx index d8e26826caa..0e55a8af650 100644 --- a/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx +++ b/web/app/components/app-sidebar/dataset-sidebar-dropdown.tsx @@ -116,7 +116,7 @@ const DatasetSidebarDropdown = ({ {isExternalProvider && t('dataset.externalTag')} {!isExternalProvider && dataset.doc_form && dataset.indexing_technique && (
- {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string} {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)}
)} diff --git a/web/app/components/app/annotation/header-opts/index.spec.tsx b/web/app/components/app/annotation/header-opts/index.spec.tsx index c742f8effc7..c52507fb227 100644 --- a/web/app/components/app/annotation/header-opts/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/index.spec.tsx @@ -1,5 +1,6 @@ import type { ComponentProps } from 'react' import type { AnnotationItemBasic } from '../type' +import type { Locale } from '@/i18n-config' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' @@ -166,7 +167,7 @@ type HeaderOptionsProps = ComponentProps const renderComponent = ( props: Partial = {}, - locale: string = LanguagesSupported[0] as string, + locale: Locale = LanguagesSupported[0], ) => { const defaultProps: HeaderOptionsProps = { appId: 'test-app-id', @@ -353,7 +354,7 @@ describe('HeaderOptions', () => { }) const revokeSpy = vi.spyOn(URL, 'revokeObjectURL').mockImplementation(vi.fn()) - renderComponent({}, LanguagesSupported[1] as string) + renderComponent({}, LanguagesSupported[1]) await expandExportMenu(user) @@ -441,7 +442,7 @@ describe('HeaderOptions', () => { view.rerender( = ({ mode }) => { <>
- {t(`app.accessControlDialog.accessItems.${label}`)} + {t(`app.accessControlDialog.accessItems.${label}` as any) as string}
) diff --git a/web/app/components/app/configuration/config-var/config-modal/index.tsx b/web/app/components/app/configuration/config-var/config-modal/index.tsx index 41f37b5895e..b1d8e8cd193 100644 --- a/web/app/components/app/configuration/config-var/config-modal/index.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/index.tsx @@ -96,7 +96,7 @@ const ConfigModal: FC = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('appDebug.variableConfig.varName') }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: t('appDebug.variableConfig.varName') }) as string, }) return false } @@ -216,7 +216,7 @@ const ConfigModal: FC = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }) as string, }) return } diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 7a2a86393a3..bf528f8ca65 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -98,7 +98,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar if (errorMsgKey) { Toast.notify({ type: 'error', - message: t(errorMsgKey, { key: t(typeName) }), + message: t(errorMsgKey as any, { key: t(typeName as any) as string }) as string, }) return false } diff --git a/web/app/components/app/configuration/config-var/select-type-item/index.tsx b/web/app/components/app/configuration/config-var/select-type-item/index.tsx index ccb958977c8..a72a74a5e96 100644 --- a/web/app/components/app/configuration/config-var/select-type-item/index.tsx +++ b/web/app/components/app/configuration/config-var/select-type-item/index.tsx @@ -23,7 +23,7 @@ const SelectTypeItem: FC = ({ onClick, }) => { const { t } = useTranslation() - const typeName = t(`appDebug.variableConfig.${i18nFileTypeMap[type] || type}`) + const typeName = t(`appDebug.variableConfig.${i18nFileTypeMap[type] || type}` as any) as string return (
= ({ const [editorKey, setEditorKey] = useState(`${flowId}-0`) const handleChooseTemplate = useCallback((key: string) => { return () => { - const template = t(`appDebug.generate.template.${key}.instruction`) + const template = t(`appDebug.generate.template.${key}.instruction` as any) as string setInstruction(template) setEditorKey(`${flowId}-${Date.now()}`) } @@ -322,7 +322,7 @@ const GetAutomaticRes: FC = ({ ))} diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 1454ab0c624..243aafdbddc 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -295,7 +295,7 @@ const SettingsModal: FC = ({ isExternal rowClass={rowClass} labelClass={labelClass} - t={t} + t={t as any} topK={topK} scoreThreshold={scoreThreshold} scoreThresholdEnabled={scoreThresholdEnabled} @@ -308,7 +308,7 @@ const SettingsModal: FC = ({ isExternal={false} rowClass={rowClass} labelClass={labelClass} - t={t} + t={t as any} indexMethod={indexMethod} retrievalConfig={retrievalConfig} showMultiModalTip={showMultiModalTip} diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index 4a0103449f8..26c21e6cf6f 100644 --- a/web/app/components/app/log/filter.tsx +++ b/web/app/components/app/log/filter.tsx @@ -50,7 +50,7 @@ const Filter: FC = ({ isChatMode, appId, queryParams, setQueryPara setQueryParams({ ...queryParams, period: item.value }) }} onClear={() => setQueryParams({ ...queryParams, period: '9' })} - items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} + items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))} /> = ({ queryParams, setQueryParams }: IFilterProps) setQueryParams({ ...queryParams, period: item.value }) }} onClear={() => setQueryParams({ ...queryParams, period: '9' })} - items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}`) }))} + items={Object.entries(TIME_PERIOD_MAPPING).map(([k, v]) => ({ value: k, name: t(`appLog.filter.period.${v.name}` as any) as string }))} /> = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }) as string, }) return } diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 24df08f8a86..3c7fd576a39 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -3,6 +3,7 @@ import type { ChatItem, Feedback, } from '../types' +import type { Locale } from '@/i18n-config' import type { // AppData, ConversationItem, @@ -93,7 +94,7 @@ export const useEmbeddedChatbot = () => { if (localeParam) { // If locale parameter exists in URL, use it instead of default - await changeLanguage(localeParam) + await changeLanguage(localeParam as Locale) } else if (localeFromSysVar) { // If locale is set as a system variable, use that diff --git a/web/app/components/base/date-and-time-picker/hooks.ts b/web/app/components/base/date-and-time-picker/hooks.ts index f79f28053fa..ba66873cc03 100644 --- a/web/app/components/base/date-and-time-picker/hooks.ts +++ b/web/app/components/base/date-and-time-picker/hooks.ts @@ -6,7 +6,7 @@ const YEAR_RANGE = 100 export const useDaysOfWeek = () => { const { t } = useTranslation() - const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => t(`time.daysInWeek.${day}`)) + const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => t(`time.daysInWeek.${day}` as any) as string) return daysOfWeek } @@ -26,7 +26,7 @@ export const useMonths = () => { 'October', 'November', 'December', - ].map(month => t(`time.months.${month}`)) + ].map(month => t(`time.months.${month}` as any) as string) return months } diff --git a/web/app/components/base/encrypted-bottom/index.tsx b/web/app/components/base/encrypted-bottom/index.tsx index ff75d53db64..75b3b8151d5 100644 --- a/web/app/components/base/encrypted-bottom/index.tsx +++ b/web/app/components/base/encrypted-bottom/index.tsx @@ -16,7 +16,7 @@ export const EncryptedBottom = (props: Props) => { return (
- {t(frontTextKey || 'common.provider.encrypted.front')} + {t((frontTextKey || 'common.provider.encrypted.front') as any) as string} { > PKCS1_OAEP - {t(backTextKey || 'common.provider.encrypted.back')} + {t((backTextKey || 'common.provider.encrypted.back') as any) as string}
) } diff --git a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx index fc1052e1724..ca407a69ceb 100644 --- a/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx +++ b/web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx @@ -97,7 +97,7 @@ const VoiceParamConfig = ({ className="h-full w-full cursor-pointer rounded-lg border-0 bg-components-input-bg-normal py-1.5 pl-3 pr-10 focus-visible:bg-state-base-hover focus-visible:outline-none group-hover:bg-state-base-hover sm:text-sm sm:leading-6" > - {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder} + {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}` as any) as string : localLanguagePlaceholder} - {t(`common.voice.language.${(item.value).toString().replace('-', '')}`)} + {t(`common.voice.language.${(item.value).toString().replace('-', '')}` as any) as string} {(selected || item.value === text2speech?.language) && ( { handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) }, onErrorCallback: (error?: any) => { - const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t) + const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t as any) notify({ type: 'error', message: errorMessage }) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, @@ -287,7 +287,7 @@ export const useFile = (fileConfig: FileUpload, noNeedToCheckEnable = true) => { handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) }, onErrorCallback: (error?: any) => { - const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t) + const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t as any) notify({ type: 'error', message: errorMessage }) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, diff --git a/web/app/components/base/form/components/field/input-type-select/hooks.tsx b/web/app/components/base/form/components/field/input-type-select/hooks.tsx index 67621fef67f..eb7da8d9d0e 100644 --- a/web/app/components/base/form/components/field/input-type-select/hooks.tsx +++ b/web/app/components/base/form/components/field/input-type-select/hooks.tsx @@ -44,7 +44,7 @@ export const useInputTypeOptions = (supportFile: boolean) => { return options.map((value) => { return { value, - label: t(`appDebug.variableConfig.${i18nFileTypeMap[value] || value}`), + label: t(`appDebug.variableConfig.${i18nFileTypeMap[value] || value}` as any), Icon: INPUT_TYPE_ICON[value], type: DATA_TYPE[value], } diff --git a/web/app/components/base/image-uploader/hooks.ts b/web/app/components/base/image-uploader/hooks.ts index 065a808d33b..f098c378eba 100644 --- a/web/app/components/base/image-uploader/hooks.ts +++ b/web/app/components/base/image-uploader/hooks.ts @@ -82,7 +82,7 @@ export const useImageFiles = () => { setFiles(newFiles) }, onErrorCallback: (error?: any) => { - const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t) + const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t as any) notify({ type: 'error', message: errorMessage }) const newFiles = [...files.slice(0, index), { ...currentImageFile, progress: -1 }, ...files.slice(index + 1)] filesRef.current = newFiles @@ -160,7 +160,7 @@ export const useLocalFileUploader = ({ limit, disabled = false, onUpload }: useL onUpload({ ...imageFile, fileId: res.id, progress: 100 }) }, onErrorCallback: (error?: any) => { - const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t) + const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t as any) notify({ type: 'error', message: errorMessage }) onUpload({ ...imageFile, progress: -1 }) }, diff --git a/web/app/components/base/tag-input/index.tsx b/web/app/components/base/tag-input/index.tsx index 32415435651..6fe0016a1b3 100644 --- a/web/app/components/base/tag-input/index.tsx +++ b/web/app/components/base/tag-input/index.tsx @@ -127,7 +127,7 @@ const TagInput: FC = ({ setValue(e.target.value) }} onKeyDown={handleKeyDown} - placeholder={t(placeholder || (isSpecialMode ? 'common.model.params.stop_sequencesPlaceholder' : 'datasetDocuments.segment.addKeyWord'))} + placeholder={t((placeholder || (isSpecialMode ? 'common.model.params.stop_sequencesPlaceholder' : 'datasetDocuments.segment.addKeyWord')) as any)} />
) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 57c85cf297f..345c915c2b7 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -106,7 +106,7 @@ const CloudPlanItem: FC = ({ {ICON_MAP[plan]}
-
{t(`${i18nPrefix}.name`)}
+
{t(`${i18nPrefix}.name` as any) as string}
{ isMostPopularPlan && (
@@ -117,7 +117,7 @@ const CloudPlanItem: FC = ({ ) }
-
{t(`${i18nPrefix}.description`)}
+
{t(`${i18nPrefix}.description` as any) as string}
{/* Price */} diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx index 544141a6a5e..73c7f31cb51 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/button.tsx @@ -42,7 +42,7 @@ const Button = ({ onClick={handleGetPayUrl} >
- {t(`${i18nPrefix}.btnText`)} + {t(`${i18nPrefix}.btnText` as any) as string} {isPremiumPlan && ( diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx index b89d0c69418..a1880af523f 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx @@ -85,16 +85,16 @@ const SelfHostedPlanItem: FC = ({
{STYLE_MAP[plan].icon}
-
{t(`${i18nPrefix}.name`)}
-
{t(`${i18nPrefix}.description`)}
+
{t(`${i18nPrefix}.name` as any) as string}
+
{t(`${i18nPrefix}.description` as any) as string}
{/* Price */}
-
{t(`${i18nPrefix}.price`)}
+
{t(`${i18nPrefix}.price` as any) as string}
{!isFreePlan && ( - {t(`${i18nPrefix}.priceTip`)} + {t(`${i18nPrefix}.priceTip` as any) as string} )}
diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx index 4ed307d36ef..e7828decb90 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx @@ -12,13 +12,13 @@ const List = ({ }: ListProps) => { const { t } = useTranslation() const i18nPrefix = `billing.plans.${plan}` - const features = t(`${i18nPrefix}.features`, { returnObjects: true }) as string[] + const features = t(`${i18nPrefix}.features` as any, { returnObjects: true }) as unknown as string[] return (
}} />
diff --git a/web/app/components/billing/priority-label/index.tsx b/web/app/components/billing/priority-label/index.tsx index e5a6857078c..b66fdc7ea9e 100644 --- a/web/app/components/billing/priority-label/index.tsx +++ b/web/app/components/billing/priority-label/index.tsx @@ -31,7 +31,7 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { return ( -
{`${t('billing.plansCommon.documentProcessingPriority')}: ${t(`billing.plansCommon.priority.${priority}`)}`}
+
{`${t('billing.plansCommon.documentProcessingPriority')}: ${t(`billing.plansCommon.priority.${priority}` as any) as string}`}
{ priority !== DocumentProcessingPriority.topPriority && (
{t('billing.plansCommon.documentProcessingPriorityTip')}
@@ -51,7 +51,7 @@ const PriorityLabel = ({ className }: PriorityLabelProps) => { ) } - {t(`billing.plansCommon.priority.${priority}`)} + {t(`billing.plansCommon.priority.${priority}` as any) as string}
) diff --git a/web/app/components/billing/upgrade-btn/index.tsx b/web/app/components/billing/upgrade-btn/index.tsx index 0f23022b352..2a43090d84b 100644 --- a/web/app/components/billing/upgrade-btn/index.tsx +++ b/web/app/components/billing/upgrade-btn/index.tsx @@ -46,8 +46,8 @@ const UpgradeBtn: FC = ({ } } - const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}`) - const label = labelKey ? t(labelKey) : defaultBadgeLabel + const defaultBadgeLabel = t(`billing.upgradeBtn.${isShort ? 'encourageShort' : 'encourage'}` as any) as string + const label = labelKey ? t(labelKey as any) as string : defaultBadgeLabel if (isPlain) { return ( @@ -56,7 +56,7 @@ const UpgradeBtn: FC = ({ style={style} onClick={onClick} > - {labelKey ? label : t('billing.upgradeBtn.plain')} + {labelKey ? label : t('billing.upgradeBtn.plain' as any) as string} ) } diff --git a/web/app/components/custom/custom-web-app-brand/index.tsx b/web/app/components/custom/custom-web-app-brand/index.tsx index 3c6557dc632..6a86441bd42 100644 --- a/web/app/components/custom/custom-web-app-brand/index.tsx +++ b/web/app/components/custom/custom-web-app-brand/index.tsx @@ -68,7 +68,7 @@ const CustomWebAppBrand = () => { setFileId(res.id) }, onErrorCallback: (error?: any) => { - const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t) + const errorMessage = getImageUploadErrorMessage(error, t('common.imageUploader.uploadFromComputerUploadError'), t as any) notify({ type: 'error', message: errorMessage }) setUploadProgress(-1) }, diff --git a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts index 7a0868b14c0..44bde33a967 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts +++ b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts @@ -145,7 +145,7 @@ export const useUpload = () => { handleUpdateFile({ ...uploadingFile, uploadedId: res.id, progress: 100 }) }, onErrorCallback: (error?: any) => { - const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t) + const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t as any) Toast.notify({ type: 'error', message: errorMessage }) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, @@ -187,7 +187,7 @@ export const useUpload = () => { }) }, onErrorCallback: (error?: any) => { - const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t) + const errorMessage = getFileUploadErrorMessage(error, t('common.fileUploader.uploadFromComputerUploadError'), t as any) Toast.notify({ type: 'error', message: errorMessage }) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, diff --git a/web/app/components/datasets/common/retrieval-method-info/index.tsx b/web/app/components/datasets/common/retrieval-method-info/index.tsx index df8a93f6661..3ab33e02781 100644 --- a/web/app/components/datasets/common/retrieval-method-info/index.tsx +++ b/web/app/components/datasets/common/retrieval-method-info/index.tsx @@ -33,8 +33,8 @@ const EconomicalRetrievalMethodConfig: FC = ({
- {t(`dataset.chunkingMode.${DOC_FORM_TEXT[chunkStructure]}`)} + {t(`dataset.chunkingMode.${DOC_FORM_TEXT[chunkStructure]}` as any) as string}
diff --git a/web/app/components/datasets/create/embedding-process/index.tsx b/web/app/components/datasets/create/embedding-process/index.tsx index 541eb62b60d..d2050268aab 100644 --- a/web/app/components/datasets/create/embedding-process/index.tsx +++ b/web/app/components/datasets/create/embedding-process/index.tsx @@ -141,7 +141,7 @@ const RuleDetail: FC<{ { - const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t) + const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t as any) notify({ type: 'error', message: errorMessage }) onFileUpdate(fileItem, -2, fileListRef.current) return Promise.resolve({ ...fileItem }) diff --git a/web/app/components/datasets/create/top-bar/index.tsx b/web/app/components/datasets/create/top-bar/index.tsx index 3f30d9a8da7..632ad3ab73b 100644 --- a/web/app/components/datasets/create/top-bar/index.tsx +++ b/web/app/components/datasets/create/top-bar/index.tsx @@ -39,7 +39,7 @@ export const TopBar: FC = (props) => {
({ - name: t(STEP_T_MAP[i + 1]), + name: t(STEP_T_MAP[i + 1] as any) as string, }))} {...rest} /> diff --git a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx index 31570ef4cf4..c36155e104a 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/data-source/local-file/index.tsx @@ -155,7 +155,7 @@ const LocalFile = ({ return Promise.resolve({ ...completeFile }) }) .catch((e) => { - const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t) + const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t as any) notify({ type: 'error', message: errorMessage }) updateFile(fileItem, -2, fileListRef.current) return Promise.resolve({ ...fileItem }) diff --git a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/rule-detail.tsx b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/rule-detail.tsx index a16e284bcfa..55590636a60 100644 --- a/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/rule-detail.tsx +++ b/web/app/components/datasets/documents/create-from-pipeline/processing/embedding-process/rule-detail.tsx @@ -63,7 +63,7 @@ const RuleDetail = ({ /> = ({ return Promise.resolve({ ...completeFile }) }) .catch((e) => { - const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t) + const errorMessage = getFileUploadErrorMessage(e, t('datasetCreation.stepOne.uploader.failed'), t as any) notify({ type: 'error', message: errorMessage }) const errorFile = { ...fileItem, diff --git a/web/app/components/datasets/documents/detail/embedding/index.tsx b/web/app/components/datasets/documents/detail/embedding/index.tsx index db83d89c404..2978ec56814 100644 --- a/web/app/components/datasets/documents/detail/embedding/index.tsx +++ b/web/app/components/datasets/documents/detail/embedding/index.tsx @@ -133,7 +133,7 @@ const RuleDetail: FC = React.memo(({ /> {icon} -
{t(`dataset.retrieval.${retrievalMethod}.title`)}
+
{t(`dataset.retrieval.${retrievalMethod}.title` as any) as string}
)} diff --git a/web/app/components/datasets/list/dataset-card/index.tsx b/web/app/components/datasets/list/dataset-card/index.tsx index 8087b80fda5..e72349db3a3 100644 --- a/web/app/components/datasets/list/dataset-card/index.tsx +++ b/web/app/components/datasets/list/dataset-card/index.tsx @@ -217,17 +217,17 @@ const DatasetCard = ({ {dataset.doc_form && ( - {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`)} + {t(`dataset.chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}` as any) as string} )} {dataset.indexing_technique && ( - {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method)} + {formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method) as any} )} {dataset.is_multimodal && ( diff --git a/web/app/components/explore/category.tsx b/web/app/components/explore/category.tsx index eba883d8498..593ed8d938c 100644 --- a/web/app/components/explore/category.tsx +++ b/web/app/components/explore/category.tsx @@ -50,7 +50,7 @@ const Category: FC = ({ className={itemClassName(name === value)} onClick={() => onChange(name)} > - {(categoryI18n as any)[name] ? t(`explore.category.${name}`) : name} + {(categoryI18n as any)[name] ? t(`explore.category.${name}` as any) as string : name}
))}
diff --git a/web/app/components/goto-anything/actions/commands/theme.tsx b/web/app/components/goto-anything/actions/commands/theme.tsx index dc8ca46bc03..34df84e33b7 100644 --- a/web/app/components/goto-anything/actions/commands/theme.tsx +++ b/web/app/components/goto-anything/actions/commands/theme.tsx @@ -36,13 +36,13 @@ const buildThemeCommands = (query: string, locale?: string): CommandSearchResult const q = query.toLowerCase() const list = THEME_ITEMS.filter(item => !q - || i18n.t(item.titleKey, { lng: locale }).toLowerCase().includes(q) + || i18n.t(item.titleKey as any, { lng: locale }).toLowerCase().includes(q) || item.id.includes(q), ) return list.map(item => ({ id: item.id, - title: i18n.t(item.titleKey, { lng: locale }), - description: i18n.t(item.descKey, { lng: locale }), + title: i18n.t(item.titleKey as any, { lng: locale }), + description: i18n.t(item.descKey as any, { lng: locale }), type: 'command' as const, icon: (
diff --git a/web/app/components/goto-anything/command-selector.tsx b/web/app/components/goto-anything/command-selector.tsx index 0d89669ec83..f62a7a38290 100644 --- a/web/app/components/goto-anything/command-selector.tsx +++ b/web/app/components/goto-anything/command-selector.tsx @@ -117,7 +117,7 @@ const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, co '/community': 'app.gotoAnything.actions.communityDesc', '/zen': 'app.gotoAnything.actions.zenDesc', } - return t(slashKeyMap[item.key] || item.description) + return t((slashKeyMap[item.key] || item.description) as any) })() ) : ( @@ -128,7 +128,7 @@ const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, co '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', } - return t(keyMap[item.key]) + return t(keyMap[item.key] as any) as string })() )} diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index 1c61cb9516a..76c2e26ebd5 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -243,7 +243,7 @@ const GotoAnything: FC = ({ knowledge: 'app.gotoAnything.emptyState.noKnowledgeBasesFound', node: 'app.gotoAnything.emptyState.noWorkflowNodesFound', } - return t(keyMap[commandType] || 'app.gotoAnything.noResults') + return t((keyMap[commandType] || 'app.gotoAnything.noResults') as any) })() : t('app.gotoAnything.noResults')}
@@ -410,7 +410,7 @@ const GotoAnything: FC = ({ 'workflow-node': 'app.gotoAnything.groups.workflowNodes', 'command': 'app.gotoAnything.groups.commands', } - return t(typeMap[type] || `${type}s`) + return t((typeMap[type] || `${type}s`) as any) })()} className="p-2 capitalize text-text-secondary" > diff --git a/web/app/components/header/account-setting/language-page/index.tsx b/web/app/components/header/account-setting/language-page/index.tsx index 00db4dbbafa..0568cb1ec99 100644 --- a/web/app/components/header/account-setting/language-page/index.tsx +++ b/web/app/components/header/account-setting/language-page/index.tsx @@ -1,6 +1,7 @@ 'use client' import type { Item } from '@/app/components/base/select' +import type { Locale } from '@/i18n-config' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' @@ -32,7 +33,7 @@ export default function LanguagePage() { await updateUserProfile({ url, body: { [bodyKey]: item.value } }) notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) - setLocaleOnClient(item.value.toString()) + setLocaleOnClient(item.value.toString() as Locale) } catch (e) { notify({ type: 'error', message: (e as Error).message }) diff --git a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx index 7d8169e4c48..07f89df24d7 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/role-selector.tsx @@ -36,7 +36,7 @@ const RoleSelector = ({ value, onChange }: RoleSelectorProps) => { className="block" >
-
{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}`) })}
+
{t('common.members.invitedAsRole', { role: t(`common.members.${toHump(value)}` as any) })}
diff --git a/web/app/components/header/account-setting/members-page/operation/index.tsx b/web/app/components/header/account-setting/members-page/operation/index.tsx index 2b3c9e350a6..da617466853 100644 --- a/web/app/components/header/account-setting/members-page/operation/index.tsx +++ b/web/app/components/header/account-setting/members-page/operation/index.tsx @@ -106,8 +106,8 @@ const Operation = ({ :
}
-
{t(`common.members.${toHump(role)}`)}
-
{t(`common.members.${toHump(role)}Tip`)}
+
{t(`common.members.${toHump(role)}` as any)}
+
{t(`common.members.${toHump(role)}Tip` as any)}
diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx index d53467028c9..9e8637ce49d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/presets-parameter.tsx @@ -44,7 +44,7 @@ const PresetsParameter: FC = ({ text: (
{getToneIcon(tone.id)} - {t(`common.model.tone.${tone.name}`) as string} + {t(`common.model.tone.${tone.name}` as any) as string}
), } diff --git a/web/app/components/plugins/base/deprecation-notice.tsx b/web/app/components/plugins/base/deprecation-notice.tsx index 8832c779610..76d64a45f47 100644 --- a/web/app/components/plugins/base/deprecation-notice.tsx +++ b/web/app/components/plugins/base/deprecation-notice.tsx @@ -82,7 +82,7 @@ const DeprecationNotice: FC = ({ ), }} values={{ - deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`), + deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}` as any) as string, alternativePluginId, }} /> @@ -91,7 +91,7 @@ const DeprecationNotice: FC = ({ { hasValidDeprecatedReason && !alternativePluginId && ( - {t(`${i18nPrefix}.onlyReason`, { deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}`) })} + {t(`${i18nPrefix}.onlyReason` as any, { deprecatedReason: t(`${i18nPrefix}.reason.${deprecatedReasonKey}` as any) as string }) as string} ) } diff --git a/web/app/components/plugins/card/index.tsx b/web/app/components/plugins/card/index.tsx index 1cb15bf70b1..f063a8d5728 100644 --- a/web/app/components/plugins/card/index.tsx +++ b/web/app/components/plugins/card/index.tsx @@ -1,11 +1,14 @@ 'use client' import type { Plugin } from '../types' +import type { Locale } from '@/i18n-config' import { RiAlertFill } from '@remixicon/react' import * as React from 'react' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' -import { renderI18nObject } from '@/i18n-config' +import { + renderI18nObject, +} from '@/i18n-config' import { getLanguage } from '@/i18n-config/language' import { Theme } from '@/types/app' import { cn } from '@/utils/classnames' @@ -30,7 +33,7 @@ export type Props = { footer?: React.ReactNode isLoading?: boolean loadingFileName?: string - locale?: string + locale?: Locale limitedInstall?: boolean } diff --git a/web/app/components/plugins/hooks.ts b/web/app/components/plugins/hooks.ts index 8303a4cc460..423ce1023ce 100644 --- a/web/app/components/plugins/hooks.ts +++ b/web/app/components/plugins/hooks.ts @@ -20,7 +20,7 @@ export const useTags = (translateFromOut?: TFunction) => { return tagKeys.map((tag) => { return { name: tag, - label: t(`pluginTags.tags.${tag}`), + label: t(`pluginTags.tags.${tag}` as any) as string, } }) }, [t]) @@ -66,14 +66,14 @@ export const useCategories = (translateFromOut?: TFunction, isSingle?: boolean) } return { name: category, - label: isSingle ? t(`plugin.categorySingle.${category}`) : t(`plugin.category.${category}s`), + label: isSingle ? t(`plugin.categorySingle.${category}` as any) as string : t(`plugin.category.${category}s` as any) as string, } }) }, [t, isSingle]) const categoriesMap = useMemo(() => { return categories.reduce((acc, category) => { - acc[category.name] = category + acc[category.name] = category as any return acc }, {} as Record) }, [categories]) diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index 5b1ac6bb09a..66a6368c083 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -1,10 +1,11 @@ +import type { Locale } from '@/i18n-config' import { getLocaleOnServer, - useTranslation as translate, + getTranslation as translate, } from '@/i18n-config/server' type DescriptionProps = { - locale?: string + locale?: Locale } const Description = async ({ locale: localeFromProps, diff --git a/web/app/components/plugins/marketplace/index.tsx b/web/app/components/plugins/marketplace/index.tsx index 47acb840e49..ff9a4d60bc2 100644 --- a/web/app/components/plugins/marketplace/index.tsx +++ b/web/app/components/plugins/marketplace/index.tsx @@ -1,5 +1,6 @@ import type { MarketplaceCollection, SearchParams } from './types' import type { Plugin } from '@/app/components/plugins/types' +import type { Locale } from '@/i18n-config' import { TanstackQueryInitializer } from '@/context/query-client' import { MarketplaceContextProvider } from './context' import Description from './description' @@ -8,7 +9,7 @@ import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper' import { getMarketplaceCollectionsAndPlugins } from './utils' type MarketplaceProps = { - locale: string + locale: Locale showInstallButton?: boolean shouldExclude?: boolean searchParams?: SearchParams diff --git a/web/app/components/plugins/marketplace/list/card-wrapper.tsx b/web/app/components/plugins/marketplace/list/card-wrapper.tsx index 159107eb979..ddc505b0d83 100644 --- a/web/app/components/plugins/marketplace/list/card-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/card-wrapper.tsx @@ -1,5 +1,6 @@ 'use client' import type { Plugin } from '@/app/components/plugins/types' +import type { Locale } from '@/i18n-config' import { RiArrowRightUpLine } from '@remixicon/react' import { useBoolean } from 'ahooks' import { useTheme } from 'next-themes' @@ -17,7 +18,7 @@ import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '.. type CardWrapperProps = { plugin: Plugin showInstallButton?: boolean - locale?: string + locale?: Locale } const CardWrapperComponent = ({ plugin, diff --git a/web/app/components/plugins/marketplace/list/index.tsx b/web/app/components/plugins/marketplace/list/index.tsx index 95f7cb37a81..54889b232ff 100644 --- a/web/app/components/plugins/marketplace/list/index.tsx +++ b/web/app/components/plugins/marketplace/list/index.tsx @@ -1,6 +1,7 @@ 'use client' import type { Plugin } from '../../types' import type { MarketplaceCollection } from '../types' +import type { Locale } from '@/i18n-config' import { cn } from '@/utils/classnames' import Empty from '../empty' import CardWrapper from './card-wrapper' @@ -11,7 +12,7 @@ type ListProps = { marketplaceCollectionPluginsMap: Record plugins?: Plugin[] showInstallButton?: boolean - locale: string + locale: Locale cardContainerClassName?: string cardRender?: (plugin: Plugin) => React.JSX.Element | null onMoreClick?: () => void diff --git a/web/app/components/plugins/marketplace/list/list-with-collection.tsx b/web/app/components/plugins/marketplace/list/list-with-collection.tsx index c401fbe3b95..2d246efb821 100644 --- a/web/app/components/plugins/marketplace/list/list-with-collection.tsx +++ b/web/app/components/plugins/marketplace/list/list-with-collection.tsx @@ -3,6 +3,7 @@ import type { MarketplaceCollection } from '../types' import type { SearchParamsFromCollection } from '@/app/components/plugins/marketplace/types' import type { Plugin } from '@/app/components/plugins/types' +import type { Locale } from '@/i18n-config' import { RiArrowRightSLine } from '@remixicon/react' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' import { getLanguage } from '@/i18n-config/language' @@ -13,7 +14,7 @@ type ListWithCollectionProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record showInstallButton?: boolean - locale: string + locale: Locale cardContainerClassName?: string cardRender?: (plugin: Plugin) => React.JSX.Element | null onMoreClick?: (searchParams?: SearchParamsFromCollection) => void diff --git a/web/app/components/plugins/marketplace/list/list-wrapper.tsx b/web/app/components/plugins/marketplace/list/list-wrapper.tsx index f2fbd085f01..650c8a74479 100644 --- a/web/app/components/plugins/marketplace/list/list-wrapper.tsx +++ b/web/app/components/plugins/marketplace/list/list-wrapper.tsx @@ -1,6 +1,7 @@ 'use client' import type { Plugin } from '../../types' import type { MarketplaceCollection } from '../types' +import type { Locale } from '@/i18n-config' import { useEffect } from 'react' import Loading from '@/app/components/base/loading' import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks' @@ -12,7 +13,7 @@ type ListWrapperProps = { marketplaceCollections: MarketplaceCollection[] marketplaceCollectionPluginsMap: Record showInstallButton?: boolean - locale: string + locale: Locale } const ListWrapper = ({ marketplaceCollections, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index 16a789e67be..31e0bd6a854 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -330,7 +330,7 @@ export const CommonCreateModal = ({ onClose, createType, builder }: Props) => { return ( = MAX_COUNT ? t('pluginTrigger.subscription.maxCount', { num: MAX_COUNT }) : t(`pluginTrigger.subscription.addType.options.${methodType.toLowerCase()}.description`)} + popupContent={subscriptionCount >= MAX_COUNT ? t('pluginTrigger.subscription.maxCount', { num: MAX_COUNT }) : t(`pluginTrigger.subscription.addType.options.${methodType.toLowerCase()}.description` as any)} disabled={!(supportedMethods?.length === 1 || subscriptionCount >= MAX_COUNT)} > - brief: Record - description: Record + label: Partial> + brief: Partial> + description: Partial> // Repo readme.md content introduction: string repository: string diff --git a/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts b/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts index 6464534c833..45c36196f50 100644 --- a/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/rag-pipeline/hooks/use-available-nodes-meta-data.ts @@ -36,8 +36,8 @@ export const useAvailableNodesMetaData = () => { const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => { const { metaData } = node - const title = t(`workflow.blocks.${metaData.type}`) - const description = t(`workflow.blocksAbout.${metaData.type}`) + const title = t(`workflow.blocks.${metaData.type}` as any) as string + const description = t(`workflow.blocksAbout.${metaData.type}` as any) as string return { ...node, metaData: { diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline-template.ts b/web/app/components/rag-pipeline/hooks/use-pipeline-template.ts index 5955ee5c45f..fc64e6c8c70 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline-template.ts +++ b/web/app/components/rag-pipeline/hooks/use-pipeline-template.ts @@ -14,7 +14,7 @@ export const usePipelineTemplate = () => { data: { ...knowledgeBaseDefault.defaultValue as KnowledgeBaseNodeType, type: knowledgeBaseDefault.metaData.type, - title: t(`workflow.blocks.${knowledgeBaseDefault.metaData.type}`), + title: t(`workflow.blocks.${knowledgeBaseDefault.metaData.type}` as any) as string, selected: true, }, position: { diff --git a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx index 4ecee282f98..5c85aee32c1 100644 --- a/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/get-schema.tsx @@ -111,7 +111,7 @@ const GetSchema: FC = ({ }} className="system-sm-regular cursor-pointer whitespace-nowrap rounded-lg px-3 py-1.5 leading-5 text-text-secondary hover:bg-components-panel-on-panel-item-bg-hover" > - {t(`tools.createTool.exampleOptions.${item.key}`)} + {t(`tools.createTool.exampleOptions.${item.key}` as any) as string}
))}
diff --git a/web/app/components/tools/edit-custom-collection-modal/index.tsx b/web/app/components/tools/edit-custom-collection-modal/index.tsx index 93ef9142d90..474c2620108 100644 --- a/web/app/components/tools/edit-custom-collection-modal/index.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/index.tsx @@ -292,7 +292,7 @@ const EditCustomCollectionModal: FC = ({
{t('tools.createTool.authMethod.title')}
setCredentialsModalShow(true)}> -
{t(`tools.createTool.authMethod.types.${credential.auth_type}`)}
+
{t(`tools.createTool.authMethod.types.${credential.auth_type}` as any) as string}
diff --git a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx index 8aacb7ad072..30ead4425b1 100644 --- a/web/app/components/tools/edit-custom-collection-modal/test-api.tsx +++ b/web/app/components/tools/edit-custom-collection-modal/test-api.tsx @@ -78,7 +78,7 @@ const TestApi: FC = ({
{t('tools.createTool.authMethod.title')}
setCredentialsModalShow(true)}> -
{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}`)}
+
{t(`tools.createTool.authMethod.types.${tempCredential.auth_type}` as any) as string}
diff --git a/web/app/components/tools/provider/empty.tsx b/web/app/components/tools/provider/empty.tsx index 7e916ba62fd..e79607751e3 100644 --- a/web/app/components/tools/provider/empty.tsx +++ b/web/app/components/tools/provider/empty.tsx @@ -33,17 +33,17 @@ const Empty = ({ const Comp = (hasLink ? Link : 'div') as any const linkProps = hasLink ? { href: getLink(type), target: '_blank' } : {} const renderType = isAgent ? 'agent' : type - const hasTitle = t(`tools.addToolModal.${renderType}.title`) !== `tools.addToolModal.${renderType}.title` + const hasTitle = t(`tools.addToolModal.${renderType}.title` as any) as string !== `tools.addToolModal.${renderType}.title` return (
- {hasTitle ? t(`tools.addToolModal.${renderType}.title`) : 'No tools available'} + {hasTitle ? t(`tools.addToolModal.${renderType}.title` as any) as string : 'No tools available'}
{(!isAgent && hasTitle) && ( - {t(`tools.addToolModal.${renderType}.tip`)} + {t(`tools.addToolModal.${renderType}.tip` as any) as string} {' '} {hasLink && } diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 24d862321d6..7f08612234e 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -42,8 +42,8 @@ export const useAvailableNodesMetaData = () => { const availableNodesMetaData = useMemo(() => mergedNodesMetaData.map((node) => { const { metaData } = node - const title = t(`workflow.blocks.${metaData.type}`) - const description = t(`workflow.blocksAbout.${metaData.type}`) + const title = t(`workflow.blocks.${metaData.type}` as any) as string + const description = t(`workflow.blocksAbout.${metaData.type}` as any) as string const helpLinkPath = `guides/workflow/node/${metaData.helpLinkUri}` return { ...node, diff --git a/web/app/components/workflow-app/hooks/use-workflow-template.ts b/web/app/components/workflow-app/hooks/use-workflow-template.ts index b48a68e1eb4..46f2c3e0a3c 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-template.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-template.ts @@ -18,7 +18,7 @@ export const useWorkflowTemplate = () => { data: { ...startDefault.defaultValue as StartNodeType, type: startDefault.metaData.type, - title: t(`workflow.blocks.${startDefault.metaData.type}`), + title: t(`workflow.blocks.${startDefault.metaData.type}` as any) as string, }, position: START_INITIAL_POSITION, }) @@ -34,7 +34,7 @@ export const useWorkflowTemplate = () => { }, selected: true, type: llmDefault.metaData.type, - title: t(`workflow.blocks.${llmDefault.metaData.type}`), + title: t(`workflow.blocks.${llmDefault.metaData.type}` as any) as string, }, position: { x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET, @@ -48,7 +48,7 @@ export const useWorkflowTemplate = () => { ...answerDefault.defaultValue, answer: `{{#${llmNode.id}.text#}}`, type: answerDefault.metaData.type, - title: t(`workflow.blocks.${answerDefault.metaData.type}`), + title: t(`workflow.blocks.${answerDefault.metaData.type}` as any) as string, }, position: { x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET * 2, diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index e626073a3ad..51d08d15df9 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -85,7 +85,7 @@ const Blocks = ({ { classification !== '-' && !!filteredList.length && (
- {t(`workflow.tabs.${classification}`)} + {t(`workflow.tabs.${classification}` as any) as string}
) } diff --git a/web/app/components/workflow/block-selector/featured-tools.tsx b/web/app/components/workflow/block-selector/featured-tools.tsx index 1e6b9767392..343d56bd1a6 100644 --- a/web/app/components/workflow/block-selector/featured-tools.tsx +++ b/web/app/components/workflow/block-selector/featured-tools.tsx @@ -2,6 +2,7 @@ import type { ToolWithProvider } from '../types' import type { ToolDefaultValue, ToolValue } from './types' import type { Plugin } from '@/app/components/plugins/types' +import type { Locale } from '@/i18n-config' import { RiMoreLine } from '@remixicon/react' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' @@ -178,7 +179,7 @@ const FeaturedTools = ({ onInstallSuccess={async () => { await onInstallSuccess?.() }} - t={t} + t={t as any} /> ))}
@@ -221,7 +222,7 @@ const FeaturedTools = ({ type FeaturedToolUninstalledItemProps = { plugin: Plugin - language: string + language: Locale onInstallSuccess?: () => Promise | void t: (key: string, options?: Record) => string } diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index c986a8abc0b..66705a9d06d 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -1,6 +1,7 @@ 'use client' import type { TriggerDefaultValue, TriggerWithProvider } from './types' import type { Plugin } from '@/app/components/plugins/types' +import type { Locale } from '@/i18n-config' import { RiMoreLine } from '@remixicon/react' import Link from 'next/link' import { useEffect, useMemo, useState } from 'react' @@ -170,7 +171,7 @@ const FeaturedTriggers = ({ onInstallSuccess={async () => { await onInstallSuccess?.() }} - t={t} + t={t as any} /> ))}
@@ -213,7 +214,7 @@ const FeaturedTriggers = ({ type FeaturedTriggerUninstalledItemProps = { plugin: Plugin - language: string + language: Locale onInstallSuccess?: () => Promise | void t: (key: string, options?: Record) => string } diff --git a/web/app/components/workflow/block-selector/hooks.ts b/web/app/components/workflow/block-selector/hooks.ts index 075a0b7d386..462d58df9f9 100644 --- a/web/app/components/workflow/block-selector/hooks.ts +++ b/web/app/components/workflow/block-selector/hooks.ts @@ -17,7 +17,7 @@ export const useBlocks = () => { return BLOCKS.map((block) => { return { ...block, - title: t(`workflow.blocks.${block.type}`), + title: t(`workflow.blocks.${block.type}` as any) as string, } }) } @@ -28,7 +28,7 @@ export const useStartBlocks = () => { return START_BLOCKS.map((block) => { return { ...block, - title: t(`workflow.blocks.${block.type}`), + title: t(`workflow.blocks.${block.type}` as any) as string, } }) } diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index 5c4311b8056..b6aaef84f9e 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -43,7 +43,7 @@ const StartBlocks = ({ if (blockType === BlockEnumValues.TriggerWebhook) return t('workflow.customWebhook') - return t(`workflow.blocks.${blockType}`) + return t(`workflow.blocks.${blockType}` as any) as string } return START_BLOCKS.filter((block) => { @@ -83,10 +83,10 @@ const StartBlocks = ({
{block.type === BlockEnumValues.TriggerWebhook ? t('workflow.customWebhook') - : t(`workflow.blocks.${block.type}`)} + : t(`workflow.blocks.${block.type}` as any) as string}
- {t(`workflow.blocksAbout.${block.type}`)} + {t(`workflow.blocksAbout.${block.type}` as any) as string}
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
@@ -107,7 +107,7 @@ const StartBlocks = ({ type={block.type} />
- {t(`workflow.blocks.${block.type}`)} + {t(`workflow.blocks.${block.type}` as any) as string} {block.type === BlockEnumValues.Start && ( {t('workflow.blocks.originalStartNode')} )} diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 6e49bfa0be9..7cead407058 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -237,8 +237,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { list.push({ id: `${type}-need-added`, type, - title: t(`workflow.blocks.${type}`), - errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }), + title: t(`workflow.blocks.${type}` as any) as string, + errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}` as any) as string }), canNavigate: false, }) } @@ -409,7 +409,7 @@ export const useChecklistBeforePublish = () => { const type = isRequiredNodesType[i] if (!filteredNodes.find(node => node.data.type === type)) { - notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) }) + notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}` as any) as string }) }) return false } } diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index 44df18ddf2a..91f89b147ed 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -44,7 +44,7 @@ const OutputVarList: FC = ({ if (!isValid) { setToastHandler(Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }), })) return } diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index 2d96baaf289..7b548c6afc5 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -57,7 +57,7 @@ const VarList: FC = ({ if (!isValid) { setToastHandle(Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }), })) return } diff --git a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx index fd346e632da..0cc905ae06c 100644 --- a/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx +++ b/web/app/components/workflow/nodes/assigner/components/operation-selector.tsx @@ -72,7 +72,7 @@ const OperationSelector: FC = ({ className={`system-sm-regular overflow-hidden truncate text-ellipsis ${selectedItem ? 'text-components-input-text-filled' : 'text-components-input-text-disabled'}`} > - {selectedItem?.name ? t(`${i18nPrefix}.operations.${selectedItem?.name}`) : t(`${i18nPrefix}.operations.title`)} + {selectedItem?.name ? t(`${i18nPrefix}.operations.${selectedItem?.name}` as any) as string : t(`${i18nPrefix}.operations.title` as any) as string}
@@ -83,7 +83,7 @@ const OperationSelector: FC = ({
-
{t(`${i18nPrefix}.operations.title`)}
+
{t(`${i18nPrefix}.operations.title` as any) as string}
{items.map(item => ( item.value === 'divider' @@ -100,7 +100,7 @@ const OperationSelector: FC = ({ }} >
- {t(`${i18nPrefix}.operations.${item.name}`)} + {t(`${i18nPrefix}.operations.${item.name}` as any) as string}
{item.value === value && (
diff --git a/web/app/components/workflow/nodes/assigner/node.tsx b/web/app/components/workflow/nodes/assigner/node.tsx index be30104242a..3eb9f0d6206 100644 --- a/web/app/components/workflow/nodes/assigner/node.tsx +++ b/web/app/components/workflow/nodes/assigner/node.tsx @@ -73,7 +73,7 @@ const NodeComponent: FC> = ({ nodeType={node?.data.type} nodeTitle={node?.data.title} rightSlot={ - writeMode && + writeMode && } />
diff --git a/web/app/components/workflow/nodes/if-else/components/condition-files-list-value.tsx b/web/app/components/workflow/nodes/if-else/components/condition-files-list-value.tsx index 53df68c337c..0bb2245c714 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-files-list-value.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-files-list-value.tsx @@ -34,7 +34,7 @@ const ConditionValue = ({ const variableSelector = variable_selector as ValueSelector - const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator + const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}` as any) as string : operator const formatValue = useCallback((c: Condition) => { const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator) if (notHasValue) @@ -59,7 +59,7 @@ const ConditionValue = ({ if (isSelect) { const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0] return name - ? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { + ? (t(`workflow.nodes.ifElse.optionName.${name.i18nKey}` as any) as string).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { const arr: string[] = b.split('.') if (isSystemVar(arr)) return `{{${b}}}` @@ -91,9 +91,9 @@ const ConditionValue = ({ sub_variable_condition?.conditions.map((c: Condition, index) => (
{c.key}
-
{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}
+
{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}` as any) as string : c.comparison_operator}
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) &&
{isSelect(c) ? selectName(c) : formatValue(c)}
} - {index !== sub_variable_condition.conditions.length - 1 && (
{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}
)} + {index !== sub_variable_condition.conditions.length - 1 && (
{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}` as any) as string}
)}
)) } diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx index b323e65066c..f49b8ef2fc2 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-item.tsx @@ -168,13 +168,13 @@ const ConditionItem = ({ if (isSelect) { if (fileAttr?.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) { return FILE_TYPE_OPTIONS.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } if (fileAttr?.key === 'transfer_method') { return TRANSFER_METHOD.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } diff --git a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx index e2753ba6e74..64c6d35d8f6 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-list/condition-operator.tsx @@ -39,7 +39,7 @@ const ConditionOperator = ({ const options = useMemo(() => { return getOperators(varType, file).map((o) => { return { - label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, + label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}` as any) as string : o, value: o, } }) @@ -65,7 +65,7 @@ const ConditionOperator = ({ { selectedOption ? selectedOption.label - : t(`${i18nPrefix}.select`) + : t(`${i18nPrefix}.select` as any) as string } diff --git a/web/app/components/workflow/nodes/if-else/components/condition-value.tsx b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx index 376c3a670f5..c0cd78e4c54 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-value.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-value.tsx @@ -35,7 +35,7 @@ const ConditionValue = ({ const { t } = useTranslation() const nodes = useNodes() const variableName = labelName || (isSystemVar(variableSelector) ? variableSelector.slice(0).join('.') : variableSelector.slice(1).join('.')) - const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator + const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}` as any) as string : operator const notHasValue = comparisonOperatorNotRequireValue(operator) const node: Node | undefined = nodes.find(n => n.id === variableSelector[0]) as Node const isException = isExceptionVariable(variableName, node?.data.type) @@ -63,7 +63,7 @@ const ConditionValue = ({ if (isSelect) { const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(value) ? value[0] : value))[0] return name - ? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { + ? (t(`workflow.nodes.ifElse.optionName.${name.i18nKey}` as any) as string).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { const arr: string[] = b.split('.') if (isSystemVar(arr)) return `{{${b}}}` diff --git a/web/app/components/workflow/nodes/iteration/use-interactions.ts b/web/app/components/workflow/nodes/iteration/use-interactions.ts index c6fddff5ad4..87a4b8ad5d2 100644 --- a/web/app/components/workflow/nodes/iteration/use-interactions.ts +++ b/web/app/components/workflow/nodes/iteration/use-interactions.ts @@ -135,7 +135,7 @@ export const useNodeIterationInteractions = () => { _isBundled: false, _connectedSourceHandleIds: [], _connectedTargetHandleIds: [], - title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}`)} ${childNodeTypeCount[childNodeType]}` : t(`workflow.blocks.${childNodeType}`), + title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${childNodeType}` as any) as string} ${childNodeTypeCount[childNodeType]}` : t(`workflow.blocks.${childNodeType}` as any) as string, iteration_id: newNodeId, type: childNodeType, }, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx index b3f2701524a..5c764cba285 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/dataset-item.tsx @@ -121,7 +121,7 @@ const DatasetItem: FC = ({ payload.provider === 'external' && ( ) } diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx index 8f0430b6552..d248d96dc0e 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-operator.tsx @@ -42,7 +42,7 @@ const ConditionOperator = ({ const options = useMemo(() => { return getOperators(variableType).map((o) => { return { - label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, + label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}` as any) as string : o, value: o, } }) @@ -68,7 +68,7 @@ const ConditionOperator = ({ { selectedOption ? selectedOption.label - : t(`${i18nPrefix}.select`) + : t(`${i18nPrefix}.select` as any) as string } diff --git a/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx b/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx index 8dd817a5ada..1ca931d32ce 100644 --- a/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx +++ b/web/app/components/workflow/nodes/list-operator/components/filter-condition.tsx @@ -66,13 +66,13 @@ const FilterCondition: FC = ({ if (isSelect) { if (condition.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) { return FILE_TYPE_OPTIONS.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } if (condition.key === 'transfer_method') { return TRANSFER_METHOD.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } diff --git a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx index 776ad6804c5..64d7a24a534 100644 --- a/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx +++ b/web/app/components/workflow/nodes/llm/components/config-prompt-item.tsx @@ -121,7 +121,7 @@ const ConfigPromptItem: FC = ({ {t(`${i18nPrefix}.roleDescription.${payload.role}`)}
+
{t(`${i18nPrefix}.roleDescription.${payload.role}` as any) as string}
} triggerClassName="w-4 h-4" /> diff --git a/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx b/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx index e13832ed46e..dfe16902fd3 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-files-list-value.tsx @@ -34,7 +34,7 @@ const ConditionValue = ({ const variableSelector = variable_selector as ValueSelector - const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator + const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}` as any) as string : operator const formatValue = useCallback((c: Condition) => { const notHasValue = comparisonOperatorNotRequireValue(c.comparison_operator) if (notHasValue) @@ -59,7 +59,7 @@ const ConditionValue = ({ if (isSelect) { const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(c.value) ? c.value[0] : c.value))[0] return name - ? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { + ? (t(`workflow.nodes.ifElse.optionName.${name.i18nKey}` as any) as string).replace(/\{\{#([^#]*)#\}\}/g, (a: string, b: string) => { const arr: string[] = b.split('.') if (isSystemVar(arr)) return `{{${b}}}` @@ -91,9 +91,9 @@ const ConditionValue = ({ sub_variable_condition?.conditions.map((c: Condition, index) => (
{c.key}
-
{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}`) : c.comparison_operator}
+
{isComparisonOperatorNeedTranslate(c.comparison_operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${c.comparison_operator}` as any) as string : c.comparison_operator}
{c.comparison_operator && !isEmptyRelatedOperator(c.comparison_operator) &&
{isSelect(c) ? selectName(c) : formatValue(c)}
} - {index !== sub_variable_condition.conditions.length - 1 && (
{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}`)}
)} + {index !== sub_variable_condition.conditions.length - 1 && (
{t(`${i18nPrefix}.${sub_variable_condition.logical_operator}` as any) as string}
)}
)) } diff --git a/web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx b/web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx index ea3e2ef5be3..95e7b58dd09 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-list/condition-item.tsx @@ -145,13 +145,13 @@ const ConditionItem = ({ if (isSelect) { if (fileAttr?.key === 'type' || condition.comparison_operator === ComparisonOperator.allOf) { return FILE_TYPE_OPTIONS.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } if (fileAttr?.key === 'transfer_method') { return TRANSFER_METHOD.map(item => ({ - name: t(`${optionNameI18NPrefix}.${item.i18nKey}`), + name: t(`${optionNameI18NPrefix}.${item.i18nKey}` as any) as string, value: item.value, })) } diff --git a/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx b/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx index a33b2b77273..9943109c2b3 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-list/condition-operator.tsx @@ -39,7 +39,7 @@ const ConditionOperator = ({ const options = useMemo(() => { return getOperators(varType, file).map((o) => { return { - label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}`) : o, + label: isComparisonOperatorNeedTranslate(o) ? t(`${i18nPrefix}.comparisonOperator.${o}` as any) as string : o, value: o, } }) @@ -65,7 +65,7 @@ const ConditionOperator = ({ { selectedOption ? selectedOption.label - : t(`${i18nPrefix}.select`) + : t(`${i18nPrefix}.select` as any) as string } diff --git a/web/app/components/workflow/nodes/loop/components/condition-value.tsx b/web/app/components/workflow/nodes/loop/components/condition-value.tsx index c24a1a18a67..10fa2cef42f 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-value.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-value.tsx @@ -27,7 +27,7 @@ const ConditionValue = ({ value, }: ConditionValueProps) => { const { t } = useTranslation() - const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}`) : operator + const operatorName = isComparisonOperatorNeedTranslate(operator) ? t(`workflow.nodes.ifElse.comparisonOperator.${operator}` as any) as string : operator const notHasValue = comparisonOperatorNotRequireValue(operator) const formatValue = useMemo(() => { if (notHasValue) @@ -50,7 +50,7 @@ const ConditionValue = ({ if (isSelect) { const name = [...FILE_TYPE_OPTIONS, ...TRANSFER_METHOD].filter(item => item.value === (Array.isArray(value) ? value[0] : value))[0] return name - ? t(`workflow.nodes.ifElse.optionName.${name.i18nKey}`).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { + ? (t(`workflow.nodes.ifElse.optionName.${name.i18nKey}` as any) as string).replace(/\{\{#([^#]*)#\}\}/g, (a, b) => { const arr: string[] = b.split('.') if (isSystemVar(arr)) return `{{${b}}}` diff --git a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx index 973e78ae73f..949c53ca975 100644 --- a/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx +++ b/web/app/components/workflow/nodes/loop/components/loop-variables/item.tsx @@ -30,7 +30,7 @@ const Item = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: t('workflow.env.modal.name') }) as string, }) return false } diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx index 288e486ea7a..61921296d4c 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/update.tsx @@ -56,7 +56,7 @@ const AddExtractParameter: FC = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }) as string, }) return } diff --git a/web/app/components/workflow/nodes/start/components/var-list.tsx b/web/app/components/workflow/nodes/start/components/var-list.tsx index bda45ca5dda..4d3c6cd8716 100644 --- a/web/app/components/workflow/nodes/start/components/var-list.tsx +++ b/web/app/components/workflow/nodes/start/components/var-list.tsx @@ -45,7 +45,7 @@ const VarList: FC = ({ if (errorMsgKey) { Toast.notify({ type: 'error', - message: t(errorMsgKey, { key: t(typeName) }), + message: t(errorMsgKey as any, { key: t(typeName as any) as string }) as string, }) return false } diff --git a/web/app/components/workflow/nodes/start/use-config.ts b/web/app/components/workflow/nodes/start/use-config.ts index 8eed650f984..e563e710ce3 100644 --- a/web/app/components/workflow/nodes/start/use-config.ts +++ b/web/app/components/workflow/nodes/start/use-config.ts @@ -99,7 +99,7 @@ const useConfig = (id: string, payload: StartNodeType) => { if (errorMsgKey) { Toast.notify({ type: 'error', - message: t(errorMsgKey, { key: t(typeName) }), + message: t(errorMsgKey as any, { key: t(typeName as any) as string }) as string, }) return false } diff --git a/web/app/components/workflow/nodes/trigger-schedule/components/mode-switcher.tsx b/web/app/components/workflow/nodes/trigger-schedule/components/mode-switcher.tsx index 7c1f4e8f9d3..de0ac01cb60 100644 --- a/web/app/components/workflow/nodes/trigger-schedule/components/mode-switcher.tsx +++ b/web/app/components/workflow/nodes/trigger-schedule/components/mode-switcher.tsx @@ -15,12 +15,12 @@ const ModeSwitcher = ({ mode, onChange }: ModeSwitcherProps) => { const options = [ { Icon: RiCalendarLine, - text: t('workflow.nodes.triggerSchedule.mode.visual'), + text: t('workflow.nodes.triggerSchedule.modeVisual'), value: 'visual' as const, }, { Icon: RiCodeLine, - text: t('workflow.nodes.triggerSchedule.mode.cron'), + text: t('workflow.nodes.triggerSchedule.modeCron'), value: 'cron' as const, }, ] diff --git a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts index dec79b8eaf1..03cc72b2377 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/use-config.ts +++ b/web/app/components/workflow/nodes/trigger-webhook/use-config.ts @@ -103,9 +103,9 @@ const useConfig = (id: string, payload: WebhookTriggerNodeType) => { if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: t('appDebug.variableConfig.varName'), - }), + }) as string, }) return false } diff --git a/web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx index d722c1d2314..d1274ee65ff 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/node-variable-item.tsx @@ -120,7 +120,7 @@ const NodeVariableItem = ({ {VariableIcon} {VariableName}
- {writeMode && } + {writeMode && }
) } diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx index 8fb1cfba61a..f8b6298a9b0 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-group-item.tsx @@ -96,7 +96,7 @@ const VarGroupItem: FC = ({ if (!isValid) { Toast.notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: errorKey }) as string, }) return } diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx index 33e2e073765..aafeffb54a2 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.tsx @@ -127,7 +127,7 @@ const ChatVariableModal = ({ if (!isValid) { notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: t('workflow.env.modal.name') }) as string, }) return false } diff --git a/web/app/components/workflow/panel/env-panel/variable-modal.tsx b/web/app/components/workflow/panel/env-panel/variable-modal.tsx index e253d6c27c9..383e15f20b3 100644 --- a/web/app/components/workflow/panel/env-panel/variable-modal.tsx +++ b/web/app/components/workflow/panel/env-panel/variable-modal.tsx @@ -37,7 +37,7 @@ const VariableModal = ({ if (!isValid) { notify({ type: 'error', - message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: t('workflow.env.modal.name') }), + message: t(`appDebug.varKeyError.${errorMessageKey}` as any, { key: t('workflow.env.modal.name') }) as string, }) return false } diff --git a/web/app/components/workflow/run/loop-result-panel.tsx b/web/app/components/workflow/run/loop-result-panel.tsx index 8238be82f38..6e376570570 100644 --- a/web/app/components/workflow/run/loop-result-panel.tsx +++ b/web/app/components/workflow/run/loop-result-panel.tsx @@ -43,7 +43,7 @@ const LoopResultPanel: FC = ({
- {t(`${i18nPrefix}.testRunLoop`)} + {t(`${i18nPrefix}.testRunLoop` as any) as string}
diff --git a/web/app/forgot-password/ForgotPasswordForm.tsx b/web/app/forgot-password/ForgotPasswordForm.tsx index 2b50c1c452a..f06b952b68f 100644 --- a/web/app/forgot-password/ForgotPasswordForm.tsx +++ b/web/app/forgot-password/ForgotPasswordForm.tsx @@ -108,7 +108,7 @@ const ForgotPasswordForm = () => { {...register('email')} placeholder={t('login.emailPlaceholder') || ''} /> - {errors.email && {t(`${errors.email?.message}`)}} + {errors.email && {t(`${errors.email?.message}` as any) as string}}
)} diff --git a/web/app/install/installForm.tsx b/web/app/install/installForm.tsx index 60de8e0501b..f0290cca50e 100644 --- a/web/app/install/installForm.tsx +++ b/web/app/install/installForm.tsx @@ -138,7 +138,7 @@ const InstallForm = () => { placeholder={t('login.emailPlaceholder') || ''} className="system-sm-regular w-full appearance-none rounded-md border border-transparent bg-components-input-bg-normal px-3 py-[7px] text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs" /> - {errors.email && {t(`${errors.email?.message}`)}} + {errors.email && {t(`${errors.email?.message}` as any) as string}}
@@ -154,7 +154,7 @@ const InstallForm = () => { className="system-sm-regular w-full appearance-none rounded-md border border-transparent bg-components-input-bg-normal px-3 py-[7px] text-components-input-text-filled caret-primary-600 outline-none placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs" />
- {errors.name && {t(`${errors.name.message}`)}} + {errors.name && {t(`${errors.name.message}` as any) as string}}
diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 9abd4366e10..e3bbe420bce 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,4 +1,5 @@ 'use client' +import type { Locale } from '@/i18n-config' import { RiAccountCircleLine } from '@remixicon/react' import { noop } from 'lodash-es' import Link from 'next/link' @@ -123,7 +124,7 @@ export default function InviteSettingsPage() { defaultValue={LanguagesSupported[0]} items={languages.filter(item => item.supported)} onSelect={(item) => { - setLanguage(item.value as string) + setLanguage(item.value as Locale) }} />
diff --git a/web/hooks/use-format-time-from-now.ts b/web/hooks/use-format-time-from-now.ts index 09d8db7321f..970a64e7d54 100644 --- a/web/hooks/use-format-time-from-now.ts +++ b/web/hooks/use-format-time-from-now.ts @@ -1,8 +1,8 @@ -import type { Locale } from '@/i18n-config' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import { useCallback } from 'react' import { useI18N } from '@/context/i18n' +import { localeMap } from '@/i18n-config/language' import 'dayjs/locale/de' import 'dayjs/locale/es' import 'dayjs/locale/fa' @@ -26,30 +26,6 @@ import 'dayjs/locale/zh-tw' dayjs.extend(relativeTime) -const localeMap: Record = { - 'en-US': 'en', - 'zh-Hans': 'zh-cn', - 'zh-Hant': 'zh-tw', - 'pt-BR': 'pt-br', - 'es-ES': 'es', - 'fr-FR': 'fr', - 'de-DE': 'de', - 'ja-JP': 'ja', - 'ko-KR': 'ko', - 'ru-RU': 'ru', - 'it-IT': 'it', - 'th-TH': 'th', - 'id-ID': 'id', - 'uk-UA': 'uk', - 'vi-VN': 'vi', - 'ro-RO': 'ro', - 'pl-PL': 'pl', - 'hi-IN': 'hi', - 'tr-TR': 'tr', - 'fa-IR': 'fa', - 'sl-SI': 'sl', -} - export const useFormatTimeFromNow = () => { const { locale } = useI18N() const formatTimeFromNow = useCallback((time: number) => { diff --git a/web/hooks/use-knowledge.ts b/web/hooks/use-knowledge.ts index 400d9722de8..e3c2cd49d11 100644 --- a/web/hooks/use-knowledge.ts +++ b/web/hooks/use-knowledge.ts @@ -5,14 +5,14 @@ export const useKnowledge = () => { const { t } = useTranslation() const formatIndexingTechnique = useCallback((indexingTechnique: string) => { - return t(`dataset.indexingTechnique.${indexingTechnique}`) + return t(`dataset.indexingTechnique.${indexingTechnique}` as any) as string }, [t]) const formatIndexingMethod = useCallback((indexingMethod: string, isEco?: boolean) => { if (isEco) return t('dataset.indexingMethod.invertedIndex') - return t(`dataset.indexingMethod.${indexingMethod}`) + return t(`dataset.indexingMethod.${indexingMethod}` as any) as string }, [t]) const formatIndexingTechniqueAndMethod = useCallback((indexingTechnique: string, indexingMethod: string) => { diff --git a/web/hooks/use-metadata.ts b/web/hooks/use-metadata.ts index a51e6b150e2..6b0946b68de 100644 --- a/web/hooks/use-metadata.ts +++ b/web/hooks/use-metadata.ts @@ -86,7 +86,7 @@ export const useMetadataMap = (): MetadataMap => { }, 'volume/issue/page_numbers': { label: t(`${fieldPrefix}.paper.volumeIssuePage`) }, 'doi': { label: t(`${fieldPrefix}.paper.DOI`) }, - 'topic/keywords': { label: t(`${fieldPrefix}.paper.topicKeywords`) }, + 'topic/keywords': { label: t(`${fieldPrefix}.paper.topicKeywords` as any) as string }, 'abstract': { label: t(`${fieldPrefix}.paper.abstract`), inputType: 'textarea', @@ -160,7 +160,7 @@ export const useMetadataMap = (): MetadataMap => { 'end_date': { label: t(`${fieldPrefix}.IMChat.endDate`) }, 'participants': { label: t(`${fieldPrefix}.IMChat.participants`) }, 'topicKeywords': { - label: t(`${fieldPrefix}.IMChat.topicKeywords`), + label: t(`${fieldPrefix}.IMChat.topicKeywords` as any) as string, inputType: 'textarea', }, 'fileType': { label: t(`${fieldPrefix}.IMChat.fileType`) }, @@ -193,7 +193,7 @@ export const useMetadataMap = (): MetadataMap => { allowEdit: false, subFieldsMap: { 'title': { label: t(`${fieldPrefix}.notion.title`) }, - 'language': { label: t(`${fieldPrefix}.notion.lang`), inputType: 'select' }, + 'language': { label: t(`${fieldPrefix}.notion.lang` as any) as string, inputType: 'select' }, 'author/creator': { label: t(`${fieldPrefix}.notion.author`) }, 'creation_date': { label: t(`${fieldPrefix}.notion.createdTime`) }, 'last_modified_date': { @@ -201,7 +201,7 @@ export const useMetadataMap = (): MetadataMap => { }, 'notion_page_link': { label: t(`${fieldPrefix}.notion.url`) }, 'category/tags': { label: t(`${fieldPrefix}.notion.tag`) }, - 'description': { label: t(`${fieldPrefix}.notion.desc`) }, + 'description': { label: t(`${fieldPrefix}.notion.desc` as any) as string }, }, }, synced_from_github: { @@ -241,7 +241,7 @@ export const useMetadataMap = (): MetadataMap => { }, 'data_source_type': { label: t(`${fieldPrefix}.originInfo.source`), - render: value => t(`datasetDocuments.metadata.source.${value === 'notion_import' ? 'notion' : value}`), + render: value => t(`datasetDocuments.metadata.source.${value === 'notion_import' ? 'notion' : value}` as any) as string, }, }, }, @@ -323,7 +323,7 @@ export const useLanguages = () => { cs: t(`${langPrefix}cs`), th: t(`${langPrefix}th`), id: t(`${langPrefix}id`), - ro: t(`${langPrefix}ro`), + ro: t(`${langPrefix}ro` as any) as string, } } diff --git a/web/i18n-config/README.md b/web/i18n-config/README.md index 0fe89223453..c724d94aa72 100644 --- a/web/i18n-config/README.md +++ b/web/i18n-config/README.md @@ -55,28 +55,9 @@ cp -r en-US id-ID 1. Add type to new language in the `language.ts` file. -```typescript -export type I18nText = { - 'en-US': string - 'zh-Hans': string - 'pt-BR': string - 'es-ES': string - 'fr-FR': string - 'de-DE': string - 'ja-JP': string - 'ko-KR': string - 'ru-RU': string - 'it-IT': string - 'uk-UA': string - 'id-ID': string - 'tr-TR': string - 'fa-IR': string - 'ar-TN': string - 'YOUR_LANGUAGE_CODE': string -} -``` +> Note: `I18nText` type is now automatically derived from `LanguagesSupported`, so you don't need to manually add types. -4. Add the new language to the `language.json` file. +4. Add the new language to the `languages.ts` file. ```typescript export const languages = [ @@ -189,11 +170,10 @@ We have a list of languages that we support in the `language.ts` file. But some ## Utility scripts -- Auto-fill translations: `pnpm run auto-gen-i18n -- --file app common --lang zh-Hans ja-JP [--dry-run]` +- Auto-fill translations: `pnpm run auto-gen-i18n --file app common --lang zh-Hans ja-JP [--dry-run]` - Use space-separated values; repeat `--file` / `--lang` as needed. Defaults to all en-US files and all supported locales except en-US. - Protects placeholders (`{{var}}`, `${var}`, ``) before translation and restores them after. -- Check missing/extra keys: `pnpm run check-i18n -- --file app billing --lang zh-Hans [--auto-remove]` +- Check missing/extra keys: `pnpm run check-i18n --file app billing --lang zh-Hans [--auto-remove]` - Use space-separated values; repeat `--file` / `--lang` as needed. Returns non-zero on missing/extra keys (CI will fail); `--auto-remove` deletes extra keys automatically. -- Generate types: `pnpm run gen:i18n-types`; verify sync: `pnpm run check:i18n-types`. -Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on en-US changes to main; `.github/workflows/web-tests.yml` checks i18n keys and type sync on web changes. +Workflows: `.github/workflows/translate-i18n-base-on-english.yml` auto-runs the translation generator on en-US changes to main; `.github/workflows/web-tests.yml` checks i18n keys on web changes. diff --git a/web/i18n-config/auto-gen-i18n.js b/web/i18n-config/auto-gen-i18n.js index 561fa95869b..6c8cb05bbd4 100644 --- a/web/i18n-config/auto-gen-i18n.js +++ b/web/i18n-config/auto-gen-i18n.js @@ -6,11 +6,11 @@ import vm from 'node:vm' import { translate } from 'bing-translate-api' import { generateCode, loadFile, parseModule } from 'magicast' import { transpile } from 'typescript' +import data from './languages' const require = createRequire(import.meta.url) const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -const data = require('./languages.json') const targetLanguage = 'en-US' const i18nFolder = '../i18n' // Path to i18n folder relative to this script @@ -117,8 +117,8 @@ Options: -h, --help Show help Examples: - pnpm run auto-gen-i18n -- --file app common --lang zh-Hans ja-JP - pnpm run auto-gen-i18n -- --dry-run + pnpm run auto-gen-i18n --file app common --lang zh-Hans ja-JP + pnpm run auto-gen-i18n --dry-run `) } diff --git a/web/i18n-config/check-i18n-sync.js b/web/i18n-config/check-i18n-sync.js deleted file mode 100644 index af00d238753..00000000000 --- a/web/i18n-config/check-i18n-sync.js +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import lodash from 'lodash' -import ts from 'typescript' - -const { camelCase } = lodash - -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -// Import the NAMESPACES array from i18next-config.ts -function getNamespacesFromConfig() { - const configPath = path.join(__dirname, 'i18next-config.ts') - const configContent = fs.readFileSync(configPath, 'utf8') - const sourceFile = ts.createSourceFile(configPath, configContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - - const namespaces = [] - - const visit = (node) => { - if ( - ts.isVariableDeclaration(node) - && node.name.getText() === 'NAMESPACES' - && node.initializer - && ts.isArrayLiteralExpression(node.initializer) - ) { - node.initializer.elements.forEach((el) => { - if (ts.isStringLiteral(el)) - namespaces.push(el.text) - }) - } - ts.forEachChild(node, visit) - } - - visit(sourceFile) - - if (!namespaces.length) - throw new Error('Could not find NAMESPACES array in i18next-config.ts') - - return namespaces -} - -function getNamespacesFromTypes() { - const typesPath = path.join(__dirname, '../types/i18n.d.ts') - - if (!fs.existsSync(typesPath)) { - return null - } - - const typesContent = fs.readFileSync(typesPath, 'utf8') - - // Extract namespaces from Messages type - const messagesMatch = typesContent.match(/export type Messages = \{([\s\S]*?)\}/) - if (!messagesMatch) { - return null - } - - // Parse the properties - const propertiesStr = messagesMatch[1] - const properties = propertiesStr - .split('\n') - .map(line => line.trim()) - .filter(line => line.includes(':')) - .map(line => line.split(':')[0].trim()) - .filter(prop => prop.length > 0) - - return properties -} - -function main() { - try { - console.log('🔍 Checking i18n types synchronization...') - - // Get namespaces from config - const configNamespaces = getNamespacesFromConfig() - console.log(`📦 Found ${configNamespaces.length} namespaces in config`) - - // Convert to camelCase for comparison - const configCamelCase = configNamespaces.map(ns => camelCase(ns)).sort() - - // Get namespaces from type definitions - const typeNamespaces = getNamespacesFromTypes() - - if (!typeNamespaces) { - console.error('❌ Type definitions file not found or invalid') - console.error(' Run: pnpm run gen:i18n-types') - process.exit(1) - } - - console.log(`🔧 Found ${typeNamespaces.length} namespaces in types`) - - const typeCamelCase = typeNamespaces.sort() - - // Compare arrays - const configSet = new Set(configCamelCase) - const typeSet = new Set(typeCamelCase) - - // Find missing in types - const missingInTypes = configCamelCase.filter(ns => !typeSet.has(ns)) - - // Find extra in types - const extraInTypes = typeCamelCase.filter(ns => !configSet.has(ns)) - - let hasErrors = false - - if (missingInTypes.length > 0) { - hasErrors = true - console.error('❌ Missing in type definitions:') - missingInTypes.forEach(ns => console.error(` - ${ns}`)) - } - - if (extraInTypes.length > 0) { - hasErrors = true - console.error('❌ Extra in type definitions:') - extraInTypes.forEach(ns => console.error(` - ${ns}`)) - } - - if (hasErrors) { - console.error('\n💡 To fix synchronization issues:') - console.error(' Run: pnpm run gen:i18n-types') - process.exit(1) - } - - console.log('✅ i18n types are synchronized') - } - catch (error) { - console.error('❌ Error:', error.message) - process.exit(1) - } -} - -main() diff --git a/web/i18n-config/check-i18n.js b/web/i18n-config/check-i18n.js index d69885e6f04..d70564556c6 100644 --- a/web/i18n-config/check-i18n.js +++ b/web/i18n-config/check-i18n.js @@ -4,13 +4,13 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import vm from 'node:vm' import { transpile } from 'typescript' +import data from './languages' const require = createRequire(import.meta.url) const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) const targetLanguage = 'en-US' -const data = require('./languages.json') const languages = data.languages.filter(language => language.supported).map(language => language.value) @@ -103,8 +103,8 @@ Options: -h, --help Show help Examples: - pnpm run check-i18n -- --file app billing --lang zh-Hans ja-JP - pnpm run check-i18n -- --auto-remove + pnpm run check-i18n --file app billing --lang zh-Hans ja-JP + pnpm run check-i18n --auto-remove `) } diff --git a/web/i18n-config/generate-i18n-types.js b/web/i18n-config/generate-i18n-types.js deleted file mode 100644 index 0b3c0195aff..00000000000 --- a/web/i18n-config/generate-i18n-types.js +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs' -import path from 'node:path' -import { fileURLToPath } from 'node:url' -import lodash from 'lodash' -import ts from 'typescript' - -const { camelCase } = lodash -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) - -// Import the NAMESPACES array from i18next-config.ts -function getNamespacesFromConfig() { - const configPath = path.join(__dirname, 'i18next-config.ts') - const configContent = fs.readFileSync(configPath, 'utf8') - const sourceFile = ts.createSourceFile(configPath, configContent, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS) - - const namespaces = [] - - const visit = (node) => { - if ( - ts.isVariableDeclaration(node) - && node.name.getText() === 'NAMESPACES' - && node.initializer - && ts.isArrayLiteralExpression(node.initializer) - ) { - node.initializer.elements.forEach((el) => { - if (ts.isStringLiteral(el)) - namespaces.push(el.text) - }) - } - ts.forEachChild(node, visit) - } - - visit(sourceFile) - - if (!namespaces.length) - throw new Error('Could not find NAMESPACES array in i18next-config.ts') - - return namespaces -} - -function generateTypeDefinitions(namespaces) { - const header = `// TypeScript type definitions for Dify's i18next configuration -// This file is auto-generated. Do not edit manually. -// To regenerate, run: pnpm run gen:i18n-types -import 'react-i18next' - -// Extract types from translation files using typeof import pattern` - - // Generate individual type definitions - const typeDefinitions = namespaces.map((namespace) => { - const typeName = `${camelCase(namespace).replace(/^\w/, c => c.toUpperCase())}Messages` - return `type ${typeName} = typeof import('../i18n/en-US/${namespace}').default` - }).join('\n') - - // Generate Messages interface - const messagesInterface = ` -// Complete type structure that matches i18next-config.ts camelCase conversion -export type Messages = { -${namespaces.map((namespace) => { - const camelCased = camelCase(namespace) - const typeName = `${camelCase(namespace).replace(/^\w/, c => c.toUpperCase())}Messages` - return ` ${camelCased}: ${typeName};` -}).join('\n')} -}` - - const utilityTypes = ` -// Utility type to flatten nested object keys into dot notation -type FlattenKeys = T extends object - ? { - [K in keyof T]: T[K] extends object - ? \`\${K & string}.\${FlattenKeys & string}\` - : \`\${K & string}\` - }[keyof T] - : never - -export type ValidTranslationKeys = FlattenKeys` - - const moduleDeclarations = ` -// Extend react-i18next with Dify's type structure -declare module 'react-i18next' { - interface CustomTypeOptions { - defaultNS: 'translation'; - resources: { - translation: Messages; - }; - } -} - -// Extend i18next for complete type safety -declare module 'i18next' { - interface CustomTypeOptions { - defaultNS: 'translation'; - resources: { - translation: Messages; - }; - } -}` - - return [header, typeDefinitions, messagesInterface, utilityTypes, moduleDeclarations].join('\n\n') -} - -function main() { - const args = process.argv.slice(2) - const checkMode = args.includes('--check') - - try { - console.log('📦 Generating i18n type definitions...') - - // Get namespaces from config - const namespaces = getNamespacesFromConfig() - console.log(`✅ Found ${namespaces.length} namespaces`) - - // Generate type definitions - const typeDefinitions = generateTypeDefinitions(namespaces) - - const outputPath = path.join(__dirname, '../types/i18n.d.ts') - - if (checkMode) { - // Check mode: compare with existing file - if (!fs.existsSync(outputPath)) { - console.error('❌ Type definitions file does not exist') - process.exit(1) - } - - const existingContent = fs.readFileSync(outputPath, 'utf8') - if (existingContent.trim() !== typeDefinitions.trim()) { - console.error('❌ Type definitions are out of sync') - console.error(' Run: pnpm run gen:i18n-types') - process.exit(1) - } - - console.log('✅ Type definitions are in sync') - } - else { - // Generate mode: write file - fs.writeFileSync(outputPath, typeDefinitions) - console.log(`✅ Generated type definitions: ${outputPath}`) - } - } - catch (error) { - console.error('❌ Error:', error.message) - process.exit(1) - } -} - -main() diff --git a/web/i18n-config/i18next-config.ts b/web/i18n-config/i18next-config.ts index b310d380e26..e82f5f2acb7 100644 --- a/web/i18n-config/i18next-config.ts +++ b/web/i18n-config/i18next-config.ts @@ -1,10 +1,10 @@ 'use client' +import type { Locale } from '.' import i18n from 'i18next' -import { camelCase } from 'lodash-es' -import { initReactI18next } from 'react-i18next' +import { camelCase, kebabCase } from 'lodash-es' +import { initReactI18next } from 'react-i18next' import app from '../i18n/en-US/app' -// Static imports for en-US (fallback language) import appAnnotation from '../i18n/en-US/app-annotation' import appApi from '../i18n/en-US/app-api' import appDebug from '../i18n/en-US/app-debug' @@ -35,7 +35,56 @@ import time from '../i18n/en-US/time' import tools from '../i18n/en-US/tools' import workflow from '../i18n/en-US/workflow' -const requireSilent = async (lang: string, namespace: string) => { +// @keep-sorted +export const messagesEN = { + app, + appAnnotation, + appApi, + appDebug, + appLog, + appOverview, + billing, + common, + custom, + dataset, + datasetCreation, + datasetDocuments, + datasetHitTesting, + datasetPipeline, + datasetSettings, + education, + explore, + layout, + login, + oauth, + pipeline, + plugin, + pluginTags, + pluginTrigger, + register, + runLog, + share, + time, + tools, + workflow, +} + +// pluginTrigger -> plugin-trigger + +export type KebabCase = S extends `${infer T}${infer U}` + ? T extends Lowercase + ? `${T}${KebabCase}` + : `-${Lowercase}${KebabCase}` + : S + +export type CamelCase = S extends `${infer T}-${infer U}` + ? `${T}${Capitalize>}` + : S + +export type KeyPrefix = keyof typeof messagesEN +export type Namespace = KebabCase + +const requireSilent = async (lang: Locale, namespace: Namespace) => { let res try { res = (await import(`../i18n/${lang}/${namespace}`)).default @@ -47,40 +96,9 @@ const requireSilent = async (lang: string, namespace: string) => { return res } -const NAMESPACES = [ - 'app-annotation', - 'app-api', - 'app-debug', - 'app-log', - 'app-overview', - 'app', - 'billing', - 'common', - 'custom', - 'dataset-creation', - 'dataset-documents', - 'dataset-hit-testing', - 'dataset-pipeline', - 'dataset-settings', - 'dataset', - 'education', - 'explore', - 'layout', - 'login', - 'oauth', - 'pipeline', - 'plugin-tags', - 'plugin-trigger', - 'plugin', - 'register', - 'run-log', - 'share', - 'time', - 'tools', - 'workflow', -] +const NAMESPACES = Object.keys(messagesEN).map(kebabCase) as Namespace[] -export const loadLangResources = async (lang: string) => { +export const loadLangResources = async (lang: Locale) => { const modules = await Promise.all( NAMESPACES.map(ns => requireSilent(lang, ns)), ) @@ -93,41 +111,9 @@ export const loadLangResources = async (lang: string) => { // Load en-US resources first to make sure fallback works const getInitialTranslations = () => { - const en_USResources: Record = { - appAnnotation, - appApi, - appDebug, - appLog, - appOverview, - app, - billing, - common, - custom, - datasetCreation, - datasetDocuments, - datasetHitTesting, - datasetPipeline, - datasetSettings, - dataset, - education, - explore, - layout, - login, - oauth, - pipeline, - pluginTags, - pluginTrigger, - plugin, - register, - runLog, - share, - time, - tools, - workflow, - } return { 'en-US': { - translation: en_USResources, + translation: messagesEN, }, } } @@ -140,7 +126,7 @@ if (!i18n.isInitialized) { }) } -export const changeLanguage = async (lng?: string) => { +export const changeLanguage = async (lng?: Locale) => { if (!lng) return if (!i18n.hasResourceBundle(lng, 'translation')) { diff --git a/web/i18n-config/index.ts b/web/i18n-config/index.ts index 8a0f712f2aa..bb73ef4b71b 100644 --- a/web/i18n-config/index.ts +++ b/web/i18n-config/index.ts @@ -1,5 +1,6 @@ -import Cookies from 'js-cookie' +import type { Locale } from '@/i18n-config/language' +import Cookies from 'js-cookie' import { LOCALE_COOKIE_NAME } from '@/config' import { changeLanguage } from '@/i18n-config/i18next-config' import { LanguagesSupported } from '@/i18n-config/language' @@ -9,7 +10,7 @@ export const i18n = { locales: LanguagesSupported, } as const -export type Locale = typeof i18n['locales'][number] +export { Locale } export const setLocaleOnClient = async (locale: Locale, reloadPage = true) => { Cookies.set(LOCALE_COOKIE_NAME, locale, { expires: 365 }) diff --git a/web/i18n-config/language.ts b/web/i18n-config/language.ts index a1fe6e790f9..28afd9eabf1 100644 --- a/web/i18n-config/language.ts +++ b/web/i18n-config/language.ts @@ -1,4 +1,4 @@ -import data from './languages.json' +import data from './languages' export type Item = { value: number | string @@ -6,40 +6,20 @@ export type Item = { example: string } -export type I18nText = { - 'en-US': string - 'zh-Hans': string - 'zh-Hant': string - 'pt-BR': string - 'es-ES': string - 'fr-FR': string - 'de-DE': string - 'ja-JP': string - 'ko-KR': string - 'ru-RU': string - 'it-IT': string - 'th-TH': string - 'id-ID': string - 'uk-UA': string - 'vi-VN': string - 'ro-RO': string - 'pl-PL': string - 'hi-IN': string - 'tr-TR': string - 'fa-IR': string - 'sl-SI': string - 'ar-TN': string -} +export type I18nText = Record export const languages = data.languages -export const LanguagesSupported = languages.filter(item => item.supported).map(item => item.value) +// for compatibility +export type Locale = 'ja_JP' | 'zh_Hans' | 'en_US' | (typeof languages[number])['value'] -export const getLanguage = (locale: string) => { +export const LanguagesSupported: Locale[] = languages.filter(item => item.supported).map(item => item.value) + +export const getLanguage = (locale: Locale): Locale => { if (['zh-Hans', 'ja-JP'].includes(locale)) - return locale.replace('-', '_') + return locale.replace('-', '_') as Locale - return LanguagesSupported[0].replace('-', '_') + return LanguagesSupported[0].replace('-', '_') as Locale } const DOC_LANGUAGE: Record = { @@ -48,6 +28,34 @@ const DOC_LANGUAGE: Record = { 'en-US': 'en', } +export const localeMap: Record = { + 'en-US': 'en', + 'en_US': 'en', + 'zh-Hans': 'zh-cn', + 'zh_Hans': 'zh-cn', + 'zh-Hant': 'zh-tw', + 'pt-BR': 'pt-br', + 'es-ES': 'es', + 'fr-FR': 'fr', + 'de-DE': 'de', + 'ja-JP': 'ja', + 'ja_JP': 'ja', + 'ko-KR': 'ko', + 'ru-RU': 'ru', + 'it-IT': 'it', + 'th-TH': 'th', + 'id-ID': 'id', + 'uk-UA': 'uk', + 'vi-VN': 'vi', + 'ro-RO': 'ro', + 'pl-PL': 'pl', + 'hi-IN': 'hi', + 'tr-TR': 'tr', + 'fa-IR': 'fa', + 'sl-SI': 'sl', + 'ar-TN': 'ar', +} + export const getDocLanguage = (locale: string) => { return DOC_LANGUAGE[locale] || 'en' } diff --git a/web/i18n-config/languages.json b/web/i18n-config/languages.json deleted file mode 100644 index 6e0025b8de3..00000000000 --- a/web/i18n-config/languages.json +++ /dev/null @@ -1,158 +0,0 @@ -{ - "languages": [ - { - "value": "en-US", - "name": "English (United States)", - "prompt_name": "English", - "example": "Hello, Dify!", - "supported": true - }, - { - "value": "zh-Hans", - "name": "简体中文", - "prompt_name": "Chinese Simplified", - "example": "你好,Dify!", - "supported": true - }, - { - "value": "zh-Hant", - "name": "繁體中文", - "prompt_name": "Chinese Traditional", - "example": "你好,Dify!", - "supported": true - }, - { - "value": "pt-BR", - "name": "Português (Brasil)", - "prompt_name": "Portuguese", - "example": "Olá, Dify!", - "supported": true - }, - { - "value": "es-ES", - "name": "Español (España)", - "prompt_name": "Spanish", - "example": "¡Hola, Dify!", - "supported": true - }, - { - "value": "fr-FR", - "name": "Français (France)", - "prompt_name": "French", - "example": "Bonjour, Dify!", - "supported": true - }, - { - "value": "de-DE", - "name": "Deutsch (Deutschland)", - "prompt_name": "German", - "example": "Hallo, Dify!", - "supported": true - }, - { - "value": "ja-JP", - "name": "日本語 (日本)", - "prompt_name": "Japanese", - "example": "こんにちは、Dify!", - "supported": true - }, - { - "value": "ko-KR", - "name": "한국어 (대한민국)", - "prompt_name": "Korean", - "example": "안녕하세요, Dify!", - "supported": true - }, - { - "value": "ru-RU", - "name": "Русский (Россия)", - "prompt_name": "Russian", - "example": " Привет, Dify!", - "supported": true - }, - { - "value": "it-IT", - "name": "Italiano (Italia)", - "prompt_name": "Italian", - "example": "Ciao, Dify!", - "supported": true - }, - { - "value": "th-TH", - "name": "ไทย (ประเทศไทย)", - "prompt_name": "Thai", - "example": "สวัสดี Dify!", - "supported": true - }, - { - "value": "uk-UA", - "name": "Українська (Україна)", - "prompt_name": "Ukrainian", - "example": "Привет, Dify!", - "supported": true - }, - { - "value": "vi-VN", - "name": "Tiếng Việt (Việt Nam)", - "prompt_name": "Vietnamese", - "example": "Xin chào, Dify!", - "supported": true - }, - { - "value": "ro-RO", - "name": "Română (România)", - "prompt_name": "Romanian", - "example": "Salut, Dify!", - "supported": true - }, - { - "value": "pl-PL", - "name": "Polski (Polish)", - "prompt_name": "Polish", - "example": "Cześć, Dify!", - "supported": true - }, - { - "value": "hi-IN", - "name": "Hindi (India)", - "prompt_name": "Hindi", - "example": "नमस्ते, Dify!", - "supported": true - }, - { - "value": "tr-TR", - "name": "Türkçe", - "prompt_name": "Türkçe", - "example": "Selam!", - "supported": true - }, - { - "value": "fa-IR", - "name": "Farsi (Iran)", - "prompt_name": "Farsi", - "example": "سلام, دیفای!", - "supported": true - }, - { - "value": "sl-SI", - "name": "Slovensko (Slovenija)", - "prompt_name": "Slovensko", - "example": "Zdravo, Dify!", - "supported": true - }, - { - "value": "id-ID", - "name": "Bahasa Indonesia", - "prompt_name": "Indonesian", - "example": "Halo, Dify!", - "supported": true - }, - { - "value": "ar-TN", - "name": "العربية (تونس)", - "prompt_name": "Tunisian Arabic", - "example": "مرحبا، Dify!", - "supported": true - } - ] -} diff --git a/web/i18n-config/languages.ts b/web/i18n-config/languages.ts new file mode 100644 index 00000000000..5077aee1d27 --- /dev/null +++ b/web/i18n-config/languages.ts @@ -0,0 +1,160 @@ +const data = { + languages: [ + { + value: 'en-US', + name: 'English (United States)', + prompt_name: 'English', + example: 'Hello, Dify!', + supported: true, + }, + { + value: 'zh-Hans', + name: '简体中文', + prompt_name: 'Chinese Simplified', + example: '你好,Dify!', + supported: true, + }, + { + value: 'zh-Hant', + name: '繁體中文', + prompt_name: 'Chinese Traditional', + example: '你好,Dify!', + supported: true, + }, + { + value: 'pt-BR', + name: 'Português (Brasil)', + prompt_name: 'Portuguese', + example: 'Olá, Dify!', + supported: true, + }, + { + value: 'es-ES', + name: 'Español (España)', + prompt_name: 'Spanish', + example: '¡Hola, Dify!', + supported: true, + }, + { + value: 'fr-FR', + name: 'Français (France)', + prompt_name: 'French', + example: 'Bonjour, Dify!', + supported: true, + }, + { + value: 'de-DE', + name: 'Deutsch (Deutschland)', + prompt_name: 'German', + example: 'Hallo, Dify!', + supported: true, + }, + { + value: 'ja-JP', + name: '日本語 (日本)', + prompt_name: 'Japanese', + example: 'こんにちは、Dify!', + supported: true, + }, + { + value: 'ko-KR', + name: '한국어 (대한민국)', + prompt_name: 'Korean', + example: '안녕하세요, Dify!', + supported: true, + }, + { + value: 'ru-RU', + name: 'Русский (Россия)', + prompt_name: 'Russian', + example: ' Привет, Dify!', + supported: true, + }, + { + value: 'it-IT', + name: 'Italiano (Italia)', + prompt_name: 'Italian', + example: 'Ciao, Dify!', + supported: true, + }, + { + value: 'th-TH', + name: 'ไทย (ประเทศไทย)', + prompt_name: 'Thai', + example: 'สวัสดี Dify!', + supported: true, + }, + { + value: 'uk-UA', + name: 'Українська (Україна)', + prompt_name: 'Ukrainian', + example: 'Привет, Dify!', + supported: true, + }, + { + value: 'vi-VN', + name: 'Tiếng Việt (Việt Nam)', + prompt_name: 'Vietnamese', + example: 'Xin chào, Dify!', + supported: true, + }, + { + value: 'ro-RO', + name: 'Română (România)', + prompt_name: 'Romanian', + example: 'Salut, Dify!', + supported: true, + }, + { + value: 'pl-PL', + name: 'Polski (Polish)', + prompt_name: 'Polish', + example: 'Cześć, Dify!', + supported: true, + }, + { + value: 'hi-IN', + name: 'Hindi (India)', + prompt_name: 'Hindi', + example: 'नमस्ते, Dify!', + supported: true, + }, + { + value: 'tr-TR', + name: 'Türkçe', + prompt_name: 'Türkçe', + example: 'Selam!', + supported: true, + }, + { + value: 'fa-IR', + name: 'Farsi (Iran)', + prompt_name: 'Farsi', + example: 'سلام, دیفای!', + supported: true, + }, + { + value: 'sl-SI', + name: 'Slovensko (Slovenija)', + prompt_name: 'Slovensko', + example: 'Zdravo, Dify!', + supported: true, + }, + { + value: 'id-ID', + name: 'Bahasa Indonesia', + prompt_name: 'Indonesian', + example: 'Halo, Dify!', + supported: true, + }, + { + value: 'ar-TN', + name: 'العربية (تونس)', + prompt_name: 'Tunisian Arabic', + example: 'مرحبا، Dify!', + supported: true, + }, + ], +} as const + +export default data diff --git a/web/i18n-config/server.ts b/web/i18n-config/server.ts index c4e008cf840..75a5f3943b3 100644 --- a/web/i18n-config/server.ts +++ b/web/i18n-config/server.ts @@ -1,19 +1,20 @@ import type { Locale } from '.' +import type { KeyPrefix, Namespace } from './i18next-config' import { match } from '@formatjs/intl-localematcher' import { createInstance } from 'i18next' - import resourcesToBackend from 'i18next-resources-to-backend' +import { camelCase } from 'lodash-es' import Negotiator from 'negotiator' import { cookies, headers } from 'next/headers' import { initReactI18next } from 'react-i18next/initReactI18next' import { i18n } from '.' // https://locize.com/blog/next-13-app-dir-i18n/ -const initI18next = async (lng: Locale, ns: string) => { +const initI18next = async (lng: Locale, ns: Namespace) => { const i18nInstance = createInstance() await i18nInstance .use(initReactI18next) - .use(resourcesToBackend((language: string, namespace: string) => import(`../i18n/${language}/${namespace}.ts`))) + .use(resourcesToBackend((language: Locale, namespace: Namespace) => import(`../i18n/${language}/${namespace}.ts`))) .init({ lng: lng === 'zh-Hans' ? 'zh-Hans' : lng, ns, @@ -22,10 +23,10 @@ const initI18next = async (lng: Locale, ns: string) => { return i18nInstance } -export async function useTranslation(lng: Locale, ns = '', options: Record = {}) { +export async function getTranslation(lng: Locale, ns: Namespace) { const i18nextInstance = await initI18next(lng, ns) return { - t: i18nextInstance.getFixedT(lng, ns, options.keyPrefix), + t: i18nextInstance.getFixedT(lng, 'translation', camelCase(ns) as KeyPrefix), i18n: i18nextInstance, } } diff --git a/web/i18n/en-US/app-api.ts b/web/i18n/en-US/app-api.ts index 1fba63c977d..17f1a06782f 100644 --- a/web/i18n/en-US/app-api.ts +++ b/web/i18n/en-US/app-api.ts @@ -79,6 +79,7 @@ const translation = { pathParams: 'Path Params', query: 'Query', toc: 'Contents', + noContent: 'No content', }, } diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 815c6d9aeb2..5604de3dd16 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -32,6 +32,9 @@ const translation = { cancelDisagree: 'Cancel dislike', userAction: 'User ', }, + code: { + instruction: 'Instruction', + }, notSetAPIKey: { title: 'LLM provider key has not been set', trailFinished: 'Trail finished', diff --git a/web/i18n/en-US/app.ts b/web/i18n/en-US/app.ts index 1f41d3601eb..45ebd61aece 100644 --- a/web/i18n/en-US/app.ts +++ b/web/i18n/en-US/app.ts @@ -1,4 +1,9 @@ const translation = { + theme: { + switchDark: 'Switch to dark theme', + switchLight: 'Switch to light theme', + }, + appNamePlaceholder: 'Give your app a name', createApp: 'CREATE APP', types: { all: 'All', @@ -298,6 +303,7 @@ const translation = { commandHint: 'Type @ to browse by category', slashHint: 'Type / to see all available commands', actions: { + slashTitle: 'Commands', searchApplications: 'Search Applications', searchApplicationsDesc: 'Search and navigate to your applications', searchPlugins: 'Search Plugins', diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 92d24b13516..2e378afeda8 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -1,4 +1,6 @@ const translation = { + loading: 'Loading', + error: 'Error', theme: { theme: 'Theme', light: 'light', @@ -71,6 +73,8 @@ const translation = { saveAndRegenerate: 'Save & Regenerate Child Chunks', view: 'View', viewMore: 'VIEW MORE', + back: 'Back', + imageDownloaded: 'Image downloaded', regenerate: 'Regenerate', submit: 'Submit', skip: 'Skip', @@ -252,6 +256,7 @@ const translation = { feedbackPlaceholder: 'Optional', editWorkspaceInfo: 'Edit Workspace Info', workspaceName: 'Workspace Name', + workspaceNamePlaceholder: 'Enter workspace name', workspaceIcon: 'Workspace Icon', changeEmail: { title: 'Change Email', @@ -515,6 +520,7 @@ const translation = { emptyProviderTip: 'Please install a model provider first.', auth: { unAuthorized: 'Unauthorized', + credentialRemoved: 'Credential removed', authRemoved: 'Auth removed', apiKeys: 'API Keys', addApiKey: 'Add API Key', diff --git a/web/i18n/en-US/dataset.ts b/web/i18n/en-US/dataset.ts index 6ffd312fff6..ee1997f6999 100644 --- a/web/i18n/en-US/dataset.ts +++ b/web/i18n/en-US/dataset.ts @@ -245,6 +245,7 @@ const translation = { button: 'Drag and drop file or folder, or', browse: 'Browse', tip: '{{supportTypes}} (Max {{batchCount}}, {{size}}MB each)', + fileSizeLimitExceeded: 'File size exceeds the {{size}}MB limit', }, } diff --git a/web/i18n/en-US/login.ts b/web/i18n/en-US/login.ts index dd923db2171..3b0c8bbba1f 100644 --- a/web/i18n/en-US/login.ts +++ b/web/i18n/en-US/login.ts @@ -64,6 +64,7 @@ const translation = { passwordInvalid: 'Password must contain letters and numbers, and the length must be greater than 8', registrationNotAllowed: 'Account not found. Please contact the system admin to register.', invalidEmailOrPassword: 'Invalid email or password.', + redirectUrlMissing: 'Redirect URL is missing', }, license: { tip: 'Before starting Dify Community Edition, read the GitHub', diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts index aedd0c62255..f1d697e5076 100644 --- a/web/i18n/en-US/plugin-trigger.ts +++ b/web/i18n/en-US/plugin-trigger.ts @@ -158,6 +158,7 @@ const translation = { }, errors: { createFailed: 'Failed to create subscription', + updateFailed: 'Failed to update subscription', verifyFailed: 'Failed to verify credentials', authFailed: 'Authorization failed', networkError: 'Network error, please try again', diff --git a/web/i18n/en-US/tools.ts b/web/i18n/en-US/tools.ts index 86c225c1b2c..b2753a5721d 100644 --- a/web/i18n/en-US/tools.ts +++ b/web/i18n/en-US/tools.ts @@ -14,6 +14,7 @@ const translation = { }, author: 'By', auth: { + unauthorized: 'Unauthorized', authorized: 'Authorized', setup: 'Set up authorization to use', setupModalTitle: 'Set Up Authorization', diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index a023ac2b910..2122c20aaa3 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -1106,6 +1106,9 @@ const translation = { lastDay: 'Last day', lastDayTooltip: 'Not all months have 31 days. Use the \'last day\' option to select each month\'s final day.', mode: 'Mode', + modeVisual: 'Visual', + modeCron: 'Cron', + selectTime: 'Select time', timezone: 'Timezone', visualConfig: 'Visual Configuration', monthlyDay: 'Monthly Day', diff --git a/web/i18n/ja-JP/app-api.ts b/web/i18n/ja-JP/app-api.ts index e344ad04a9b..35203e53e08 100644 --- a/web/i18n/ja-JP/app-api.ts +++ b/web/i18n/ja-JP/app-api.ts @@ -78,6 +78,7 @@ const translation = { pathParams: 'パスパラメータ', query: 'クエリ', toc: '内容', + noContent: 'コンテンツなし', }, regenerate: '再生', } diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index 77d991974f9..9d1deb47ebc 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -32,6 +32,9 @@ const translation = { cancelDisagree: 'いいえをキャンセル', userAction: 'ユーザー', }, + code: { + instruction: '指示', + }, notSetAPIKey: { title: 'LLM プロバイダーキーが設定されていません', trailFinished: 'トライアル終了', diff --git a/web/i18n/ja-JP/app.ts b/web/i18n/ja-JP/app.ts index f084fc3b8c4..899405e8e7e 100644 --- a/web/i18n/ja-JP/app.ts +++ b/web/i18n/ja-JP/app.ts @@ -1,4 +1,9 @@ const translation = { + theme: { + switchDark: 'ダークテーマに切り替え', + switchLight: 'ライトテーマに切り替え', + }, + appNamePlaceholder: 'アプリに名前を付ける', createApp: 'アプリを作成する', types: { all: '全て', @@ -295,6 +300,7 @@ const translation = { commandHint: '@ を入力してカテゴリ別に参照', slashHint: '/ を入力してすべてのコマンドを表示', actions: { + slashTitle: 'コマンド', searchApplications: 'アプリケーションを検索', searchApplicationsDesc: 'アプリケーションを検索してナビゲート', searchPlugins: 'プラグインを検索', diff --git a/web/i18n/ja-JP/common.ts b/web/i18n/ja-JP/common.ts index bde00cb66ba..87d9fa1fb1d 100644 --- a/web/i18n/ja-JP/common.ts +++ b/web/i18n/ja-JP/common.ts @@ -1,4 +1,6 @@ const translation = { + loading: '読み込み中', + error: 'エラー', theme: { theme: 'テーマ', light: '明るい', @@ -68,6 +70,8 @@ const translation = { selectAll: 'すべて選択', deSelectAll: 'すべて選択解除', now: '今', + back: '戻る', + imageDownloaded: '画像がダウンロードされました', config: 'コンフィグ', yes: 'はい', no: 'いいえ', @@ -248,6 +252,7 @@ const translation = { sendVerificationButton: '確認コードの送信', editWorkspaceInfo: 'ワークスペース情報を編集', workspaceName: 'ワークスペース名', + workspaceNamePlaceholder: 'ワークスペース名を入力', workspaceIcon: 'ワークスペースアイコン', changeEmail: { title: 'メールアドレスを変更', @@ -512,6 +517,7 @@ const translation = { authorizationError: '認証エラー', apiKeys: 'APIキー', unAuthorized: '無許可', + credentialRemoved: '認証情報が削除されました', configModel: 'モデルを構成する', addApiKey: 'APIキーを追加してください', addCredential: '認証情報を追加する', diff --git a/web/i18n/ja-JP/dataset.ts b/web/i18n/ja-JP/dataset.ts index a880dd4f5aa..b6c4e1d40cc 100644 --- a/web/i18n/ja-JP/dataset.ts +++ b/web/i18n/ja-JP/dataset.ts @@ -245,6 +245,7 @@ const translation = { button: 'ファイルまたはフォルダをドラッグアンドドロップ、または', browse: '閲覧', tip: '{{supportTypes}}(最大 {{batchCount}}、各 {{size}}MB)', + fileSizeLimitExceeded: 'ファイルサイズが {{size}}MB の制限を超えています', }, } diff --git a/web/i18n/ja-JP/login.ts b/web/i18n/ja-JP/login.ts index 7069315c9dd..c9e0fe3e1ed 100644 --- a/web/i18n/ja-JP/login.ts +++ b/web/i18n/ja-JP/login.ts @@ -57,6 +57,7 @@ const translation = { passwordInvalid: 'パスワードは文字と数字を含み、長さは 8 以上である必要があります', registrationNotAllowed: 'アカウントが見つかりません。登録するためにシステム管理者に連絡してください。', invalidEmailOrPassword: '無効なメールアドレスまたはパスワードです。', + redirectUrlMissing: 'リダイレクト URL が見つかりません', }, license: { tip: 'GitHub のオープンソースライセンスを確認してから、Dify Community Edition を開始してください。', diff --git a/web/i18n/ja-JP/plugin-trigger.ts b/web/i18n/ja-JP/plugin-trigger.ts index c7453cff423..7dbd8619094 100644 --- a/web/i18n/ja-JP/plugin-trigger.ts +++ b/web/i18n/ja-JP/plugin-trigger.ts @@ -165,6 +165,7 @@ const translation = { verifyFailed: '認証情報の検証に失敗しました', authFailed: '認証に失敗しました', networkError: 'ネットワークエラーです。再試行してください', + updateFailed: 'サブスクリプションの更新に失敗しました', }, }, events: { diff --git a/web/i18n/ja-JP/tools.ts b/web/i18n/ja-JP/tools.ts index 30f623575fa..41dc30ac30d 100644 --- a/web/i18n/ja-JP/tools.ts +++ b/web/i18n/ja-JP/tools.ts @@ -15,6 +15,7 @@ const translation = { author: '著者:', auth: { authorized: '認証済み', + unauthorized: '未認証', setup: '使用するための認証を設定する', setupModalTitle: '認証の設定', setupModalTitleDescription: '資格情報を構成した後、ワークスペース内のすべてのメンバーがアプリケーションのオーケストレーション時にこのツールを使用できます。', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 7f4e7a30093..24f05d6c31a 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -1062,6 +1062,9 @@ const translation = { useVisualPicker: 'ビジュアル設定を使用', nodeTitle: 'スケジュールトリガー', mode: 'モード', + modeVisual: 'ビジュアル', + modeCron: 'Cron', + selectTime: '時間を選択', timezone: 'タイムゾーン', visualConfig: 'ビジュアル設定', monthlyDay: '月の日', diff --git a/web/i18n/zh-Hans/app-api.ts b/web/i18n/zh-Hans/app-api.ts index 4fe97f82317..70219e0cc60 100644 --- a/web/i18n/zh-Hans/app-api.ts +++ b/web/i18n/zh-Hans/app-api.ts @@ -79,6 +79,7 @@ const translation = { pathParams: 'Path Params', query: 'Query', toc: '目录', + noContent: '暂无内容', }, } diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index 33f563af99d..e3df0ea9eaa 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -32,6 +32,9 @@ const translation = { cancelDisagree: '取消反对', userAction: '用户表示', }, + code: { + instruction: '指令', + }, notSetAPIKey: { title: 'LLM 提供者的密钥未设置', trailFinished: '试用已结束', diff --git a/web/i18n/zh-Hans/app.ts b/web/i18n/zh-Hans/app.ts index 517c41de10d..71edaa16296 100644 --- a/web/i18n/zh-Hans/app.ts +++ b/web/i18n/zh-Hans/app.ts @@ -1,4 +1,9 @@ const translation = { + theme: { + switchDark: '切换至深色主题', + switchLight: '切换至浅色主题', + }, + appNamePlaceholder: '给你的应用起个名字', createApp: '创建应用', types: { all: '全部', @@ -297,6 +302,7 @@ const translation = { commandHint: '输入 @ 按类别浏览', slashHint: '输入 / 查看所有可用命令', actions: { + slashTitle: '命令', searchApplications: '搜索应用程序', searchApplicationsDesc: '搜索并导航到您的应用程序', searchPlugins: '搜索插件', diff --git a/web/i18n/zh-Hans/common.ts b/web/i18n/zh-Hans/common.ts index bd0e0e3ba4b..977ffe1919d 100644 --- a/web/i18n/zh-Hans/common.ts +++ b/web/i18n/zh-Hans/common.ts @@ -1,4 +1,6 @@ const translation = { + loading: '加载中', + error: '错误', theme: { theme: '主题', light: '浅色', @@ -78,6 +80,8 @@ const translation = { selectAll: '全选', deSelectAll: '取消全选', now: '现在', + back: '返回', + imageDownloaded: '图片已下载', }, errorMsg: { fieldRequired: '{{field}} 为必填项', @@ -252,6 +256,7 @@ const translation = { feedbackPlaceholder: '选填', editWorkspaceInfo: '编辑工作空间信息', workspaceName: '工作空间名称', + workspaceNamePlaceholder: '输入工作空间名称', workspaceIcon: '工作空间图标', changeEmail: { title: '更改邮箱', @@ -509,6 +514,7 @@ const translation = { emptyProviderTip: '请安装模型供应商。', auth: { unAuthorized: '未授权', + credentialRemoved: '凭据已移除', authRemoved: '授权已移除', apiKeys: 'API 密钥', addApiKey: '添加 API 密钥', diff --git a/web/i18n/zh-Hans/dataset.ts b/web/i18n/zh-Hans/dataset.ts index 73996047628..781fb5aa943 100644 --- a/web/i18n/zh-Hans/dataset.ts +++ b/web/i18n/zh-Hans/dataset.ts @@ -245,6 +245,7 @@ const translation = { tip: '支持 {{supportTypes}} (最多 {{batchCount}} 个,每个大小不超过 {{size}}MB)', button: '拖拽文件或文件夹,或', browse: '浏览', + fileSizeLimitExceeded: '文件大小超过 {{size}}MB 限制', }, } diff --git a/web/i18n/zh-Hans/login.ts b/web/i18n/zh-Hans/login.ts index 13a75eaaaa3..d79005fbdd8 100644 --- a/web/i18n/zh-Hans/login.ts +++ b/web/i18n/zh-Hans/login.ts @@ -64,6 +64,7 @@ const translation = { passwordLengthInValid: '密码必须至少为 8 个字符', registrationNotAllowed: '账户不存在,请联系系统管理员注册账户', invalidEmailOrPassword: '邮箱或密码错误', + redirectUrlMissing: '重定向 URL 缺失', }, license: { tip: '启动 Dify 社区版之前,请阅读 GitHub 上的', diff --git a/web/i18n/zh-Hans/plugin-trigger.ts b/web/i18n/zh-Hans/plugin-trigger.ts index 304cdd47bdd..4f31f517ebd 100644 --- a/web/i18n/zh-Hans/plugin-trigger.ts +++ b/web/i18n/zh-Hans/plugin-trigger.ts @@ -161,6 +161,7 @@ const translation = { verifyFailed: '验证凭据失败', authFailed: '授权失败', networkError: '网络错误,请重试', + updateFailed: '更新订阅失败', }, }, events: { diff --git a/web/i18n/zh-Hans/tools.ts b/web/i18n/zh-Hans/tools.ts index 624fbb241ab..7893a66f66f 100644 --- a/web/i18n/zh-Hans/tools.ts +++ b/web/i18n/zh-Hans/tools.ts @@ -15,6 +15,7 @@ const translation = { author: '作者', auth: { authorized: '已授权', + unauthorized: '未授权', setup: '要使用请先授权', setupModalTitle: '设置授权', setupModalTitleDescription: '配置凭据后,工作区中的所有成员都可以在编排应用程序时使用此工具。', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index fd86292252d..a6daa566679 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -1062,6 +1062,9 @@ const translation = { days: '天', notConfigured: '未配置', mode: '模式', + modeVisual: '可视化', + modeCron: 'Cron', + selectTime: '选择时间', timezone: '时区', visualConfig: '可视化配置', monthlyDay: '月份日期', diff --git a/web/package.json b/web/package.json index baecd2c5c75..f3cc0958118 100644 --- a/web/package.json +++ b/web/package.json @@ -33,10 +33,8 @@ "prepare": "cd ../ && node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky ./web/.husky", "gen-icons": "node ./app/components/base/icons/script.mjs", "uglify-embed": "node ./bin/uglify-embed", - "check-i18n": "node ./i18n-config/check-i18n.js", - "auto-gen-i18n": "node ./i18n-config/auto-gen-i18n.js", - "gen:i18n-types": "node ./i18n-config/generate-i18n-types.js", - "check:i18n-types": "node ./i18n-config/check-i18n-sync.js", + "check-i18n": "tsx ./i18n-config/check-i18n.js", + "auto-gen-i18n": "tsx ./i18n-config/auto-gen-i18n.js", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", @@ -212,7 +210,7 @@ "sass": "^1.93.2", "storybook": "9.1.17", "tailwindcss": "^3.4.18", - "ts-node": "^10.9.2", + "tsx": "^4.21.0", "typescript": "^5.9.3", "uglify-js": "^3.19.3", "vite": "^7.3.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 95d35c24d80..24e710534f0 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -544,9 +544,9 @@ importers: tailwindcss: specifier: ^3.4.18 version: 3.4.18(tsx@4.21.0)(yaml@2.8.2) - ts-node: - specifier: ^10.9.2 - version: 10.9.2(@types/node@18.15.0)(typescript@5.9.3) + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1360,10 +1360,6 @@ packages: '@code-inspector/webpack@1.2.9': resolution: {integrity: sha512-9YEykVrOIc0zMV7pyTyZhCprjScjn6gPPmxb4/OQXKCrP2fAm+NB188rg0s95e4sM7U3qRUpPA4NUH5F7Ogo+g==} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - '@csstools/color-helpers@5.1.0': resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} engines: {node: '>=18'} @@ -2175,9 +2171,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@lexical/clipboard@0.38.2': resolution: {integrity: sha512-dDShUplCu8/o6BB9ousr3uFZ9bltR+HtleF/Tl8FXFNPpZ4AXhbLKUoJuucRuIr+zqT7RxEv/3M6pk/HEoE6NQ==} @@ -3405,18 +3398,6 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -4094,9 +4075,6 @@ packages: resolution: {integrity: sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==} engines: {node: '>=14'} - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} @@ -4624,9 +4602,6 @@ packages: create-hmac@1.1.7: resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - cron-parser@5.4.0: resolution: {integrity: sha512-HxYB8vTvnQFx4dLsZpGRa0uHp6X3qIzS3ZJgJ9v6l/5TJMgeWQbLkR5yiJ5hOxGbc9+jCADDnydIe15ReLZnJA==} engines: {node: '>=18'} @@ -4936,10 +4911,6 @@ packages: resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} - diff@4.0.2: - resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} - engines: {node: '>=0.3.1'} - diffie-hellman@5.0.3: resolution: {integrity: sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==} @@ -6364,9 +6335,6 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - markdown-extensions@2.0.0: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} @@ -8159,20 +8127,6 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - ts-pattern@5.9.0: resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} @@ -8407,9 +8361,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -8797,10 +8748,6 @@ packages: resolution: {integrity: sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -9954,10 +9901,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - '@csstools/color-helpers@5.1.0': {} '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': @@ -10600,11 +10543,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - '@lexical/clipboard@0.38.2': dependencies: '@lexical/html': 0.38.2 @@ -11923,14 +11861,6 @@ snapshots: dependencies: '@testing-library/dom': 10.4.1 - '@tsconfig/node10@1.0.12': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -12742,8 +12672,6 @@ snapshots: are-docs-informative@0.0.2: {} - arg@4.1.3: {} - arg@5.0.2: {} argparse@2.0.1: {} @@ -13291,8 +13219,6 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.12 - create-require@1.1.1: {} - cron-parser@5.4.0: dependencies: luxon: 3.7.2 @@ -13625,8 +13551,6 @@ snapshots: diff-sequences@27.5.1: {} - diff@4.0.2: {} - diffie-hellman@5.0.3: dependencies: bn.js: 4.12.2 @@ -15350,8 +15274,6 @@ snapshots: dependencies: semver: 7.7.3 - make-error@1.3.6: {} - markdown-extensions@2.0.0: {} markdown-table@3.0.4: {} @@ -17655,24 +17577,6 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@18.15.0)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 18.15.0 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - ts-pattern@5.9.0: {} tsconfck@3.1.6(typescript@5.9.3): @@ -17888,8 +17792,6 @@ snapshots: uuid@11.1.0: {} - v8-compile-cache-lib@3.0.1: {} - vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -18312,8 +18214,6 @@ snapshots: dependencies: lib0: 0.2.115 - yn@3.1.1: {} - yocto-queue@0.1.0: {} yocto-queue@1.2.2: {} diff --git a/web/types/i18n.d.ts b/web/types/i18n.d.ts index b5e5b39aa78..3e5b10674a5 100644 --- a/web/types/i18n.d.ts +++ b/web/types/i18n.d.ts @@ -1,74 +1,8 @@ -// TypeScript type definitions for Dify's i18next configuration -// This file is auto-generated. Do not edit manually. -// To regenerate, run: pnpm run gen:i18n-types +import type { messagesEN } from '../i18n-config/i18next-config' import 'react-i18next' -// Extract types from translation files using typeof import pattern - -type AppAnnotationMessages = typeof import('../i18n/en-US/app-annotation').default -type AppApiMessages = typeof import('../i18n/en-US/app-api').default -type AppDebugMessages = typeof import('../i18n/en-US/app-debug').default -type AppLogMessages = typeof import('../i18n/en-US/app-log').default -type AppOverviewMessages = typeof import('../i18n/en-US/app-overview').default -type AppMessages = typeof import('../i18n/en-US/app').default -type BillingMessages = typeof import('../i18n/en-US/billing').default -type CommonMessages = typeof import('../i18n/en-US/common').default -type CustomMessages = typeof import('../i18n/en-US/custom').default -type DatasetCreationMessages = typeof import('../i18n/en-US/dataset-creation').default -type DatasetDocumentsMessages = typeof import('../i18n/en-US/dataset-documents').default -type DatasetHitTestingMessages = typeof import('../i18n/en-US/dataset-hit-testing').default -type DatasetPipelineMessages = typeof import('../i18n/en-US/dataset-pipeline').default -type DatasetSettingsMessages = typeof import('../i18n/en-US/dataset-settings').default -type DatasetMessages = typeof import('../i18n/en-US/dataset').default -type EducationMessages = typeof import('../i18n/en-US/education').default -type ExploreMessages = typeof import('../i18n/en-US/explore').default -type LayoutMessages = typeof import('../i18n/en-US/layout').default -type LoginMessages = typeof import('../i18n/en-US/login').default -type OauthMessages = typeof import('../i18n/en-US/oauth').default -type PipelineMessages = typeof import('../i18n/en-US/pipeline').default -type PluginTagsMessages = typeof import('../i18n/en-US/plugin-tags').default -type PluginTriggerMessages = typeof import('../i18n/en-US/plugin-trigger').default -type PluginMessages = typeof import('../i18n/en-US/plugin').default -type RegisterMessages = typeof import('../i18n/en-US/register').default -type RunLogMessages = typeof import('../i18n/en-US/run-log').default -type ShareMessages = typeof import('../i18n/en-US/share').default -type TimeMessages = typeof import('../i18n/en-US/time').default -type ToolsMessages = typeof import('../i18n/en-US/tools').default -type WorkflowMessages = typeof import('../i18n/en-US/workflow').default - // Complete type structure that matches i18next-config.ts camelCase conversion -export type Messages = { - appAnnotation: AppAnnotationMessages - appApi: AppApiMessages - appDebug: AppDebugMessages - appLog: AppLogMessages - appOverview: AppOverviewMessages - app: AppMessages - billing: BillingMessages - common: CommonMessages - custom: CustomMessages - datasetCreation: DatasetCreationMessages - datasetDocuments: DatasetDocumentsMessages - datasetHitTesting: DatasetHitTestingMessages - datasetPipeline: DatasetPipelineMessages - datasetSettings: DatasetSettingsMessages - dataset: DatasetMessages - education: EducationMessages - explore: ExploreMessages - layout: LayoutMessages - login: LoginMessages - oauth: OauthMessages - pipeline: PipelineMessages - pluginTags: PluginTagsMessages - pluginTrigger: PluginTriggerMessages - plugin: PluginMessages - register: RegisterMessages - runLog: RunLogMessages - share: ShareMessages - time: TimeMessages - tools: ToolsMessages - workflow: WorkflowMessages -} +export type Messages = typeof messagesEN // Utility type to flatten nested object keys into dot notation type FlattenKeys = T extends object @@ -81,19 +15,9 @@ type FlattenKeys = T extends object export type ValidTranslationKeys = FlattenKeys -// Extend react-i18next with Dify's type structure -declare module 'react-i18next' { - type CustomTypeOptions = { - defaultNS: 'translation' - resources: { - translation: Messages - } - } -} - -// Extend i18next for complete type safety declare module 'i18next' { - type CustomTypeOptions = { + // eslint-disable-next-line ts/consistent-type-definitions + interface CustomTypeOptions { defaultNS: 'translation' resources: { translation: Messages diff --git a/web/utils/format.ts b/web/utils/format.ts index a2c3ef97514..d087d690a23 100644 --- a/web/utils/format.ts +++ b/web/utils/format.ts @@ -1,5 +1,6 @@ import type { Dayjs } from 'dayjs' import type { Locale } from '@/i18n-config' +import { localeMap } from '@/i18n-config/language' import 'dayjs/locale/de' import 'dayjs/locale/es' import 'dayjs/locale/fa' @@ -21,30 +22,6 @@ import 'dayjs/locale/vi' import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-tw' -const localeMap: Record = { - 'en-US': 'en', - 'zh-Hans': 'zh-cn', - 'zh-Hant': 'zh-tw', - 'pt-BR': 'pt-br', - 'es-ES': 'es', - 'fr-FR': 'fr', - 'de-DE': 'de', - 'ja-JP': 'ja', - 'ko-KR': 'ko', - 'ru-RU': 'ru', - 'it-IT': 'it', - 'th-TH': 'th', - 'id-ID': 'id', - 'uk-UA': 'uk', - 'vi-VN': 'vi', - 'ro-RO': 'ro', - 'pl-PL': 'pl', - 'hi-IN': 'hi', - 'tr-TR': 'tr', - 'fa-IR': 'fa', - 'sl-SI': 'sl', -} - /** * Formats a number with comma separators. * @example formatNumber(1234567) will return '1,234,567' @@ -149,6 +126,6 @@ export const formatNumberAbbreviated = (num: number) => { } } -export const formatToLocalTime = (time: Dayjs, local: string, format: string) => { +export const formatToLocalTime = (time: Dayjs, local: Locale, format: string) => { return time.locale(localeMap[local] ?? 'en').format(format) } From 18d69775ef6750e407409973ca4412e763bb214e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:03:43 +0800 Subject: [PATCH 05/17] refactor(web): migrate explore app lists from useSWR to TanStack Query (#30076) --- .../app/create-app-dialog/app-list/index.tsx | 21 ++---- .../explore/app-list/index.spec.tsx | 22 +++--- web/app/components/explore/app-list/index.tsx | 21 ++---- .../components/workflow-header/index.spec.tsx | 67 +++++++++++++------ web/service/explore.ts | 14 +--- web/service/use-explore.ts | 27 +++++++- 6 files changed, 91 insertions(+), 81 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index df54de2ff18..ee644781414 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -8,7 +8,6 @@ import { useRouter } from 'next/navigation' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import { useContext } from 'use-context-selector' import AppTypeSelector from '@/app/components/app/type-selector' import { trackEvent } from '@/app/components/base/amplitude' @@ -24,7 +23,8 @@ import ExploreContext from '@/context/explore-context' import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { DSLImportMode } from '@/models/app' import { importDSL } from '@/service/apps' -import { fetchAppDetail, fetchAppList } from '@/service/explore' +import { fetchAppDetail } from '@/service/explore' +import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { cn } from '@/utils/classnames' @@ -70,21 +70,8 @@ const Apps = ({ }) const { - data: { categories, allList }, - } = useSWR( - ['/explore/apps'], - () => - fetchAppList().then(({ categories, recommended_apps }) => ({ - categories, - allList: recommended_apps.sort((a, b) => a.position - b.position), - })), - { - fallbackData: { - categories: [], - allList: [], - }, - }, - ) + data: { categories, allList } = exploreAppListInitialData, + } = useExploreAppList() const filteredList = useMemo(() => { const filteredByCategory = allList.filter((item) => { diff --git a/web/app/components/explore/app-list/index.spec.tsx b/web/app/components/explore/app-list/index.spec.tsx index e73fcdf0ad5..ebf2c9c075d 100644 --- a/web/app/components/explore/app-list/index.spec.tsx +++ b/web/app/components/explore/app-list/index.spec.tsx @@ -10,7 +10,7 @@ import AppList from './index' const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}' let mockTabValue = allCategoriesEn const mockSetTab = vi.fn() -let mockSWRData: { categories: string[], allList: App[] } = { categories: [], allList: [] } +let mockExploreData: { categories: string[], allList: App[] } = { categories: [], allList: [] } const mockHandleImportDSL = vi.fn() const mockHandleImportDSLConfirm = vi.fn() @@ -33,9 +33,9 @@ vi.mock('ahooks', async () => { } }) -vi.mock('swr', () => ({ - __esModule: true, - default: () => ({ data: mockSWRData }), +vi.mock('@/service/use-explore', () => ({ + exploreAppListInitialData: { categories: [], allList: [] }, + useExploreAppList: () => ({ data: mockExploreData }), })) vi.mock('@/service/explore', () => ({ @@ -135,14 +135,14 @@ describe('AppList', () => { beforeEach(() => { vi.clearAllMocks() mockTabValue = allCategoriesEn - mockSWRData = { categories: [], allList: [] } + mockExploreData = { categories: [], allList: [] } }) // Rendering: show loading when categories are not ready. describe('Rendering', () => { it('should render loading when categories are empty', () => { // Arrange - mockSWRData = { categories: [], allList: [] } + mockExploreData = { categories: [], allList: [] } // Act renderWithContext() @@ -153,7 +153,7 @@ describe('AppList', () => { it('should render app cards when data is available', () => { // Arrange - mockSWRData = { + mockExploreData = { categories: ['Writing', 'Translate'], allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })], } @@ -172,7 +172,7 @@ describe('AppList', () => { it('should filter apps by selected category', () => { // Arrange mockTabValue = 'Writing' - mockSWRData = { + mockExploreData = { categories: ['Writing', 'Translate'], allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Beta' }, category: 'Translate' })], } @@ -190,7 +190,7 @@ describe('AppList', () => { describe('User Interactions', () => { it('should filter apps by search keywords', async () => { // Arrange - mockSWRData = { + mockExploreData = { categories: ['Writing'], allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })], } @@ -210,7 +210,7 @@ describe('AppList', () => { it('should handle create flow and confirm DSL when pending', async () => { // Arrange const onSuccess = vi.fn() - mockSWRData = { + mockExploreData = { categories: ['Writing'], allList: [createApp()], }; @@ -246,7 +246,7 @@ describe('AppList', () => { describe('Edge Cases', () => { it('should reset search results when clear icon is clicked', async () => { // Arrange - mockSWRData = { + mockExploreData = { categories: ['Writing'], allList: [createApp(), createApp({ app_id: 'app-2', app: { ...createApp().app, name: 'Gamma' } })], } diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 585c4e60c14..244a116e360 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -6,7 +6,6 @@ import { useDebounceFn } from 'ahooks' import * as React from 'react' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import { useContext } from 'use-context-selector' import DSLConfirmModal from '@/app/components/app/create-from-dsl-modal/dsl-confirm-modal' import Input from '@/app/components/base/input' @@ -20,7 +19,8 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { DSLImportMode, } from '@/models/app' -import { fetchAppDetail, fetchAppList } from '@/service/explore' +import { fetchAppDetail } from '@/service/explore' +import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore' import { cn } from '@/utils/classnames' import s from './style.module.css' @@ -58,21 +58,8 @@ const Apps = ({ }) const { - data: { categories, allList }, - } = useSWR( - ['/explore/apps'], - () => - fetchAppList().then(({ categories, recommended_apps }) => ({ - categories, - allList: recommended_apps.sort((a, b) => a.position - b.position), - })), - { - fallbackData: { - categories: [], - allList: [], - }, - }, - ) + data: { categories, allList } = exploreAppListInitialData, + } = useExploreAppList() const filteredList = allList.filter(item => currCategory === allCategoriesEn || item.category === currCategory) diff --git a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx index 8eee878cd9a..87d7fb30e74 100644 --- a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx @@ -1,6 +1,6 @@ import type { HeaderProps } from '@/app/components/workflow/header' import type { App } from '@/types/app' -import { render, screen } from '@testing-library/react' +import { fireEvent, render, screen } from '@testing-library/react' import { AppModeEnum } from '@/types/app' import WorkflowHeader from './index' @@ -9,8 +9,47 @@ const mockSetCurrentLogItem = vi.fn() const mockSetShowMessageLogModal = vi.fn() const mockResetWorkflowVersionHistory = vi.fn() +const createMockApp = (overrides: Partial = {}): App => ({ + id: 'app-id', + name: 'Workflow App', + description: 'Workflow app description', + author_name: 'Workflow app author', + icon_type: 'emoji', + icon: 'app-icon', + icon_background: '#FFFFFF', + icon_url: null, + use_icon_as_answer_icon: false, + mode: AppModeEnum.COMPLETION, + enable_site: true, + enable_api: true, + api_rpm: 60, + api_rph: 3600, + is_demo: false, + model_config: {} as App['model_config'], + app_model_config: {} as App['app_model_config'], + created_at: 0, + updated_at: 0, + site: { + access_token: 'token', + app_base_url: 'https://example.com', + } as App['site'], + api_base_url: 'https://api.example.com', + tags: [], + access_mode: 'public_access' as App['access_mode'], + ...overrides, +}) + let appDetail: App +const mockAppStore = (overrides: Partial = {}) => { + appDetail = createMockApp(overrides) + mockUseAppStoreSelector.mockImplementation(selector => selector({ + appDetail, + setCurrentLogItem: mockSetCurrentLogItem, + setShowMessageLogModal: mockSetShowMessageLogModal, + })) +} + vi.mock('@/app/components/app/store', () => ({ __esModule: true, useStore: (selector: (state: { appDetail?: App, setCurrentLogItem: typeof mockSetCurrentLogItem, setShowMessageLogModal: typeof mockSetShowMessageLogModal }) => unknown) => mockUseAppStoreSelector(selector), @@ -60,13 +99,7 @@ vi.mock('@/service/use-workflow', () => ({ describe('WorkflowHeader', () => { beforeEach(() => { vi.clearAllMocks() - appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App - - mockUseAppStoreSelector.mockImplementation(selector => selector({ - appDetail, - setCurrentLogItem: mockSetCurrentLogItem, - setShowMessageLogModal: mockSetShowMessageLogModal, - })) + mockAppStore() }) // Verifies the wrapper renders the workflow header shell. @@ -84,12 +117,7 @@ describe('WorkflowHeader', () => { describe('Props', () => { it('should configure preview mode when app is in advanced chat mode', () => { // Arrange - appDetail = { id: 'app-id', mode: AppModeEnum.ADVANCED_CHAT } as unknown as App - mockUseAppStoreSelector.mockImplementation(selector => selector({ - appDetail, - setCurrentLogItem: mockSetCurrentLogItem, - setShowMessageLogModal: mockSetShowMessageLogModal, - })) + mockAppStore({ mode: AppModeEnum.ADVANCED_CHAT }) // Act render() @@ -104,12 +132,7 @@ describe('WorkflowHeader', () => { it('should configure run mode when app is not in advanced chat mode', () => { // Arrange - appDetail = { id: 'app-id', mode: AppModeEnum.COMPLETION } as unknown as App - mockUseAppStoreSelector.mockImplementation(selector => selector({ - appDetail, - setCurrentLogItem: mockSetCurrentLogItem, - setShowMessageLogModal: mockSetShowMessageLogModal, - })) + mockAppStore({ mode: AppModeEnum.COMPLETION }) // Act render() @@ -130,7 +153,7 @@ describe('WorkflowHeader', () => { render() // Act - screen.getByRole('button', { name: 'clear-history' }).click() + fireEvent.click(screen.getByRole('button', { name: /clear-history/i })) // Assert expect(mockSetCurrentLogItem).toHaveBeenCalledWith() @@ -145,7 +168,7 @@ describe('WorkflowHeader', () => { render() // Assert - screen.getByRole('button', { name: 'restore-settled' }).click() + fireEvent.click(screen.getByRole('button', { name: /restore-settled/i })) expect(mockResetWorkflowVersionHistory).toHaveBeenCalled() }) }) diff --git a/web/service/explore.ts b/web/service/explore.ts index 70d5de37f29..b4056da4ab8 100644 --- a/web/service/explore.ts +++ b/web/service/explore.ts @@ -1,6 +1,6 @@ import type { AccessMode } from '@/models/access-control' import type { App, AppCategory } from '@/models/explore' -import { del, get, patch, post } from './base' +import { del, get, patch } from './base' export const fetchAppList = () => { return get<{ @@ -17,14 +17,6 @@ export const fetchInstalledAppList = (app_id?: string | null) => { return get(`/installed-apps${app_id ? `?app_id=${app_id}` : ''}`) } -export const installApp = (id: string) => { - return post('/installed-apps', { - body: { - app_id: id, - }, - }) -} - export const uninstallApp = (id: string) => { return del(`/installed-apps/${id}`) } @@ -37,10 +29,6 @@ export const updatePinStatus = (id: string, isPinned: boolean) => { }) } -export const getToolProviders = () => { - return get('/workspaces/current/tool-providers') -} - export const getAppAccessModeByAppId = (appId: string) => { return get<{ accessMode: AccessMode }>(`/enterprise/webapp/app/access-mode?appId=${appId}`) } diff --git a/web/service/use-explore.ts b/web/service/use-explore.ts index 6e57599b695..8bda8779089 100644 --- a/web/service/use-explore.ts +++ b/web/service/use-explore.ts @@ -1,11 +1,36 @@ +import type { App, AppCategory } from '@/models/explore' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useGlobalPublicStore } from '@/context/global-public-context' import { AccessMode } from '@/models/access-control' -import { fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' +import { fetchAppList, fetchInstalledAppList, getAppAccessModeByAppId, uninstallApp, updatePinStatus } from './explore' import { fetchAppMeta, fetchAppParams } from './share' const NAME_SPACE = 'explore' +type ExploreAppListData = { + categories: AppCategory[] + allList: App[] +} + +export const exploreAppListInitialData: ExploreAppListData = { + categories: [], + allList: [], +} + +export const useExploreAppList = () => { + return useQuery({ + queryKey: [NAME_SPACE, 'appList'], + queryFn: async () => { + const { categories, recommended_apps } = await fetchAppList() + return { + categories, + allList: [...recommended_apps].sort((a, b) => a.position - b.position), + } + }, + placeholderData: exploreAppListInitialData, + }) +} + export const useGetInstalledApps = () => { return useQuery({ queryKey: [NAME_SPACE, 'installedApps'], From eb73f9a9b9fc9eee6be786d2bf5f41fc77e240c4 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 24 Dec 2025 17:17:36 +0800 Subject: [PATCH 06/17] chore: no template string in translation (#30101) --- web/i18n/de-DE/app-debug.ts | 4 +--- web/i18n/en-US/app-debug.ts | 4 +--- web/i18n/es-ES/app-debug.ts | 4 +--- web/i18n/fr-FR/app-debug.ts | 4 +--- web/i18n/it-IT/app-debug.ts | 4 +--- web/i18n/ja-JP/app-debug.ts | 4 +--- web/i18n/ko-KR/app-debug.ts | 4 +--- web/i18n/pl-PL/app-debug.ts | 4 +--- web/i18n/pt-BR/app-debug.ts | 4 +--- web/i18n/ro-RO/app-debug.ts | 4 +--- web/i18n/ru-RU/app-debug.ts | 4 +--- web/i18n/uk-UA/app-debug.ts | 4 +--- web/i18n/vi-VN/app-debug.ts | 4 +--- web/i18n/zh-Hans/app-debug.ts | 4 +--- web/i18n/zh-Hant/app-debug.ts | 4 +--- 15 files changed, 15 insertions(+), 45 deletions(-) diff --git a/web/i18n/de-DE/app-debug.ts b/web/i18n/de-DE/app-debug.ts index 07c9d4be99c..c12a1f52917 100644 --- a/web/i18n/de-DE/app-debug.ts +++ b/web/i18n/de-DE/app-debug.ts @@ -357,9 +357,7 @@ const translation = { visionSettings: { title: 'Vision-Einstellungen', resolution: 'Auflösung', - resolutionTooltip: `Niedrige Auflösung ermöglicht es dem Modell, eine Bildversion mit niedriger Auflösung von 512 x 512 zu erhalten und das Bild mit einem Budget von 65 Tokens darzustellen. Dies ermöglicht schnellere Antworten des API und verbraucht weniger Eingabetokens für Anwendungsfälle, die kein hohes Detail benötigen. - \n - Hohe Auflösung ermöglicht zunächst, dass das Modell das Bild mit niedriger Auflösung sieht und dann detaillierte Ausschnitte von Eingabebildern als 512px Quadrate basierend auf der Größe des Eingabebildes erstellt. Jeder der detaillierten Ausschnitte verwendet das doppelte Token-Budget für insgesamt 129 Tokens.`, + resolutionTooltip: 'Niedrige Auflösung ermöglicht es dem Modell, eine Bildversion mit niedriger Auflösung von 512 x 512 zu erhalten und das Bild mit einem Budget von 65 Tokens darzustellen. Dies ermöglicht schnellere Antworten des API und verbraucht weniger Eingabetokens für Anwendungsfälle, die kein hohes Detail benötigen.\nHohe Auflösung ermöglicht zunächst, dass das Modell das Bild mit niedriger Auflösung sieht und dann detaillierte Ausschnitte von Eingabebildern als 512px Quadrate basierend auf der Größe des Eingabebildes erstellt. Jeder der detaillierten Ausschnitte verwendet das doppelte Token-Budget für insgesamt 129 Tokens.', high: 'Hoch', low: 'Niedrig', uploadMethod: 'Upload-Methode', diff --git a/web/i18n/en-US/app-debug.ts b/web/i18n/en-US/app-debug.ts index 5604de3dd16..9d69c36d830 100644 --- a/web/i18n/en-US/app-debug.ts +++ b/web/i18n/en-US/app-debug.ts @@ -448,9 +448,7 @@ const translation = { visionSettings: { title: 'Vision Settings', resolution: 'Resolution', - resolutionTooltip: `low res will allow model receive a low-res 512 x 512 version of the image, and represent the image with a budget of 65 tokens. This allows the API to return faster responses and consume fewer input tokens for use cases that do not require high detail. - \n - high res will first allows the model to see the low res image and then creates detailed crops of input images as 512px squares based on the input image size. Each of the detailed crops uses twice the token budget for a total of 129 tokens.`, + resolutionTooltip: 'low res will allow model receive a low-res 512 x 512 version of the image, and represent the image with a budget of 65 tokens. This allows the API to return faster responses and consume fewer input tokens for use cases that do not require high detail.\nhigh res will first allows the model to see the low res image and then creates detailed crops of input images as 512px squares based on the input image size. Each of the detailed crops uses twice the token budget for a total of 129 tokens.', high: 'High', low: 'Low', uploadMethod: 'Upload Method', diff --git a/web/i18n/es-ES/app-debug.ts b/web/i18n/es-ES/app-debug.ts index 892e718d32b..4252037e1f9 100644 --- a/web/i18n/es-ES/app-debug.ts +++ b/web/i18n/es-ES/app-debug.ts @@ -353,9 +353,7 @@ const translation = { visionSettings: { title: 'Configuraciones de Visión', resolution: 'Resolución', - resolutionTooltip: `Baja resolución permitirá que el modelo reciba una versión de baja resolución de 512 x 512 de la imagen, y represente la imagen con un presupuesto de 65 tokens. Esto permite que la API devuelva respuestas más rápidas y consuma menos tokens de entrada para casos de uso que no requieren alta detalle. - \n - Alta resolución permitirá primero que el modelo vea la imagen de baja resolución y luego crea recortes detallados de las imágenes de entrada como cuadrados de 512px basados en el tamaño de la imagen de entrada. Cada uno de los recortes detallados usa el doble del presupuesto de tokens para un total de 129 tokens.`, + resolutionTooltip: 'Baja resolución permitirá que el modelo reciba una versión de baja resolución de 512 x 512 de la imagen, y represente la imagen con un presupuesto de 65 tokens. Esto permite que la API devuelva respuestas más rápidas y consuma menos tokens de entrada para casos de uso que no requieren alta detalle.\nAlta resolución permitirá primero que el modelo vea la imagen de baja resolución y luego crea recortes detallados de las imágenes de entrada como cuadrados de 512px basados en el tamaño de la imagen de entrada. Cada uno de los recortes detallados usa el doble del presupuesto de tokens para un total de 129 tokens.', high: 'Alta', low: 'Baja', uploadMethod: 'Método de carga', diff --git a/web/i18n/fr-FR/app-debug.ts b/web/i18n/fr-FR/app-debug.ts index 26ebeca68da..2e65e681cab 100644 --- a/web/i18n/fr-FR/app-debug.ts +++ b/web/i18n/fr-FR/app-debug.ts @@ -357,9 +357,7 @@ const translation = { visionSettings: { title: 'Paramètres de Vision', resolution: 'Résolution', - resolutionTooltip: `low res will allow model receive a low-res 512 x 512 version of the image, and represent the image with a budget of 65 tokens. This allows the API to return faster responses and consume fewer input tokens for use cases that do not require high detail. - \n - high res will first allows the model to see the low res image and then creates detailed crops of input images as 512px squares based on the input image size. Each of the detailed crops uses twice the token budget for a total of 129 tokens.`, + resolutionTooltip: 'low res will allow model receive a low-res 512 x 512 version of the image, and represent the image with a budget of 65 tokens. This allows the API to return faster responses and consume fewer input tokens for use cases that do not require high detail.\nhigh res will first allows the model to see the low res image and then creates detailed crops of input images as 512px squares based on the input image size. Each of the detailed crops uses twice the token budget for a total of 129 tokens.', high: 'Élevé', low: 'Faible', uploadMethod: 'Méthode de Téléchargement', diff --git a/web/i18n/it-IT/app-debug.ts b/web/i18n/it-IT/app-debug.ts index ecae1f3a2ef..e81a83a3dd1 100644 --- a/web/i18n/it-IT/app-debug.ts +++ b/web/i18n/it-IT/app-debug.ts @@ -384,9 +384,7 @@ const translation = { visionSettings: { title: 'Impostazioni di visione', resolution: 'Risoluzione', - resolutionTooltip: `La bassa risoluzione permetterà al modello di ricevere una versione a bassa risoluzione 512 x 512 dell\\'immagine e di rappresentare l\\'immagine con un budget di 65 token. Questo permette all\\'API di restituire risposte più veloci e di consumare meno token di input per casi d\\'uso che non richiedono alta definizione. - \n - L\\'alta risoluzione permetterà al modello di vedere prima l\\'immagine a bassa risoluzione e poi di creare ritagli dettagliati delle immagini di input come quadrati 512px basati sulla dimensione dell\\'immagine di input. Ciascuno dei ritagli dettagliati utilizza il doppio del budget dei token per un totale di 129 token.`, + resolutionTooltip: 'La bassa risoluzione permetterà al modello di ricevere una versione a bassa risoluzione 512 x 512 dell\'immagine e di rappresentare l\'immagine con un budget di 65 token. Questo permette all\'API di restituire risposte più veloci e di consumare meno token di input per casi d\'uso che non richiedono alta definizione.\nL\'alta risoluzione permetterà al modello di vedere prima l\'immagine a bassa risoluzione e poi di creare ritagli dettagliati delle immagini di input come quadrati 512px basati sulla dimensione dell\'immagine di input. Ciascuno dei ritagli dettagliati utilizza il doppio del budget dei token per un totale di 129 token.', high: 'Alta', low: 'Bassa', uploadMethod: 'Metodo di caricamento', diff --git a/web/i18n/ja-JP/app-debug.ts b/web/i18n/ja-JP/app-debug.ts index 9d1deb47ebc..06b47c1a473 100644 --- a/web/i18n/ja-JP/app-debug.ts +++ b/web/i18n/ja-JP/app-debug.ts @@ -442,9 +442,7 @@ const translation = { visionSettings: { title: 'ビジョン設定', resolution: '解像度', - resolutionTooltip: `低解像度では、モデルに低解像度の 512 x 512 バージョンの画像を受け取らせ、画像を 65 トークンの予算で表現します。これにより、API がより迅速な応答を返し、高い詳細が必要なユースケースでは入力トークンを消費します。 - \n - 高解像度では、まずモデルに低解像度の画像を見せ、その後、入力画像サイズに基づいて 512px の正方形の詳細なクロップを作成します。詳細なクロップごとに 129 トークンの予算を使用します。`, + resolutionTooltip: '低解像度では、モデルに低解像度の 512 x 512 バージョンの画像を受け取らせ、画像を 65 トークンの予算で表現します。これにより、API がより迅速な応答を返し、高い詳細が必要なユースケースでは入力トークンを消費します。\n高解像度では、まずモデルに低解像度の画像を見せ、その後、入力画像サイズに基づいて 512px の正方形の詳細なクロップを作成します。詳細なクロップごとに 129 トークンの予算を使用します。', high: '高', low: '低', uploadMethod: 'アップロード方法', diff --git a/web/i18n/ko-KR/app-debug.ts b/web/i18n/ko-KR/app-debug.ts index c9e048df0ef..5258287285f 100644 --- a/web/i18n/ko-KR/app-debug.ts +++ b/web/i18n/ko-KR/app-debug.ts @@ -353,9 +353,7 @@ const translation = { visionSettings: { title: '비전 설정', resolution: '해상도', - resolutionTooltip: `저해상도는 모델에게 512 x 512 해상도의 저해상도 이미지를 제공하여 65 토큰의 예산으로 이미지를 표현합니다. 이로 인해 API 는 더 빠른 응답을 제공하며 높은 세부 정보가 필요한 경우 토큰 소모를 늘립니다. - \n - 고해상도는 먼저 모델에게 저해상도 이미지를 보여주고, 그 후 입력 이미지 크기에 따라 512px 의 정사각형 세부 사진을 만듭니다. 각 세부 사진에 대해 129 토큰의 예산을 사용합니다.`, + resolutionTooltip: '저해상도는 모델에게 512 x 512 해상도의 저해상도 이미지를 제공하여 65 토큰의 예산으로 이미지를 표현합니다. 이로 인해 API 는 더 빠른 응답을 제공하며 높은 세부 정보가 필요한 경우 토큰 소모를 늘립니다.\n고해상도는 먼저 모델에게 저해상도 이미지를 보여주고, 그 후 입력 이미지 크기에 따라 512px 의 정사각형 세부 사진을 만듭니다. 각 세부 사진에 대해 129 토큰의 예산을 사용합니다.', high: '고', low: '저', uploadMethod: '업로드 방식', diff --git a/web/i18n/pl-PL/app-debug.ts b/web/i18n/pl-PL/app-debug.ts index 262b4a204fc..943896effc7 100644 --- a/web/i18n/pl-PL/app-debug.ts +++ b/web/i18n/pl-PL/app-debug.ts @@ -379,9 +379,7 @@ const translation = { visionSettings: { title: 'Ustawienia Wizji', resolution: 'Rozdzielczość', - resolutionTooltip: `niska rozdzielczość pozwoli modelowi odbierać obrazy o rozdzielczości 512 x 512 i reprezentować obraz z limitem 65 tokenów. Pozwala to API na szybsze odpowiedzi i zużywa mniej tokenów wejściowych dla przypadków, które nie wymagają wysokiego szczegółu. - \n - wysoka rozdzielczość pozwala najpierw modelowi zobaczyć obraz niskiej rozdzielczości, a następnie tworzy szczegółowe przycięcia obrazów wejściowych jako 512px kwadratów w oparciu o rozmiar obrazu wejściowego. Każde z tych szczegółowych przycięć używa dwukrotności budżetu tokenów, co daje razem 129 tokenów.`, + resolutionTooltip: 'niska rozdzielczość pozwoli modelowi odbierać obrazy o rozdzielczości 512 x 512 i reprezentować obraz z limitem 65 tokenów. Pozwala to API na szybsze odpowiedzi i zużywa mniej tokenów wejściowych dla przypadków, które nie wymagają wysokiego szczegółu.\nwysoka rozdzielczość pozwala najpierw modelowi zobaczyć obraz niskiej rozdzielczości, a następnie tworzy szczegółowe przycięcia obrazów wejściowych jako 512px kwadratów w oparciu o rozmiar obrazu wejściowego. Każde z tych szczegółowych przycięć używa dwukrotności budżetu tokenów, co daje razem 129 tokenów.', high: 'Wysoka', low: 'Niska', uploadMethod: 'Metoda przesyłania', diff --git a/web/i18n/pt-BR/app-debug.ts b/web/i18n/pt-BR/app-debug.ts index d578cb5a84c..30b9f59dd47 100644 --- a/web/i18n/pt-BR/app-debug.ts +++ b/web/i18n/pt-BR/app-debug.ts @@ -359,9 +359,7 @@ const translation = { visionSettings: { title: 'Configurações de Visão', resolution: 'Resolução', - resolutionTooltip: `Baixa resolução permitirá que o modelo receba uma versão de baixa resolução de 512 x 512 da imagem e represente a imagem com um orçamento de 65 tokens. Isso permite que a API retorne respostas mais rápidas e consuma menos tokens de entrada para casos de uso que não exigem alta precisão. - \n - Alta resolução permitirá que o modelo veja a imagem de baixa resolução e crie recortes detalhados das imagens de entrada como quadrados de 512px com base no tamanho da imagem de entrada. Cada um dos recortes detalhados usa o dobro do orçamento de tokens, totalizando 129 tokens.`, + resolutionTooltip: 'Baixa resolução permitirá que o modelo receba uma versão de baixa resolução de 512 x 512 da imagem e represente a imagem com um orçamento de 65 tokens. Isso permite que a API retorne respostas mais rápidas e consuma menos tokens de entrada para casos de uso que não exigem alta precisão.\nAlta resolução permitirá que o modelo veja a imagem de baixa resolução e crie recortes detalhados das imagens de entrada como quadrados de 512px com base no tamanho da imagem de entrada. Cada um dos recortes detalhados usa o dobro do orçamento de tokens, totalizando 129 tokens.', high: 'Alta', low: 'Baixa', uploadMethod: 'Método de Upload', diff --git a/web/i18n/ro-RO/app-debug.ts b/web/i18n/ro-RO/app-debug.ts index de8fd7a44ff..8e36078be52 100644 --- a/web/i18n/ro-RO/app-debug.ts +++ b/web/i18n/ro-RO/app-debug.ts @@ -359,9 +359,7 @@ const translation = { visionSettings: { title: 'Setări Viziune', resolution: 'Rezoluție', - resolutionTooltip: `rezoluția joasă va permite modelului să primească o versiune de 512 x 512 pixeli a imaginii și să o reprezinte cu un buget de 65 de tokenuri. Acest lucru permite API-ului să returneze răspunsuri mai rapide și să consume mai puține tokenuri de intrare pentru cazurile de utilizare care nu necesită detalii ridicate. - \n - rezoluția ridicată va permite în primul rând modelului să vadă imaginea la rezoluție scăzută și apoi va crea decupaje detaliate ale imaginilor de intrare ca pătrate de 512 pixeli, în funcție de dimensiunea imaginii de intrare. Fiecare decupaj detaliat utilizează un buget de token dublu, pentru un total de 129 de tokenuri.`, + resolutionTooltip: 'rezoluția joasă va permite modelului să primească o versiune de 512 x 512 pixeli a imaginii și să o reprezinte cu un buget de 65 de tokenuri. Acest lucru permite API-ului să returneze răspunsuri mai rapide și să consume mai puține tokenuri de intrare pentru cazurile de utilizare care nu necesită detalii ridicate.\nrezoluția ridicată va permite în primul rând modelului să vadă imaginea la rezoluție scăzută și apoi va crea decupaje detaliate ale imaginilor de intrare ca pătrate de 512 pixeli, în funcție de dimensiunea imaginii de intrare. Fiecare decupaj detaliat utilizează un buget de token dublu, pentru un total de 129 de tokenuri.', high: 'Ridicat', low: 'Scăzut', uploadMethod: 'Metodă de încărcare', diff --git a/web/i18n/ru-RU/app-debug.ts b/web/i18n/ru-RU/app-debug.ts index 1d86e0778a0..ea3b969df4a 100644 --- a/web/i18n/ru-RU/app-debug.ts +++ b/web/i18n/ru-RU/app-debug.ts @@ -425,9 +425,7 @@ const translation = { visionSettings: { title: 'Настройки зрения', resolution: 'Разрешение', - resolutionTooltip: `Низкое разрешение позволит модели получать версию изображения с низким разрешением 512 x 512 и представлять изображение с бюджетом 65 токенов. Это позволяет API возвращать ответы быстрее и потреблять меньше входных токенов для случаев использования, не требующих высокой детализации. - \n - Высокое разрешение сначала позволит модели увидеть изображение с низким разрешением, а затем создаст детальные фрагменты входных изображений в виде квадратов 512 пикселей на основе размера входного изображения. Каждый из детальных фрагментов использует вдвое больший бюджет токенов, в общей сложности 129 токенов.`, + resolutionTooltip: 'Низкое разрешение позволит модели получать версию изображения с низким разрешением 512 x 512 и представлять изображение с бюджетом 65 токенов. Это позволяет API возвращать ответы быстрее и потреблять меньше входных токенов для случаев использования, не требующих высокой детализации.\nВысокое разрешение сначала позволит модели увидеть изображение с низким разрешением, а затем создаст детальные фрагменты входных изображений в виде квадратов 512 пикселей на основе размера входного изображения. Каждый из детальных фрагментов использует вдвое больший бюджет токенов, в общей сложности 129 токенов.', high: 'Высокое', low: 'Низкое', uploadMethod: 'Метод загрузки', diff --git a/web/i18n/uk-UA/app-debug.ts b/web/i18n/uk-UA/app-debug.ts index c593d2a7307..18b4d321639 100644 --- a/web/i18n/uk-UA/app-debug.ts +++ b/web/i18n/uk-UA/app-debug.ts @@ -373,9 +373,7 @@ const translation = { visionSettings: { title: 'Налаштування зображень', // Vision Settings resolution: 'Роздільна здатність', // Resolution - resolutionTooltip: `низька роздільна здатність дозволить моделі отримати зображення з низькою роздільною здатністю 512 x 512 пікселів і представити зображення з обмеженням у 65 токенів. Це дозволяє API швидше повертати відповіді та споживати менше вхідних токенів для випадків використання, які не потребують високої деталізації. - \n - висока роздільна здатність спочатку дозволить моделі побачити зображення з низькою роздільною здатністю, а потім створити детальні фрагменти вхідних зображень у вигляді квадратів 512px на основі розміру вхідного зображення. Кожен із детальних фрагментів використовує подвійний запас токенів, загалом 129 токенів.`, + resolutionTooltip: 'низька роздільна здатність дозволить моделі отримати зображення з низькою роздільною здатністю 512 x 512 пікселів і представити зображення з обмеженням у 65 токенів. Це дозволяє API швидше повертати відповіді та споживати менше вхідних токенів для випадків використання, які не потребують високої деталізації.\nвисока роздільна здатність спочатку дозволить моделі побачити зображення з низькою роздільною здатністю, а потім створити детальні фрагменти вхідних зображень у вигляді квадратів 512px на основі розміру вхідного зображення. Кожен із детальних фрагментів використовує подвійний запас токенів, загалом 129 токенів.', high: 'Висока', // High low: 'Низька', // Low uploadMethod: 'Спосіб завантаження', // Upload Method diff --git a/web/i18n/vi-VN/app-debug.ts b/web/i18n/vi-VN/app-debug.ts index 150dfe488b5..158a6b6ce96 100644 --- a/web/i18n/vi-VN/app-debug.ts +++ b/web/i18n/vi-VN/app-debug.ts @@ -353,9 +353,7 @@ const translation = { visionSettings: { title: 'Cài đặt thị giác', resolution: 'Độ phân giải', - resolutionTooltip: `Độ phân giải thấp sẽ cho phép mô hình nhận một phiên bản hình ảnh 512 x 512 thấp hơn, và đại diện cho hình ảnh với ngân sách 65 token. Điều này cho phép API trả về phản hồi nhanh hơn và tiêu thụ ít token đầu vào cho các trường hợp sử dụng không yêu cầu chi tiết cao. - \n - Độ phân giải cao sẽ đầu tiên cho phép mô hình nhìn thấy hình ảnh thấp hơn và sau đó tạo ra các cắt chi tiết của hình ảnh đầu vào dưới dạng hình vuông 512px dựa trên kích thước hình ảnh đầu vào. Mỗi cắt chi tiết sử dụng hai lần ngân sách token cho tổng cộng 129 token.`, + resolutionTooltip: 'Độ phân giải thấp sẽ cho phép mô hình nhận một phiên bản hình ảnh 512 x 512 thấp hơn, và đại diện cho hình ảnh với ngân sách 65 token. Điều này cho phép API trả về phản hồi nhanh hơn và tiêu thụ ít token đầu vào cho các trường hợp sử dụng không yêu cầu chi tiết cao.\nĐộ phân giải cao sẽ đầu tiên cho phép mô hình nhìn thấy hình ảnh thấp hơn và sau đó tạo ra các cắt chi tiết của hình ảnh đầu vào dưới dạng hình vuông 512px dựa trên kích thước hình ảnh đầu vào. Mỗi cắt chi tiết sử dụng hai lần ngân sách token cho tổng cộng 129 token.', high: 'Cao', low: 'Thấp', uploadMethod: 'Phương thức tải lên', diff --git a/web/i18n/zh-Hans/app-debug.ts b/web/i18n/zh-Hans/app-debug.ts index e3df0ea9eaa..5d6c2842a5f 100644 --- a/web/i18n/zh-Hans/app-debug.ts +++ b/web/i18n/zh-Hans/app-debug.ts @@ -444,9 +444,7 @@ const translation = { visionSettings: { title: '视觉设置', resolution: '分辨率', - resolutionTooltip: `低分辨率模式将使模型接收图像的低分辨率版本,尺寸为 512 x 512,并使用 65 Tokens 来表示图像。这样可以使 API 更快地返回响应,并在不需要高细节的用例中消耗更少的输入。 - \n - 高分辨率模式将首先允许模型查看低分辨率图像,然后根据输入图像的大小创建 512 像素的详细裁剪图像。每个详细裁剪图像使用两倍的预算总共为 129 Tokens。`, + resolutionTooltip: '低分辨率模式将使模型接收图像的低分辨率版本,尺寸为 512 x 512,并使用 65 Tokens 来表示图像。这样可以使 API 更快地返回响应,并在不需要高细节的用例中消耗更少的输入。\n高分辨率模式将首先允许模型查看低分辨率图像,然后根据输入图像的大小创建 512 像素的详细裁剪图像。每个详细裁剪图像使用两倍的预算总共为 129 Tokens。', high: '高', low: '低', uploadMethod: '上传方式', diff --git a/web/i18n/zh-Hant/app-debug.ts b/web/i18n/zh-Hant/app-debug.ts index b66fcb98169..78b179097de 100644 --- a/web/i18n/zh-Hant/app-debug.ts +++ b/web/i18n/zh-Hant/app-debug.ts @@ -353,9 +353,7 @@ const translation = { visionSettings: { title: '視覺設定', resolution: '解析度', - resolutionTooltip: `低解析度模式將使模型接收影象的低解析度版本,尺寸為 512 x 512,並使用 65 Tokens 來表示影象。這樣可以使 API 更快地返回響應,並在不需要高細節的用例中消耗更少的輸入。 - \n - 高解析度模式將首先允許模型檢視低解析度影象,然後根據輸入影象的大小建立 512 畫素的詳細裁剪影象。每個詳細裁剪影象使用兩倍的預算總共為 129 Tokens。`, + resolutionTooltip: '低解析度模式將使模型接收影象的低解析度版本,尺寸為 512 x 512,並使用 65 Tokens 來表示影象。這樣可以使 API 更快地返回響應,並在不需要高細節的用例中消耗更少的輸入。\n高解析度模式將首先允許模型檢視低解析度影象,然後根據輸入影象的大小建立 512 畫素的詳細裁剪影象。每個詳細裁剪影象使用兩倍的預算總共為 129 Tokens。', high: '高', low: '低', uploadMethod: '上傳方式', From 2f9d718997393e38e369a8ab378ad12a8b89a743 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 24 Dec 2025 17:23:30 +0800 Subject: [PATCH 07/17] fix: fix use build_request lead unexpect param (#30095) --- api/core/helper/ssrf_proxy.py | 10 ++- .../unit_tests/core/helper/test_ssrf_proxy.py | 61 ++----------------- 2 files changed, 8 insertions(+), 63 deletions(-) diff --git a/api/core/helper/ssrf_proxy.py b/api/core/helper/ssrf_proxy.py index f2172e4e2f9..0b36969cf9a 100644 --- a/api/core/helper/ssrf_proxy.py +++ b/api/core/helper/ssrf_proxy.py @@ -118,13 +118,11 @@ def make_request(method, url, max_retries=SSRF_DEFAULT_MAX_RETRIES, **kwargs): # Build the request manually to preserve the Host header # httpx may override the Host header when using a proxy, so we use # the request API to explicitly set headers before sending - request = client.build_request(method=method, url=url, **kwargs) - - # If user explicitly provided a Host header, ensure it's preserved + headers = {k: v for k, v in headers.items() if k.lower() != "host"} if user_provided_host is not None: - request.headers["Host"] = user_provided_host - - response = client.send(request) + headers["host"] = user_provided_host + kwargs["headers"] = headers + response = client.request(method=method, url=url, **kwargs) # Check for SSRF protection by Squid proxy if response.status_code in (401, 403): diff --git a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py index d5bc3283feb..beae1d03587 100644 --- a/api/tests/unit_tests/core/helper/test_ssrf_proxy.py +++ b/api/tests/unit_tests/core/helper/test_ssrf_proxy.py @@ -1,11 +1,9 @@ -import secrets from unittest.mock import MagicMock, patch import pytest from core.helper.ssrf_proxy import ( SSRF_DEFAULT_MAX_RETRIES, - STATUS_FORCELIST, _get_user_provided_host_header, make_request, ) @@ -14,11 +12,10 @@ from core.helper.ssrf_proxy import ( @patch("core.helper.ssrf_proxy._get_ssrf_client") def test_successful_request(mock_get_client): mock_client = MagicMock() - mock_request = MagicMock() mock_response = MagicMock() mock_response.status_code = 200 mock_client.send.return_value = mock_response - mock_client.build_request.return_value = mock_request + mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client response = make_request("GET", "http://example.com") @@ -28,11 +25,10 @@ def test_successful_request(mock_get_client): @patch("core.helper.ssrf_proxy._get_ssrf_client") def test_retry_exceed_max_retries(mock_get_client): mock_client = MagicMock() - mock_request = MagicMock() mock_response = MagicMock() mock_response.status_code = 500 mock_client.send.return_value = mock_response - mock_client.build_request.return_value = mock_request + mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client with pytest.raises(Exception) as e: @@ -40,32 +36,6 @@ def test_retry_exceed_max_retries(mock_get_client): assert str(e.value) == f"Reached maximum retries ({SSRF_DEFAULT_MAX_RETRIES - 1}) for URL http://example.com" -@patch("core.helper.ssrf_proxy._get_ssrf_client") -def test_retry_logic_success(mock_get_client): - mock_client = MagicMock() - mock_request = MagicMock() - mock_response = MagicMock() - mock_response.status_code = 200 - - side_effects = [] - for _ in range(SSRF_DEFAULT_MAX_RETRIES): - status_code = secrets.choice(STATUS_FORCELIST) - retry_response = MagicMock() - retry_response.status_code = status_code - side_effects.append(retry_response) - - side_effects.append(mock_response) - mock_client.send.side_effect = side_effects - mock_client.build_request.return_value = mock_request - mock_get_client.return_value = mock_client - - response = make_request("GET", "http://example.com", max_retries=SSRF_DEFAULT_MAX_RETRIES) - - assert response.status_code == 200 - assert mock_client.send.call_count == SSRF_DEFAULT_MAX_RETRIES + 1 - assert mock_client.build_request.call_count == SSRF_DEFAULT_MAX_RETRIES + 1 - - class TestGetUserProvidedHostHeader: """Tests for _get_user_provided_host_header function.""" @@ -111,14 +81,12 @@ def test_host_header_preservation_without_user_header(mock_get_client): mock_response = MagicMock() mock_response.status_code = 200 mock_client.send.return_value = mock_response - mock_client.build_request.return_value = mock_request + mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client response = make_request("GET", "http://example.com") assert response.status_code == 200 - # build_request should be called without headers dict containing Host - mock_client.build_request.assert_called_once() # Host should not be set if not provided by user assert "Host" not in mock_request.headers or mock_request.headers.get("Host") is None @@ -132,31 +100,10 @@ def test_host_header_preservation_with_user_header(mock_get_client): mock_response = MagicMock() mock_response.status_code = 200 mock_client.send.return_value = mock_response - mock_client.build_request.return_value = mock_request + mock_client.request.return_value = mock_response mock_get_client.return_value = mock_client custom_host = "custom.example.com:8080" response = make_request("GET", "http://example.com", headers={"Host": custom_host}) assert response.status_code == 200 - # Verify build_request was called - mock_client.build_request.assert_called_once() - # Verify the Host header was set on the request object - assert mock_request.headers.get("Host") == custom_host - mock_client.send.assert_called_once_with(mock_request) - - -@patch("core.helper.ssrf_proxy._get_ssrf_client") -@pytest.mark.parametrize("host_key", ["host", "HOST"]) -def test_host_header_preservation_case_insensitive(mock_get_client, host_key): - """Test that Host header is preserved regardless of case.""" - mock_client = MagicMock() - mock_request = MagicMock() - mock_request.headers = {} - mock_response = MagicMock() - mock_response.status_code = 200 - mock_client.send.return_value = mock_response - mock_client.build_request.return_value = mock_request - mock_get_client.return_value = mock_client - response = make_request("GET", "http://example.com", headers={host_key: "api.example.com"}) - assert mock_request.headers.get("Host") == "api.example.com" From 64a14dcdbcc79498a22a36f592c711c0f28049f0 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:20:36 +0800 Subject: [PATCH 08/17] fix(web): remove incorrect placeholderData usage in useExploreAppList (#30102) --- .../app/create-app-dialog/app-list/index.tsx | 14 +++++++---- .../explore/app-list/index.spec.tsx | 18 ++++++++++---- web/app/components/explore/app-list/index.tsx | 24 ++++++++++++------- web/service/use-explore.ts | 6 ----- 4 files changed, 37 insertions(+), 25 deletions(-) diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index ee644781414..0df13e1ba19 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -24,7 +24,7 @@ import { useTabSearchParams } from '@/hooks/use-tab-searchparams' import { DSLImportMode } from '@/models/app' import { importDSL } from '@/service/apps' import { fetchAppDetail } from '@/service/explore' -import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore' +import { useExploreAppList } from '@/service/use-explore' import { AppModeEnum } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { cn } from '@/utils/classnames' @@ -70,10 +70,14 @@ const Apps = ({ }) const { - data: { categories, allList } = exploreAppListInitialData, + data, + isLoading, } = useExploreAppList() const filteredList = useMemo(() => { + if (!data) + return [] + const { allList } = data const filteredByCategory = allList.filter((item) => { if (currCategory === allCategoriesEn) return true @@ -94,7 +98,7 @@ const Apps = ({ return true return false }) - }, [currentType, currCategory, allCategoriesEn, allList]) + }, [currentType, currCategory, allCategoriesEn, data]) const searchFilteredList = useMemo(() => { if (!searchKeywords || !filteredList || filteredList.length === 0) @@ -156,7 +160,7 @@ const Apps = ({ } } - if (!categories || categories.length === 0) { + if (isLoading) { return (
@@ -190,7 +194,7 @@ const Apps = ({
{!searchKeywords && (
- { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} /> + { setCurrCategory(category) }} onCreateFromBlank={onCreateFromBlank} />
)}
diff --git a/web/app/components/explore/app-list/index.spec.tsx b/web/app/components/explore/app-list/index.spec.tsx index ebf2c9c075d..c84da1931c6 100644 --- a/web/app/components/explore/app-list/index.spec.tsx +++ b/web/app/components/explore/app-list/index.spec.tsx @@ -10,7 +10,9 @@ import AppList from './index' const allCategoriesEn = 'explore.apps.allCategories:{"lng":"en"}' let mockTabValue = allCategoriesEn const mockSetTab = vi.fn() -let mockExploreData: { categories: string[], allList: App[] } = { categories: [], allList: [] } +let mockExploreData: { categories: string[], allList: App[] } | undefined = { categories: [], allList: [] } +let mockIsLoading = false +let mockIsError = false const mockHandleImportDSL = vi.fn() const mockHandleImportDSLConfirm = vi.fn() @@ -34,8 +36,11 @@ vi.mock('ahooks', async () => { }) vi.mock('@/service/use-explore', () => ({ - exploreAppListInitialData: { categories: [], allList: [] }, - useExploreAppList: () => ({ data: mockExploreData }), + useExploreAppList: () => ({ + data: mockExploreData, + isLoading: mockIsLoading, + isError: mockIsError, + }), })) vi.mock('@/service/explore', () => ({ @@ -136,13 +141,16 @@ describe('AppList', () => { vi.clearAllMocks() mockTabValue = allCategoriesEn mockExploreData = { categories: [], allList: [] } + mockIsLoading = false + mockIsError = false }) // Rendering: show loading when categories are not ready. describe('Rendering', () => { - it('should render loading when categories are empty', () => { + it('should render loading when the query is loading', () => { // Arrange - mockExploreData = { categories: [], allList: [] } + mockExploreData = undefined + mockIsLoading = true // Act renderWithContext() diff --git a/web/app/components/explore/app-list/index.tsx b/web/app/components/explore/app-list/index.tsx index 244a116e360..5ab68f9b045 100644 --- a/web/app/components/explore/app-list/index.tsx +++ b/web/app/components/explore/app-list/index.tsx @@ -20,7 +20,7 @@ import { DSLImportMode, } from '@/models/app' import { fetchAppDetail } from '@/service/explore' -import { exploreAppListInitialData, useExploreAppList } from '@/service/use-explore' +import { useExploreAppList } from '@/service/use-explore' import { cn } from '@/utils/classnames' import s from './style.module.css' @@ -28,11 +28,6 @@ type AppsProps = { onSuccess?: () => void } -export enum PageType { - EXPLORE = 'explore', - CREATE = 'create', -} - const Apps = ({ onSuccess, }: AppsProps) => { @@ -58,10 +53,16 @@ const Apps = ({ }) const { - data: { categories, allList } = exploreAppListInitialData, + data, + isLoading, + isError, } = useExploreAppList() - const filteredList = allList.filter(item => currCategory === allCategoriesEn || item.category === currCategory) + const filteredList = useMemo(() => { + if (!data) + return [] + return data.allList.filter(item => currCategory === allCategoriesEn || item.category === currCategory) + }, [data, currCategory, allCategoriesEn]) const searchFilteredList = useMemo(() => { if (!searchKeywords || !filteredList || filteredList.length === 0) @@ -119,7 +120,7 @@ const Apps = ({ }) }, [handleImportDSLConfirm, onSuccess]) - if (!categories || categories.length === 0) { + if (isLoading) { return (
@@ -127,6 +128,11 @@ const Apps = ({ ) } + if (isError || !data) + return null + + const { categories } = data + return (
{ return useQuery({ queryKey: [NAME_SPACE, 'appList'], @@ -27,7 +22,6 @@ export const useExploreAppList = () => { allList: [...recommended_apps].sort((a, b) => a.position - b.position), } }, - placeholderData: exploreAppListInitialData, }) } From 5896bc89f5e2d1f939fc5a3b35c63b43341a6121 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 24 Dec 2025 18:21:01 +0800 Subject: [PATCH 09/17] refactor(web): migrate workflow run history from useSWR to TanStack Query (#30077) --- .../components/rag-pipeline-header/index.tsx | 2 -- .../components/workflow-header/index.spec.tsx | 11 -------- .../components/workflow-header/index.tsx | 4 --- .../workflow/header/view-history.tsx | 11 +++----- web/service/use-workflow.ts | 9 +++++++ web/service/workflow.ts | 27 +++++-------------- 6 files changed, 19 insertions(+), 45 deletions(-) diff --git a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx b/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx index fff720469cb..fe2490f2810 100644 --- a/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx +++ b/web/app/components/rag-pipeline/components/rag-pipeline-header/index.tsx @@ -8,7 +8,6 @@ import Header from '@/app/components/workflow/header' import { useStore, } from '@/app/components/workflow/store' -import { fetchWorkflowRunHistory } from '@/service/workflow' import InputFieldButton from './input-field-button' import Publisher from './publisher' import RunMode from './run-mode' @@ -21,7 +20,6 @@ const RagPipelineHeader = () => { const viewHistoryProps = useMemo(() => { return { historyUrl: `/rag/pipelines/${pipelineId}/workflow-runs`, - historyFetcher: fetchWorkflowRunHistory, } }, [pipelineId]) diff --git a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx index 87d7fb30e74..5563af01d39 100644 --- a/web/app/components/workflow-app/components/workflow-header/index.spec.tsx +++ b/web/app/components/workflow-app/components/workflow-header/index.spec.tsx @@ -58,16 +58,12 @@ vi.mock('@/app/components/app/store', () => ({ vi.mock('@/app/components/workflow/header', () => ({ __esModule: true, default: (props: HeaderProps) => { - const historyFetcher = props.normal?.runAndHistoryProps?.viewHistoryProps?.historyFetcher - const hasHistoryFetcher = typeof historyFetcher === 'function' - return (
- - - +
+ + + + + + +
@@ -78,6 +94,14 @@ const SubscriptionCard = ({ data }: Props) => { workflowsInUse={data.workflows_in_use} /> )} + + {isShowEditModal && ( + + )} ) } diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts b/web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts new file mode 100644 index 00000000000..adfda165477 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/types.ts @@ -0,0 +1,9 @@ +export enum SubscriptionListMode { + PANEL = 'panel', + SELECTOR = 'selector', +} + +export type SimpleSubscription = { + id: string + name: string +} diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index 818c2a0388a..4aa0326cb4a 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -131,7 +131,7 @@ export type ParametersSchema = { scope: any required: boolean multiple: boolean - default?: string[] + default?: string | string[] min: any max: any precision: any diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 6ed4d7f2d50..07efb0d02f1 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -39,9 +39,9 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & { title: string plugin_unique_identifier: string is_team_authorization: boolean - params: Record - paramSchemas: Record[] - output_schema: Record + params: Record + paramSchemas: Record[] + output_schema: Record subscription_id?: string meta?: PluginMeta } @@ -52,9 +52,9 @@ export type ToolDefaultValue = PluginCommonDefaultValue & { tool_description: string title: string is_team_authorization: boolean - params: Record - paramSchemas: Record[] - output_schema?: Record + params: Record + paramSchemas: Record[] + output_schema?: Record credential_id?: string meta?: PluginMeta plugin_id?: string @@ -82,10 +82,10 @@ export type ToolValue = { tool_name: string tool_label: string tool_description?: string - settings?: Record - parameters?: Record + settings?: Record + parameters?: Record enabled?: boolean - extra?: Record + extra?: { description?: string } & Record credential_id?: string } @@ -94,7 +94,7 @@ export type DataSourceItem = { plugin_unique_identifier: string provider: string declaration: { - credentials_schema: any[] + credentials_schema: unknown[] provider_type: string identity: { author: string @@ -113,10 +113,10 @@ export type DataSourceItem = { name: string provider: string } - parameters: any[] + parameters: unknown[] output_schema?: { type: string - properties: Record + properties: Record } }[] } @@ -133,15 +133,15 @@ export type TriggerParameter = { | 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select' auto_generate?: { type: string - value?: any + value?: unknown } | null template?: { type: string - value?: any + value?: unknown } | null scope?: string | null required?: boolean - default?: any + default?: unknown min?: number | null max?: number | null precision?: number | null @@ -158,7 +158,7 @@ export type TriggerCredentialField = { name: string scope?: string | null required: boolean - default?: string | number | boolean | Array | null + default?: string | number | boolean | Array | null options?: Array<{ value: string label: TypeWithI18N @@ -191,7 +191,7 @@ export type TriggerApiEntity = { identity: TriggerIdentity description: TypeWithI18N parameters: TriggerParameter[] - output_schema?: Record + output_schema?: Record } export type TriggerProviderApiEntity = { @@ -237,32 +237,15 @@ type TriggerSubscriptionStructure = { name: string provider: string credential_type: TriggerCredentialTypeEnum - credentials: TriggerSubCredentials + credentials: Record endpoint: string - parameters: TriggerSubParameters - properties: TriggerSubProperties + parameters: Record + properties: Record workflows_in_use: number } export type TriggerSubscription = TriggerSubscriptionStructure -export type TriggerSubCredentials = { - access_tokens: string -} - -export type TriggerSubParameters = { - repository: string - webhook_secret?: string -} - -export type TriggerSubProperties = { - active: boolean - events: string[] - external_id: string - repository: string - webhook_secret?: string -} - export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure // OAuth configuration types @@ -275,7 +258,7 @@ export type TriggerOAuthConfig = { params: { client_id: string client_secret: string - [key: string]: any + [key: string]: string } system_configured: boolean } diff --git a/web/app/components/workflow/nodes/trigger-plugin/hooks/use-trigger-auth-flow.ts b/web/app/components/workflow/nodes/trigger-plugin/hooks/use-trigger-auth-flow.ts index 36bcbf1cc7f..f551f2f420c 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/hooks/use-trigger-auth-flow.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/hooks/use-trigger-auth-flow.ts @@ -4,11 +4,11 @@ import { useBuildTriggerSubscription, useCreateTriggerSubscriptionBuilder, useUpdateTriggerSubscriptionBuilder, - useVerifyTriggerSubscriptionBuilder, + useVerifyAndUpdateTriggerSubscriptionBuilder, } from '@/service/use-triggers' // Helper function to serialize complex values to strings for backend encryption -const serializeFormValues = (values: Record): Record => { +const serializeFormValues = (values: Record): Record => { const result: Record = {} for (const [key, value] of Object.entries(values)) { @@ -23,6 +23,17 @@ const serializeFormValues = (values: Record): Record { + if (error instanceof Error && error.message) + return error.message + if (typeof error === 'object' && error && 'message' in error) { + const message = (error as { message?: string }).message + if (typeof message === 'string' && message) + return message + } + return fallback +} + export type AuthFlowStep = 'auth' | 'params' | 'complete' export type AuthFlowState = { @@ -34,8 +45,8 @@ export type AuthFlowState = { export type AuthFlowActions = { startAuth: () => Promise - verifyAuth: (credentials: Record) => Promise - completeConfig: (parameters: Record, properties?: Record, name?: string) => Promise + verifyAuth: (credentials: Record) => Promise + completeConfig: (parameters: Record, properties?: Record, name?: string) => Promise reset: () => void } @@ -47,7 +58,7 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState const createBuilder = useCreateTriggerSubscriptionBuilder() const updateBuilder = useUpdateTriggerSubscriptionBuilder() - const verifyBuilder = useVerifyTriggerSubscriptionBuilder() + const verifyBuilder = useVerifyAndUpdateTriggerSubscriptionBuilder() const buildSubscription = useBuildTriggerSubscription() const startAuth = useCallback(async () => { @@ -64,8 +75,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState setBuilderId(response.subscription_builder.id) setStep('auth') } - catch (err: any) { - setError(err.message || 'Failed to start authentication flow') + catch (err: unknown) { + setError(getErrorMessage(err, 'Failed to start authentication flow')) throw err } finally { @@ -73,7 +84,7 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState } }, [provider.name, createBuilder, builderId]) - const verifyAuth = useCallback(async (credentials: Record) => { + const verifyAuth = useCallback(async (credentials: Record) => { if (!builderId) { setError('No builder ID available') return @@ -96,8 +107,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState setStep('params') } - catch (err: any) { - setError(err.message || 'Authentication verification failed') + catch (err: unknown) { + setError(getErrorMessage(err, 'Authentication verification failed')) throw err } finally { @@ -106,8 +117,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState }, [provider.name, builderId, updateBuilder, verifyBuilder]) const completeConfig = useCallback(async ( - parameters: Record, - properties: Record = {}, + parameters: Record, + properties: Record = {}, name?: string, ) => { if (!builderId) { @@ -134,8 +145,8 @@ export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState setStep('complete') } - catch (err: any) { - setError(err.message || 'Configuration failed') + catch (err: unknown) { + setError(getErrorMessage(err, 'Configuration failed')) throw err } finally { diff --git a/web/i18n/en-US/common.ts b/web/i18n/en-US/common.ts index 2e378afeda8..0117f2ae003 100644 --- a/web/i18n/en-US/common.ts +++ b/web/i18n/en-US/common.ts @@ -21,6 +21,7 @@ const translation = { cancel: 'Cancel', clear: 'Clear', save: 'Save', + saving: 'Saving...', yes: 'Yes', no: 'No', deleteConfirmTitle: 'Delete?', diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts index f1d697e5076..16ee1e13628 100644 --- a/web/i18n/en-US/plugin-trigger.ts +++ b/web/i18n/en-US/plugin-trigger.ts @@ -30,6 +30,11 @@ const translation = { unauthorized: 'Manual', }, actions: { + edit: { + title: 'Edit Subscription', + success: 'Subscription updated successfully', + error: 'Failed to update subscription', + }, delete: 'Delete', deleteConfirm: { title: 'Delete {{name}}?', diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index c21d1aa9795..6036f5ab343 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -1,3 +1,4 @@ +import type { FormOption } from '@/app/components/base/form/types' import type { TriggerLogEntity, TriggerOAuthClientParams, @@ -149,9 +150,9 @@ export const useUpdateTriggerSubscriptionBuilder = () => { provider: string subscriptionBuilderId: string name?: string - properties?: Record - parameters?: Record - credentials?: Record + properties?: Record + parameters?: Record + credentials?: Record }) => { const { provider, subscriptionBuilderId, ...body } = payload return post( @@ -162,17 +163,35 @@ export const useUpdateTriggerSubscriptionBuilder = () => { }) } -export const useVerifyTriggerSubscriptionBuilder = () => { +export const useVerifyAndUpdateTriggerSubscriptionBuilder = () => { return useMutation({ - mutationKey: [NAME_SPACE, 'verify-subscription-builder'], + mutationKey: [NAME_SPACE, 'verify-and-update-subscription-builder'], mutationFn: (payload: { provider: string subscriptionBuilderId: string - credentials?: Record + credentials?: Record }) => { const { provider, subscriptionBuilderId, ...body } = payload return post<{ verified: boolean }>( - `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`, + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify-and-update/${subscriptionBuilderId}`, + { body }, + { silent: true }, + ) + }, + }) +} + +export const useVerifyTriggerSubscription = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'verify-subscription'], + mutationFn: (payload: { + provider: string + subscriptionId: string + credentials?: Record + }) => { + const { provider, subscriptionId, ...body } = payload + return post<{ verified: boolean }>( + `/workspaces/current/trigger-provider/${provider}/subscriptions/verify/${subscriptionId}`, { body }, { silent: true }, ) @@ -184,7 +203,7 @@ export type BuildTriggerSubscriptionPayload = { provider: string subscriptionBuilderId: string name?: string - parameters?: Record + parameters?: Record } export const useBuildTriggerSubscription = () => { @@ -211,6 +230,27 @@ export const useDeleteTriggerSubscription = () => { }) } +export type UpdateTriggerSubscriptionPayload = { + subscriptionId: string + name?: string + properties?: Record + parameters?: Record + credentials?: Record +} + +export const useUpdateTriggerSubscription = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-subscription'], + mutationFn: (payload: UpdateTriggerSubscriptionPayload) => { + const { subscriptionId, ...body } = payload + return post<{ result: string, id: string }>( + `/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/update`, + { body }, + ) + }, + }) +} + export const useTriggerSubscriptionBuilderLogs = ( provider: string, subscriptionBuilderId: string, @@ -290,20 +330,45 @@ export const useTriggerPluginDynamicOptions = (payload: { action: string parameter: string credential_id: string - extra?: Record + credentials?: Record + extra?: Record }, enabled = true) => { - return useQuery<{ options: Array<{ value: string, label: any }> }>({ - queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.extra], - queryFn: () => get<{ options: Array<{ value: string, label: any }> }>( - '/workspaces/current/plugin/parameters/dynamic-options', - { - params: { - ...payload, - provider_type: 'trigger', // Add required provider_type parameter + return useQuery<{ options: FormOption[] }>({ + queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.credential_id, payload.credentials, payload.extra], + queryFn: () => { + // Use new endpoint with POST when credentials provided (for edit mode) + if (payload.credentials) { + return post<{ options: FormOption[] }>( + '/workspaces/current/plugin/parameters/dynamic-options-with-credentials', + { + body: { + plugin_id: payload.plugin_id, + provider: payload.provider, + action: payload.action, + parameter: payload.parameter, + credential_id: payload.credential_id, + credentials: payload.credentials, + }, + }, + { silent: true }, + ) + } + // Use original GET endpoint for normal cases + return get<{ options: FormOption[] }>( + '/workspaces/current/plugin/parameters/dynamic-options', + { + params: { + plugin_id: payload.plugin_id, + provider: payload.provider, + action: payload.action, + parameter: payload.parameter, + credential_id: payload.credential_id, + provider_type: 'trigger', + }, }, - }, - { silent: true }, - ), + { silent: true }, + ) + }, enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id, retry: 0, }) From fdaeec7f7d4bbe76fea73be7bb71bb1d6bddf8de Mon Sep 17 00:00:00 2001 From: Maries Date: Wed, 24 Dec 2025 20:23:52 +0800 Subject: [PATCH 11/17] fix: trigger subscription delete not working for non-auto-created credentials (#30122) Co-authored-by: Claude Opus 4.5 --- .../trigger/trigger_provider_service.py | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/api/services/trigger/trigger_provider_service.py b/api/services/trigger/trigger_provider_service.py index 71f35dada6b..57de9b3cee8 100644 --- a/api/services/trigger/trigger_provider_service.py +++ b/api/services/trigger/trigger_provider_service.py @@ -359,10 +359,6 @@ class TriggerProviderService: raise ValueError(f"Trigger provider subscription {subscription_id} not found") credential_type: CredentialType = CredentialType.of(subscription.credential_type) - is_auto_created: bool = credential_type in [CredentialType.OAUTH2, CredentialType.API_KEY] - if not is_auto_created: - return None - provider_id = TriggerProviderID(subscription.provider_id) provider_controller: PluginTriggerProviderController = TriggerManager.get_trigger_provider( tenant_id=tenant_id, provider_id=provider_id @@ -372,17 +368,20 @@ class TriggerProviderService: controller=provider_controller, subscription=subscription, ) - try: - TriggerManager.unsubscribe_trigger( - tenant_id=tenant_id, - user_id=subscription.user_id, - provider_id=provider_id, - subscription=subscription.to_entity(), - credentials=encrypter.decrypt(subscription.credentials), - credential_type=credential_type, - ) - except Exception as e: - logger.exception("Error unsubscribing trigger", exc_info=e) + + is_auto_created: bool = credential_type in [CredentialType.OAUTH2, CredentialType.API_KEY] + if is_auto_created: + try: + TriggerManager.unsubscribe_trigger( + tenant_id=tenant_id, + user_id=subscription.user_id, + provider_id=provider_id, + subscription=subscription.to_entity(), + credentials=encrypter.decrypt(subscription.credentials), + credential_type=credential_type, + ) + except Exception as e: + logger.exception("Error unsubscribing trigger", exc_info=e) session.delete(subscription) # Clear cache From 3cbbb06dc418786e78b260bce72c3b0d370ba8cf Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 25 Dec 2025 09:44:57 +0800 Subject: [PATCH 12/17] chore(web): migrate lodash-es to es-toolkit compat (#30126) --- web/__mocks__/provider-context.ts | 2 +- .../time-range-picker/date-picker.tsx | 2 +- .../webapp-reset-password/page.tsx | 2 +- .../components/mail-and-code-auth.tsx | 2 +- .../components/mail-and-password-auth.tsx | 2 +- .../account-page/email-change-modal.tsx | 2 +- .../batch-add-annotation-modal/index.tsx | 2 +- .../base/operation-btn/index.tsx | 2 +- .../config-prompt/simple-prompt-input.tsx | 2 +- .../config/agent/prompt-editor.tsx | 2 +- .../dataset-config/index.spec.tsx | 2 +- .../configuration/dataset-config/index.tsx | 2 +- .../params-config/config-content.tsx | 13 ++++++---- .../params-config/weighted-score.tsx | 2 +- .../dataset-config/settings-modal/index.tsx | 2 +- .../debug-with-multiple-model/context.tsx | 2 +- .../text-generation-item.tsx | 2 +- .../app/configuration/debug/hooks.tsx | 2 +- .../app/configuration/debug/index.tsx | 3 +-- .../hooks/use-advanced-prompt-config.ts | 2 +- .../components/app/configuration/index.tsx | 2 +- .../tools/external-data-tool-modal.tsx | 2 +- .../app/create-from-dsl-modal/index.tsx | 2 +- .../components/app/duplicate-modal/index.tsx | 2 +- web/app/components/app/log/index.tsx | 2 +- web/app/components/app/log/list.tsx | 2 +- .../apikey-info-panel.test-utils.tsx | 2 +- web/app/components/app/overview/app-chart.tsx | 2 +- .../components/app/switch-app-modal/index.tsx | 2 +- web/app/components/app/workflow-log/index.tsx | 2 +- .../base/agent-log-modal/detail.tsx | 2 +- .../components/base/app-icon-picker/index.tsx | 2 +- .../base/chat/__tests__/utils.spec.ts | 2 +- .../base/chat/chat-with-history/context.tsx | 2 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- web/app/components/base/chat/chat/hooks.ts | 2 +- web/app/components/base/chat/chat/index.tsx | 2 +- .../base/chat/embedded-chatbot/context.tsx | 2 +- .../base/chat/embedded-chatbot/hooks.tsx | 2 +- .../components/base/copy-feedback/index.tsx | 2 +- web/app/components/base/copy-icon/index.tsx | 2 +- .../components/base/emoji-picker/index.tsx | 2 +- .../conversation-opener/modal.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- .../components/base/file-uploader/hooks.ts | 2 +- .../base/file-uploader/pdf-preview.tsx | 2 +- .../components/base/file-uploader/store.tsx | 2 +- .../base/fullscreen-modal/index.tsx | 2 +- web/app/components/base/icons/script.mjs | 2 +- .../base/image-uploader/image-preview.tsx | 2 +- .../base/input-with-copy/index.spec.tsx | 4 +-- .../components/base/input-with-copy/index.tsx | 2 +- web/app/components/base/input/index.tsx | 2 +- web/app/components/base/markdown/index.tsx | 2 +- .../base/markdown/markdown-utils.ts | 2 +- web/app/components/base/modal/index.tsx | 2 +- web/app/components/base/modal/modal.tsx | 2 +- .../components/base/pagination/pagination.tsx | 2 +- .../context-block-replacement-block.tsx | 2 +- .../plugins/context-block/index.tsx | 2 +- .../history-block-replacement-block.tsx | 2 +- .../plugins/history-block/index.tsx | 2 +- web/app/components/base/radio-card/index.tsx | 2 +- .../components/base/tag-management/panel.tsx | 2 +- .../base/tag-management/tag-remove-modal.tsx | 2 +- web/app/components/base/toast/index.spec.tsx | 2 +- web/app/components/base/toast/index.tsx | 2 +- .../base/with-input-validation/index.spec.tsx | 2 +- .../index-failed.tsx | 2 +- .../create-from-dsl-modal/index.tsx | 2 +- .../datasets/create/step-two/index.tsx | 2 +- .../documents/detail/batch-modal/index.tsx | 2 +- .../completed/common/full-screen-drawer.tsx | 2 +- .../completed/common/regeneration-modal.tsx | 2 +- .../documents/detail/completed/index.tsx | 2 +- .../documents/detail/metadata/index.tsx | 2 +- .../settings/pipeline-settings/index.tsx | 2 +- .../components/datasets/documents/list.tsx | 2 +- .../datasets/documents/operations.tsx | 2 +- .../metadata/hooks/use-metadata-document.ts | 2 +- .../metadata-dataset/create-content.tsx | 2 +- .../datasets/rename-modal/index.tsx | 2 +- .../explore/create-app-modal/index.tsx | 2 +- .../api-based-extension-page/modal.tsx | 2 +- .../data-source-notion/index.tsx | 2 +- .../data-source-page/panel/config-item.tsx | 2 +- .../account-setting/key-validator/hooks.ts | 2 +- .../edit-workspace-modal/index.tsx | 2 +- .../members-page/invite-modal/index.tsx | 2 +- .../members-page/invited-modal/index.tsx | 2 +- .../transfer-ownership-modal/index.tsx | 2 +- .../header/account-setting/menu-dialog.tsx | 2 +- web/app/components/header/app-nav/index.tsx | 2 +- .../components/header/app-selector/index.tsx | 2 +- .../components/header/dataset-nav/index.tsx | 2 +- .../header/nav/nav-selector/index.tsx | 2 +- .../plugins/base/deprecation-notice.tsx | 2 +- .../plugins/install-plugin/utils.ts | 2 +- .../plugins/marketplace/context.tsx | 2 +- .../subscription-list/create/common-modal.tsx | 2 +- .../edit/apikey-edit-modal.tsx | 2 +- .../edit/manual-edit-modal.tsx | 2 +- .../edit/oauth-edit-modal.tsx | 2 +- .../plugins/plugin-page/context.tsx | 2 +- .../plugins/plugin-page/empty/index.tsx | 2 +- .../components/plugins/plugin-page/index.tsx | 2 +- .../plugin-page/install-plugin-dropdown.tsx | 2 +- .../field-list/field-list-container.tsx | 2 +- .../publish-as-knowledge-pipeline-modal.tsx | 2 +- .../rag-pipeline/hooks/use-pipeline.tsx | 2 +- web/app/components/tools/labels/selector.tsx | 2 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../setting/build-in/config-credentials.tsx | 2 +- .../workflow-tool/confirm-modal/index.tsx | 2 +- .../workflow-app/hooks/use-workflow-run.ts | 2 +- .../workflow/block-selector/blocks.tsx | 2 +- .../market-place-plugin/list.tsx | 2 +- web/app/components/workflow/custom-edge.tsx | 2 +- .../workflow/dsl-export-confirm-modal.tsx | 2 +- .../components/workflow/hooks-store/store.ts | 2 +- .../workflow/hooks/use-nodes-layout.ts | 2 +- .../workflow/hooks/use-workflow-history.ts | 2 +- .../components/workflow/hooks/use-workflow.ts | 2 +- web/app/components/workflow/index.tsx | 2 +- .../nodes/_base/components/agent-strategy.tsx | 2 +- .../components/editor/code-editor/index.tsx | 2 +- .../nodes/_base/components/file-type-item.tsx | 2 +- .../components/input-support-select-var.tsx | 2 +- .../_base/components/next-step/index.tsx | 2 +- .../_base/components/next-step/operator.tsx | 2 +- .../panel-operator/change-block.tsx | 2 +- .../nodes/_base/components/variable/utils.ts | 2 +- .../variable/var-reference-picker.tsx | 2 +- .../variable/var-reference-vars.tsx | 2 +- .../variable-label/base/variable-label.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 2 +- .../nodes/_base/hooks/use-one-step-run.ts | 4 +-- .../assigner/components/var-list/index.tsx | 2 +- .../workflow/nodes/assigner/hooks.ts | 2 +- .../nodes/http/components/edit-body/index.tsx | 2 +- .../nodes/http/hooks/use-key-value-list.ts | 2 +- .../components/condition-number-input.tsx | 2 +- .../if-else/components/condition-wrap.tsx | 2 +- .../workflow/nodes/iteration/use-config.ts | 2 +- .../condition-list/condition-value-method.tsx | 2 +- .../metadata/metadata-filter/index.tsx | 2 +- .../components/retrieval-config.tsx | 5 ++-- .../nodes/knowledge-retrieval/panel.tsx | 4 +-- .../nodes/knowledge-retrieval/use-config.ts | 2 +- .../nodes/knowledge-retrieval/utils.ts | 2 +- .../visual-editor/context.tsx | 2 +- .../visual-editor/hooks.ts | 2 +- .../nodes/llm/use-single-run-form-params.ts | 2 +- .../components/condition-number-input.tsx | 2 +- .../use-single-run-form-params.ts | 2 +- .../components/class-item.tsx | 2 +- .../components/class-list.tsx | 2 +- .../use-single-run-form-params.ts | 2 +- .../nodes/start/components/var-item.tsx | 2 +- .../nodes/tool/components/copy-id.tsx | 2 +- .../nodes/tool/components/input-var-list.tsx | 2 +- .../components/var-list/index.tsx | 2 +- .../workflow/nodes/variable-assigner/hooks.ts | 4 +-- .../plugins/link-editor-plugin/component.tsx | 2 +- .../plugins/link-editor-plugin/hooks.ts | 2 +- .../components/variable-item.tsx | 2 +- .../conversation-variable-modal.tsx | 2 +- .../workflow/panel/debug-and-preview/hooks.ts | 2 +- .../panel/debug-and-preview/index.tsx | 2 +- .../workflow/panel/env-panel/env-item.tsx | 2 +- .../panel/global-variable-panel/item.tsx | 2 +- .../run/utils/format-log/agent/index.ts | 2 +- .../workflow/run/utils/format-log/index.ts | 2 +- .../utils/format-log/iteration/index.spec.ts | 2 +- .../run/utils/format-log/loop/index.spec.ts | 2 +- .../store/workflow/workflow-draft-slice.ts | 2 +- .../components/workflow/utils/elk-layout.ts | 2 +- .../workflow/utils/workflow-init.ts | 2 +- web/app/components/workflow/utils/workflow.ts | 2 +- .../workflow/variable-inspect/index.tsx | 2 +- .../workflow/workflow-history-store.tsx | 2 +- .../education-apply/education-apply-page.tsx | 2 +- web/app/reset-password/page.tsx | 2 +- .../components/mail-and-password-auth.tsx | 2 +- web/app/signin/invite-settings/page.tsx | 2 +- web/app/signup/components/input-mail.tsx | 2 +- web/context/app-context.tsx | 2 +- web/context/datasets-context.tsx | 2 +- web/context/debug-configuration.ts | 2 +- web/context/explore-context.ts | 2 +- web/context/i18n.ts | 2 +- web/context/mitt-context.tsx | 2 +- web/context/modal-context.tsx | 2 +- web/context/provider-context.tsx | 2 +- web/i18n-config/i18next-config.ts | 2 +- web/i18n-config/server.ts | 2 +- web/package.json | 3 +-- web/pnpm-lock.yaml | 26 ++++++------------- web/service/knowledge/use-create-dataset.ts | 2 +- web/service/use-flow.ts | 2 +- web/service/use-plugins.ts | 2 +- web/utils/index.ts | 2 +- 202 files changed, 222 insertions(+), 230 deletions(-) diff --git a/web/__mocks__/provider-context.ts b/web/__mocks__/provider-context.ts index 3ecb4e9c0e0..c69a2ad1d23 100644 --- a/web/__mocks__/provider-context.ts +++ b/web/__mocks__/provider-context.ts @@ -1,6 +1,6 @@ import type { Plan, UsagePlanInfo } from '@/app/components/billing/type' import type { ProviderContextState } from '@/context/provider-context' -import { merge, noop } from 'lodash-es' +import { merge, noop } from 'es-toolkit/compat' import { defaultPlan } from '@/app/components/billing/config' // Avoid being mocked in tests diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index ab39846a363..004f83afc54 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -4,7 +4,7 @@ import type { FC } from 'react' import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types' import { RiCalendarLine } from '@remixicon/react' import dayjs from 'dayjs' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback } from 'react' import Picker from '@/app/components/base/date-and-time-picker/date-picker' diff --git a/web/app/(shareLayout)/webapp-reset-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/page.tsx index 92c39eb7292..108bd4b22e2 100644 --- a/web/app/(shareLayout)/webapp-reset-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/page.tsx @@ -1,6 +1,6 @@ 'use client' import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx index 90208583473..8b611b9eead 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-code-auth.tsx @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx index 67e4bf7af2f..46645ed68cc 100644 --- a/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx +++ b/web/app/(shareLayout)/webapp-signin/components/mail-and-password-auth.tsx @@ -1,5 +1,5 @@ 'use client' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useState } from 'react' diff --git a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx index 99b4f5c6860..655452ea245 100644 --- a/web/app/account/(commonLayout)/account-page/email-change-modal.tsx +++ b/web/app/account/(commonLayout)/account-page/email-change-modal.tsx @@ -1,6 +1,6 @@ import type { ResponseError } from '@/service/fetch' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import * as React from 'react' import { useState } from 'react' diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx index 7fbb745c489..7289ed384d9 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/base/operation-btn/index.tsx b/web/app/components/app/configuration/base/operation-btn/index.tsx index b9f55de26b8..6a22dc6d3b8 100644 --- a/web/app/components/app/configuration/base/operation-btn/index.tsx +++ b/web/app/components/app/configuration/base/operation-btn/index.tsx @@ -4,7 +4,7 @@ import { RiAddLine, RiEditLine, } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 9b558b58c11..3f39828e79b 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -4,8 +4,8 @@ import type { ExternalDataTool } from '@/models/common' import type { PromptVariable } from '@/models/debug' import type { GenRes } from '@/service/debug' import { useBoolean } from 'ahooks' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/config/agent/prompt-editor.tsx b/web/app/components/app/configuration/config/agent/prompt-editor.tsx index 0a09609ccae..7399aaeac99 100644 --- a/web/app/components/app/configuration/config/agent/prompt-editor.tsx +++ b/web/app/components/app/configuration/config/agent/prompt-editor.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { ExternalDataTool } from '@/models/common' import copy from 'copy-to-clipboard' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/app/configuration/dataset-config/index.spec.tsx b/web/app/components/app/configuration/dataset-config/index.spec.tsx index acccdec98ea..e3791db9c0c 100644 --- a/web/app/components/app/configuration/dataset-config/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/index.spec.tsx @@ -52,7 +52,7 @@ vi.mock('../debug/hooks', () => ({ useFormattingChangedDispatcher: vi.fn(() => vi.fn()), })) -vi.mock('lodash-es', () => ({ +vi.mock('es-toolkit/compat', () => ({ intersectionBy: vi.fn((...arrays) => { // Mock realistic intersection behavior based on metadata name const validArrays = arrays.filter(Array.isArray) diff --git a/web/app/components/app/configuration/dataset-config/index.tsx b/web/app/components/app/configuration/dataset-config/index.tsx index 9ac17295909..2fc82c82b66 100644 --- a/web/app/components/app/configuration/dataset-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/index.tsx @@ -8,8 +8,8 @@ import type { MetadataFilteringModeEnum, } from '@/app/components/workflow/nodes/knowledge-retrieval/types' import type { DataSet } from '@/models/datasets' +import { intersectionBy } from 'es-toolkit/compat' import { produce } from 'immer' -import { intersectionBy } from 'lodash-es' import * as React from 'react' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx index ad5199fd55f..f7aadc0c3fb 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx @@ -1,6 +1,7 @@ 'use client' import type { FC } from 'react' +import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import type { ModelConfig } from '@/app/components/workflow/types' import type { DataSet, @@ -8,7 +9,6 @@ import type { import type { DatasetConfigs, } from '@/models/debug' -import { noop } from 'lodash-es' import { memo, useCallback, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' @@ -33,17 +33,20 @@ type Props = { selectedDatasets?: DataSet[] isInWorkflow?: boolean singleRetrievalModelConfig?: ModelConfig - onSingleRetrievalModelChange?: (config: ModelConfig) => void - onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void + onSingleRetrievalModelChange?: ModelParameterModalProps['setModel'] + onSingleRetrievalModelParamsChange?: ModelParameterModalProps['onCompletionParamsChange'] } +const noopModelChange: ModelParameterModalProps['setModel'] = () => {} +const noopParamsChange: ModelParameterModalProps['onCompletionParamsChange'] = () => {} + const ConfigContent: FC = ({ datasetConfigs, onChange, isInWorkflow, singleRetrievalModelConfig: singleRetrievalConfig = {} as ModelConfig, - onSingleRetrievalModelChange = noop, - onSingleRetrievalModelParamsChange = noop, + onSingleRetrievalModelChange = noopModelChange, + onSingleRetrievalModelParamsChange = noopParamsChange, selectedDatasets = [], }) => { const { t } = useTranslation() diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx index 77f3ac0eb8b..3a62a665257 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { memo } from 'react' import { useTranslation } from 'react-i18next' import Slider from '@/app/components/base/slider' diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx index 243aafdbddc..0d8a0934e5e 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/index.tsx @@ -3,7 +3,7 @@ import type { Member } from '@/models/common' import type { DataSet } from '@/models/datasets' import type { RetrievalConfig } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx index 9d058186cf7..3f0381074f5 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/context.tsx @@ -1,7 +1,7 @@ 'use client' import type { ModelAndParameter } from '../types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext } from 'use-context-selector' export type DebugWithMultipleModelContextType = { diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx index 8dda5b38791..9113f782d92 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx @@ -4,7 +4,7 @@ import type { OnSend, TextGenerationConfig, } from '@/app/components/base/text-generation/types' -import { cloneDeep, noop } from 'lodash-es' +import { cloneDeep, noop } from 'es-toolkit/compat' import { memo } from 'react' import TextGeneration from '@/app/components/app/text-generate/item' import { TransferMethod } from '@/app/components/base/chat/types' diff --git a/web/app/components/app/configuration/debug/hooks.tsx b/web/app/components/app/configuration/debug/hooks.tsx index 786d43bdb9e..e66185e2841 100644 --- a/web/app/components/app/configuration/debug/hooks.tsx +++ b/web/app/components/app/configuration/debug/hooks.tsx @@ -6,7 +6,7 @@ import type { ChatConfig, ChatItem, } from '@/app/components/base/chat/types' -import cloneDeep from 'lodash-es/cloneDeep' +import { cloneDeep } from 'es-toolkit/compat' import { useCallback, useRef, diff --git a/web/app/components/app/configuration/debug/index.tsx b/web/app/components/app/configuration/debug/index.tsx index fe1c6550f57..140f9421c92 100644 --- a/web/app/components/app/configuration/debug/index.tsx +++ b/web/app/components/app/configuration/debug/index.tsx @@ -11,9 +11,8 @@ import { RiSparklingFill, } from '@remixicon/react' import { useBoolean } from 'ahooks' +import { cloneDeep, noop } from 'es-toolkit/compat' import { produce, setAutoFreeze } from 'immer' -import { noop } from 'lodash-es' -import cloneDeep from 'lodash-es/cloneDeep' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts index 63603033d3d..3e8f7c5b3a7 100644 --- a/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts +++ b/web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts @@ -1,7 +1,7 @@ import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug' +import { clone } from 'es-toolkit/compat' import { produce } from 'immer' -import { clone } from 'lodash-es' import { useState } from 'react' import { checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock, PRE_PROMPT_PLACEHOLDER_TEXT } from '@/app/components/base/prompt-editor/constants' import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' diff --git a/web/app/components/app/configuration/index.tsx b/web/app/components/app/configuration/index.tsx index eb7a9f5a321..4b5bcafc9be 100644 --- a/web/app/components/app/configuration/index.tsx +++ b/web/app/components/app/configuration/index.tsx @@ -20,8 +20,8 @@ import type { import type { ModelConfig as BackendModelConfig, UserInputFormItem, VisionSettings } from '@/types/app' import { CodeBracketIcon } from '@heroicons/react/20/solid' import { useBoolean, useGetState } from 'ahooks' +import { clone, isEqual } from 'es-toolkit/compat' import { produce } from 'immer' -import { clone, isEqual } from 'lodash-es' import { usePathname } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx index 74f436289fe..ccd0b1288e5 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal.tsx +++ b/web/app/components/app/configuration/tools/external-data-tool-modal.tsx @@ -3,7 +3,7 @@ import type { CodeBasedExtensionItem, ExternalDataTool, } from '@/models/common' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/app/create-from-dsl-modal/index.tsx b/web/app/components/app/create-from-dsl-modal/index.tsx index 3d6cabedf65..809859d3ad1 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -3,7 +3,7 @@ import type { MouseEventHandler } from 'react' import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/duplicate-modal/index.tsx b/web/app/components/app/duplicate-modal/index.tsx index 420a6b159ae..f670b370765 100644 --- a/web/app/components/app/duplicate-modal/index.tsx +++ b/web/app/components/app/duplicate-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { AppIconType } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index 0e6e4bba2d6..52485f5dd2b 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import type { App } from '@/types/app' import { useDebounce } from 'ahooks' import dayjs from 'dayjs' -import { omit } from 'lodash-es' +import { omit } from 'es-toolkit/compat' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 2b13a09b3a9..cf10cff3271 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -12,7 +12,7 @@ import { RiCloseLine, RiEditFill } from '@remixicon/react' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { get, noop } from 'lodash-es' +import { get, noop } from 'es-toolkit/compat' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' diff --git a/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx b/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx index 0e8f82b00d3..5cffa1143bf 100644 --- a/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx +++ b/web/app/components/app/overview/apikey-info-panel/apikey-info-panel.test-utils.tsx @@ -2,7 +2,7 @@ import type { RenderOptions } from '@testing-library/react' import type { Mock, MockedFunction } from 'vitest' import type { ModalContextState } from '@/context/modal-context' import { fireEvent, render } from '@testing-library/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { defaultPlan } from '@/app/components/billing/config' import { useModalContext as actualUseModalContext } from '@/context/modal-context' diff --git a/web/app/components/app/overview/app-chart.tsx b/web/app/components/app/overview/app-chart.tsx index d876dbda27f..114ef7d5dbd 100644 --- a/web/app/components/app/overview/app-chart.tsx +++ b/web/app/components/app/overview/app-chart.tsx @@ -6,7 +6,7 @@ import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyM import dayjs from 'dayjs' import Decimal from 'decimal.js' import ReactECharts from 'echarts-for-react' -import { get } from 'lodash-es' +import { get } from 'es-toolkit/compat' import * as React from 'react' import { useTranslation } from 'react-i18next' import Basic from '@/app/components/app-sidebar/basic' diff --git a/web/app/components/app/switch-app-modal/index.tsx b/web/app/components/app/switch-app-modal/index.tsx index 257f7b67886..83f2efc49dc 100644 --- a/web/app/components/app/switch-app-modal/index.tsx +++ b/web/app/components/app/switch-app-modal/index.tsx @@ -2,7 +2,7 @@ import type { App } from '@/types/app' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 12438d6d17c..5aa467d03de 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -5,7 +5,7 @@ import { useDebounce } from 'ahooks' import dayjs from 'dayjs' import timezone from 'dayjs/plugin/timezone' import utc from 'dayjs/plugin/utc' -import { omit } from 'lodash-es' +import { omit } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/agent-log-modal/detail.tsx b/web/app/components/base/agent-log-modal/detail.tsx index 5451126c9ea..0b8dc302fb5 100644 --- a/web/app/components/base/agent-log-modal/detail.tsx +++ b/web/app/components/base/agent-log-modal/detail.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { IChatItem } from '@/app/components/base/chat/chat/type' import type { AgentIteration, AgentLogDetailResponse } from '@/models/log' -import { flatten, uniq } from 'lodash-es' +import { flatten, uniq } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/app-icon-picker/index.tsx b/web/app/components/base/app-icon-picker/index.tsx index ce73af36a2b..99ba7eb544e 100644 --- a/web/app/components/base/app-icon-picker/index.tsx +++ b/web/app/components/base/app-icon-picker/index.tsx @@ -3,7 +3,7 @@ import type { Area } from 'react-easy-crop' import type { OnImageInput } from './ImageInput' import type { AppIconType, ImageFile } from '@/types/app' import { RiImageCircleAiLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config' diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts index 2e86e13733b..d3e77732a5b 100644 --- a/web/app/components/base/chat/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/__tests__/utils.spec.ts @@ -1,5 +1,5 @@ import type { ChatItemInTree } from '../types' -import { get } from 'lodash-es' +import { get } from 'es-toolkit/compat' import { buildChatItemTree, getThreadMessages } from '../utils' import branchedTestMessages from './branchedTestMessages.json' import legacyTestMessages from './legacyTestMessages.json' diff --git a/web/app/components/base/chat/chat-with-history/context.tsx b/web/app/components/base/chat/chat-with-history/context.tsx index 120b6ed4bd4..d1496f82780 100644 --- a/web/app/components/base/chat/chat-with-history/context.tsx +++ b/web/app/components/base/chat/chat-with-history/context.tsx @@ -14,7 +14,7 @@ import type { AppMeta, ConversationItem, } from '@/models/share' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext } from 'use-context-selector' export type ChatWithHistoryContextValue = { diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index 67c46b2d108..3acc4805189 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -10,8 +10,8 @@ import type { ConversationItem, } from '@/models/share' import { useLocalStorageState } from 'ahooks' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/base/chat/chat/hooks.ts b/web/app/components/base/chat/chat/hooks.ts index 739fe644fe5..64ba5f0aeca 100644 --- a/web/app/components/base/chat/chat/hooks.ts +++ b/web/app/components/base/chat/chat/hooks.ts @@ -8,8 +8,8 @@ import type { InputForm } from './type' import type AudioPlayer from '@/app/components/base/audio-btn/audio' import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { Annotation } from '@/models/log' +import { noop, uniqBy } from 'es-toolkit/compat' import { produce, setAutoFreeze } from 'immer' -import { noop, uniqBy } from 'lodash-es' import { useParams, usePathname } from 'next/navigation' import { useCallback, diff --git a/web/app/components/base/chat/chat/index.tsx b/web/app/components/base/chat/chat/index.tsx index 5f5faab2403..9c27b61e1fa 100644 --- a/web/app/components/base/chat/chat/index.tsx +++ b/web/app/components/base/chat/chat/index.tsx @@ -13,7 +13,7 @@ import type { import type { InputForm } from './type' import type { Emoji } from '@/app/components/tools/types' import type { AppData } from '@/models/share' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/base/chat/embedded-chatbot/context.tsx b/web/app/components/base/chat/embedded-chatbot/context.tsx index 2738d25c756..97d3dd53cf2 100644 --- a/web/app/components/base/chat/embedded-chatbot/context.tsx +++ b/web/app/components/base/chat/embedded-chatbot/context.tsx @@ -13,7 +13,7 @@ import type { AppMeta, ConversationItem, } from '@/models/share' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext } from 'use-context-selector' export type EmbeddedChatbotContextValue = { diff --git a/web/app/components/base/chat/embedded-chatbot/hooks.tsx b/web/app/components/base/chat/embedded-chatbot/hooks.tsx index 3c7fd576a39..9e9125fc457 100644 --- a/web/app/components/base/chat/embedded-chatbot/hooks.tsx +++ b/web/app/components/base/chat/embedded-chatbot/hooks.tsx @@ -9,8 +9,8 @@ import type { ConversationItem, } from '@/models/share' import { useLocalStorageState } from 'ahooks' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/base/copy-feedback/index.tsx b/web/app/components/base/copy-feedback/index.tsx index bf809a2d189..bb71d62c11d 100644 --- a/web/app/components/base/copy-feedback/index.tsx +++ b/web/app/components/base/copy-feedback/index.tsx @@ -4,7 +4,7 @@ import { RiClipboardLine, } from '@remixicon/react' import copy from 'copy-to-clipboard' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/copy-icon/index.tsx b/web/app/components/base/copy-icon/index.tsx index 935444a3c14..73a0d80b456 100644 --- a/web/app/components/base/copy-icon/index.tsx +++ b/web/app/components/base/copy-icon/index.tsx @@ -1,6 +1,6 @@ 'use client' import copy from 'copy-to-clipboard' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/emoji-picker/index.tsx b/web/app/components/base/emoji-picker/index.tsx index 53bef278f61..4fc146ab59a 100644 --- a/web/app/components/base/emoji-picker/index.tsx +++ b/web/app/components/base/emoji-picker/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx index a1b66ae0fc6..361f24465fa 100644 --- a/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx +++ b/web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx @@ -3,8 +3,8 @@ import type { InputVar } from '@/app/components/workflow/types' import type { PromptVariable } from '@/models/debug' import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react' import { useBoolean } from 'ahooks' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx index d1e6aba6b76..7de0119e593 100644 --- a/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx +++ b/web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx @@ -2,7 +2,7 @@ import type { ChangeEvent, FC } from 'react' import type { CodeBasedExtensionItem } from '@/models/common' import type { ModerationConfig, ModerationContentConfig } from '@/models/debug' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/base/file-uploader/hooks.ts b/web/app/components/base/file-uploader/hooks.ts index e62addee2d8..6678368462f 100644 --- a/web/app/components/base/file-uploader/hooks.ts +++ b/web/app/components/base/file-uploader/hooks.ts @@ -2,8 +2,8 @@ import type { ClipboardEvent } from 'react' import type { FileEntity } from './types' import type { FileUpload } from '@/app/components/base/features/types' import type { FileUploadConfigResponse } from '@/models/common' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import { useParams } from 'next/navigation' import { useCallback, diff --git a/web/app/components/base/file-uploader/pdf-preview.tsx b/web/app/components/base/file-uploader/pdf-preview.tsx index 04a90a414cf..b4f057e91ed 100644 --- a/web/app/components/base/file-uploader/pdf-preview.tsx +++ b/web/app/components/base/file-uploader/pdf-preview.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { RiCloseLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' +import { noop } from 'es-toolkit/compat' import { t } from 'i18next' -import { noop } from 'lodash-es' import * as React from 'react' import { useState } from 'react' import { createPortal } from 'react-dom' diff --git a/web/app/components/base/file-uploader/store.tsx b/web/app/components/base/file-uploader/store.tsx index db3e3622f90..2172733f20b 100644 --- a/web/app/components/base/file-uploader/store.tsx +++ b/web/app/components/base/file-uploader/store.tsx @@ -1,7 +1,7 @@ import type { FileEntity, } from './types' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { createContext, useContext, diff --git a/web/app/components/base/fullscreen-modal/index.tsx b/web/app/components/base/fullscreen-modal/index.tsx index b822d21921b..cad91b24524 100644 --- a/web/app/components/base/fullscreen-modal/index.tsx +++ b/web/app/components/base/fullscreen-modal/index.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react' import { RiCloseLargeLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { cn } from '@/utils/classnames' type IModal = { diff --git a/web/app/components/base/icons/script.mjs b/web/app/components/base/icons/script.mjs index 1cee66d1db5..81566cc4cf0 100644 --- a/web/app/components/base/icons/script.mjs +++ b/web/app/components/base/icons/script.mjs @@ -2,7 +2,7 @@ import { access, appendFile, mkdir, open, readdir, rm, writeFile } from 'node:fs import path from 'node:path' import { fileURLToPath } from 'node:url' import { parseXml } from '@rgrove/parse-xml' -import { camelCase, template } from 'lodash-es' +import { camelCase, template } from 'es-toolkit/compat' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/web/app/components/base/image-uploader/image-preview.tsx b/web/app/components/base/image-uploader/image-preview.tsx index bfabe5e2473..519bed4d250 100644 --- a/web/app/components/base/image-uploader/image-preview.tsx +++ b/web/app/components/base/image-uploader/image-preview.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { RiAddBoxLine, RiCloseLine, RiDownloadCloud2Line, RiFileCopyLine, RiZoomInLine, RiZoomOutLine } from '@remixicon/react' +import { noop } from 'es-toolkit/compat' import { t } from 'i18next' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { createPortal } from 'react-dom' diff --git a/web/app/components/base/input-with-copy/index.spec.tsx b/web/app/components/base/input-with-copy/index.spec.tsx index 782b2bab256..a5fde4ea448 100644 --- a/web/app/components/base/input-with-copy/index.spec.tsx +++ b/web/app/components/base/input-with-copy/index.spec.tsx @@ -25,8 +25,8 @@ vi.mock('react-i18next', () => ({ }), })) -// Mock lodash-es debounce -vi.mock('lodash-es', () => ({ +// Mock es-toolkit/compat debounce +vi.mock('es-toolkit/compat', () => ({ debounce: (fn: any) => fn, })) diff --git a/web/app/components/base/input-with-copy/index.tsx b/web/app/components/base/input-with-copy/index.tsx index 151fa435e7f..41bb8f3dec2 100644 --- a/web/app/components/base/input-with-copy/index.tsx +++ b/web/app/components/base/input-with-copy/index.tsx @@ -2,7 +2,7 @@ import type { InputProps } from '../input' import { RiClipboardFill, RiClipboardLine } from '@remixicon/react' import copy from 'copy-to-clipboard' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/input/index.tsx b/web/app/components/base/input/index.tsx index 6c6a0c6a750..db7d1f09900 100644 --- a/web/app/components/base/input/index.tsx +++ b/web/app/components/base/input/index.tsx @@ -2,7 +2,7 @@ import type { VariantProps } from 'class-variance-authority' import type { ChangeEventHandler, CSSProperties, FocusEventHandler } from 'react' import { RiCloseCircleFill, RiErrorWarningLine, RiSearchLine } from '@remixicon/react' import { cva } from 'class-variance-authority' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/base/markdown/index.tsx b/web/app/components/base/markdown/index.tsx index e698f3bebec..c487b205508 100644 --- a/web/app/components/base/markdown/index.tsx +++ b/web/app/components/base/markdown/index.tsx @@ -1,5 +1,5 @@ import type { ReactMarkdownWrapperProps, SimplePluginInfo } from './react-markdown-wrapper' -import { flow } from 'lodash-es' +import { flow } from 'es-toolkit/compat' import dynamic from 'next/dynamic' import { cn } from '@/utils/classnames' import { preprocessLaTeX, preprocessThinkTag } from './markdown-utils' diff --git a/web/app/components/base/markdown/markdown-utils.ts b/web/app/components/base/markdown/markdown-utils.ts index 59326a7a19b..94ad31d1ded 100644 --- a/web/app/components/base/markdown/markdown-utils.ts +++ b/web/app/components/base/markdown/markdown-utils.ts @@ -3,7 +3,7 @@ * These functions were extracted from the main markdown renderer for better separation of concerns. * Includes preprocessing for LaTeX and custom "think" tags. */ -import { flow } from 'lodash-es' +import { flow } from 'es-toolkit/compat' import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config' export const preprocessLaTeX = (content: string) => { diff --git a/web/app/components/base/modal/index.tsx b/web/app/components/base/modal/index.tsx index 1b0ff228731..7270af1c775 100644 --- a/web/app/components/base/modal/index.tsx +++ b/web/app/components/base/modal/index.tsx @@ -1,6 +1,6 @@ import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { Fragment } from 'react' import { cn } from '@/utils/classnames' // https://headlessui.com/react/dialog diff --git a/web/app/components/base/modal/modal.tsx b/web/app/components/base/modal/modal.tsx index 69bca1ccbbb..6fa44d42d0f 100644 --- a/web/app/components/base/modal/modal.tsx +++ b/web/app/components/base/modal/modal.tsx @@ -1,6 +1,6 @@ import type { ButtonProps } from '@/app/components/base/button' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { memo } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/base/pagination/pagination.tsx b/web/app/components/base/pagination/pagination.tsx index 25bdf5a31b0..dafe0e4ab95 100644 --- a/web/app/components/base/pagination/pagination.tsx +++ b/web/app/components/base/pagination/pagination.tsx @@ -4,7 +4,7 @@ import type { IPaginationProps, PageButtonProps, } from './type' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { cn } from '@/utils/classnames' import usePagination from './hook' diff --git a/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx index 2d2a0b62636..c4a246c40d3 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/context-block-replacement-block.tsx @@ -1,8 +1,8 @@ import type { ContextBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' +import { noop } from 'es-toolkit/compat' import { $applyNodeReplacement } from 'lexical' -import { noop } from 'lodash-es' import { memo, useCallback, diff --git a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx index 9f2102bc624..ce3ed4c210d 100644 --- a/web/app/components/base/prompt-editor/plugins/context-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/context-block/index.tsx @@ -1,12 +1,12 @@ import type { ContextBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' +import { noop } from 'es-toolkit/compat' import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand, } from 'lexical' -import { noop } from 'lodash-es' import { memo, useEffect, diff --git a/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx index ba695ec95d5..f62fb6886b9 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/history-block-replacement-block.tsx @@ -1,8 +1,8 @@ import type { HistoryBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' +import { noop } from 'es-toolkit/compat' import { $applyNodeReplacement } from 'lexical' -import { noop } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx index a853de5162d..dc75fc230d2 100644 --- a/web/app/components/base/prompt-editor/plugins/history-block/index.tsx +++ b/web/app/components/base/prompt-editor/plugins/history-block/index.tsx @@ -1,12 +1,12 @@ import type { HistoryBlockType } from '../../types' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import { mergeRegister } from '@lexical/utils' +import { noop } from 'es-toolkit/compat' import { $insertNodes, COMMAND_PRIORITY_EDITOR, createCommand, } from 'lexical' -import { noop } from 'lodash-es' import { memo, useEffect, diff --git a/web/app/components/base/radio-card/index.tsx b/web/app/components/base/radio-card/index.tsx index ae8bb000996..678d5b6deea 100644 --- a/web/app/components/base/radio-card/index.tsx +++ b/web/app/components/base/radio-card/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { cn } from '@/utils/classnames' diff --git a/web/app/components/base/tag-management/panel.tsx b/web/app/components/base/tag-management/panel.tsx index 854de012a51..0023a003c57 100644 --- a/web/app/components/base/tag-management/panel.tsx +++ b/web/app/components/base/tag-management/panel.tsx @@ -3,7 +3,7 @@ import type { HtmlContentProps } from '@/app/components/base/popover' import type { Tag } from '@/app/components/base/tag-management/constant' import { RiAddLine, RiPriceTag3Line } from '@remixicon/react' import { useUnmount } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/base/tag-management/tag-remove-modal.tsx b/web/app/components/base/tag-management/tag-remove-modal.tsx index 8e796f8e0f3..8061cde5c15 100644 --- a/web/app/components/base/tag-management/tag-remove-modal.tsx +++ b/web/app/components/base/tag-management/tag-remove-modal.tsx @@ -2,7 +2,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' diff --git a/web/app/components/base/toast/index.spec.tsx b/web/app/components/base/toast/index.spec.tsx index d32619f59a2..59314063dd2 100644 --- a/web/app/components/base/toast/index.spec.tsx +++ b/web/app/components/base/toast/index.spec.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { act, render, screen, waitFor } from '@testing-library/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import Toast, { ToastProvider, useToastContext } from '.' diff --git a/web/app/components/base/toast/index.tsx b/web/app/components/base/toast/index.tsx index a0167789965..cf9e1cd909b 100644 --- a/web/app/components/base/toast/index.tsx +++ b/web/app/components/base/toast/index.tsx @@ -7,7 +7,7 @@ import { RiErrorWarningFill, RiInformation2Fill, } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { createRoot } from 'react-dom/client' diff --git a/web/app/components/base/with-input-validation/index.spec.tsx b/web/app/components/base/with-input-validation/index.spec.tsx index c1a9d55f0af..b2e67ce056b 100644 --- a/web/app/components/base/with-input-validation/index.spec.tsx +++ b/web/app/components/base/with-input-validation/index.spec.tsx @@ -1,5 +1,5 @@ import { render, screen } from '@testing-library/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { z } from 'zod' import withValidation from '.' diff --git a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx index 16357200375..f685eb1f2ea 100644 --- a/web/app/components/datasets/common/document-status-with-action/index-failed.tsx +++ b/web/app/components/datasets/common/document-status-with-action/index-failed.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { IndexingStatusResponse } from '@/models/datasets' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useReducer } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx index d5955d9bc63..b8c413ba8f7 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx @@ -1,6 +1,6 @@ 'use client' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/create/step-two/index.tsx b/web/app/components/datasets/create/step-two/index.tsx index 981b6c5a8fc..e0a330507cf 100644 --- a/web/app/components/datasets/create/step-two/index.tsx +++ b/web/app/components/datasets/create/step-two/index.tsx @@ -9,7 +9,7 @@ import { RiArrowLeftLine, RiSearchEyeLine, } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Image from 'next/image' import Link from 'next/link' import * as React from 'react' diff --git a/web/app/components/datasets/documents/detail/batch-modal/index.tsx b/web/app/components/datasets/documents/detail/batch-modal/index.tsx index 091d5c493ed..0d7199c6c6d 100644 --- a/web/app/components/datasets/documents/detail/batch-modal/index.tsx +++ b/web/app/components/datasets/documents/detail/batch-modal/index.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { ChunkingMode, FileItem } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx index 5f62bf0185b..73b5cbdef9e 100644 --- a/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/full-screen-drawer.tsx @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { cn } from '@/utils/classnames' import Drawer from './drawer' diff --git a/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx index 4957104e25e..114d713170a 100644 --- a/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/regeneration-modal.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { RiLoader2Line } from '@remixicon/react' import { useCountDown } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/detail/completed/index.tsx b/web/app/components/datasets/documents/detail/completed/index.tsx index 1b4aadfa502..e1491258656 100644 --- a/web/app/components/datasets/documents/detail/completed/index.tsx +++ b/web/app/components/datasets/documents/detail/completed/index.tsx @@ -4,7 +4,7 @@ import type { Item } from '@/app/components/base/select' import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { ChildChunkDetail, SegmentDetailModel, SegmentUpdater } from '@/models/datasets' import { useDebounceFn } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { usePathname } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' diff --git a/web/app/components/datasets/documents/detail/metadata/index.tsx b/web/app/components/datasets/documents/detail/metadata/index.tsx index 87d136c3fe7..dbae76491e1 100644 --- a/web/app/components/datasets/documents/detail/metadata/index.tsx +++ b/web/app/components/datasets/documents/detail/metadata/index.tsx @@ -4,7 +4,7 @@ import type { inputType, metadataType } from '@/hooks/use-metadata' import type { CommonResponse } from '@/models/common' import type { DocType, FullDocumentDetail } from '@/models/datasets' import { PencilIcon } from '@heroicons/react/24/outline' -import { get } from 'lodash-es' +import { get } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx index 2d71fa9e7d0..2eac6b788d4 100644 --- a/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx +++ b/web/app/components/datasets/documents/detail/settings/pipeline-settings/index.tsx @@ -1,7 +1,7 @@ import type { NotionPage } from '@/models/common' import type { CrawlResultItem, CustomFile, FileIndexingEstimateResponse } from '@/models/datasets' import type { OnlineDriveFile, PublishedPipelineRunPreviewResponse } from '@/models/pipeline' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/documents/list.tsx b/web/app/components/datasets/documents/list.tsx index 0b06d5fe152..670cdd659c9 100644 --- a/web/app/components/datasets/documents/list.tsx +++ b/web/app/components/datasets/documents/list.tsx @@ -9,7 +9,7 @@ import { RiGlobalLine, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { pick, uniq } from 'lodash-es' +import { pick, uniq } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useMemo, useState } from 'react' diff --git a/web/app/components/datasets/documents/operations.tsx b/web/app/components/datasets/documents/operations.tsx index 825a3151780..038a89b56e4 100644 --- a/web/app/components/datasets/documents/operations.tsx +++ b/web/app/components/datasets/documents/operations.tsx @@ -11,7 +11,7 @@ import { RiPlayCircleLine, } from '@remixicon/react' import { useBoolean, useDebounceFn } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import * as React from 'react' import { useCallback, useState } from 'react' diff --git a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts index 66dfc0384f2..4de18f6e500 100644 --- a/web/app/components/datasets/metadata/hooks/use-metadata-document.ts +++ b/web/app/components/datasets/metadata/hooks/use-metadata-document.ts @@ -1,6 +1,6 @@ import type { BuiltInMetadataItem, MetadataItemWithValue } from '../types' import type { FullDocumentDetail } from '@/models/datasets' -import { get } from 'lodash-es' +import { get } from 'es-toolkit/compat' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import Toast from '@/app/components/base/toast' diff --git a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx index d31e9d79576..85fd01b2bbf 100644 --- a/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx +++ b/web/app/components/datasets/metadata/metadata-dataset/create-content.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import { RiArrowLeftLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/datasets/rename-modal/index.tsx b/web/app/components/datasets/rename-modal/index.tsx index 8b152b8f028..ef988fc9bc5 100644 --- a/web/app/components/datasets/rename-modal/index.tsx +++ b/web/app/components/datasets/rename-modal/index.tsx @@ -4,7 +4,7 @@ import type { MouseEventHandler } from 'react' import type { AppIconSelection } from '../../base/app-icon-picker' import type { DataSet } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/explore/create-app-modal/index.tsx b/web/app/components/explore/create-app-modal/index.tsx index dac89bc776e..b05188fe4de 100644 --- a/web/app/components/explore/create-app-modal/index.tsx +++ b/web/app/components/explore/create-app-modal/index.tsx @@ -2,7 +2,7 @@ import type { AppIconType } from '@/types/app' import { RiCloseLine, RiCommandLine, RiCornerDownLeftLine } from '@remixicon/react' import { useDebounceFn, useKeyPress } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx index d1f64f0d592..f691044bd32 100644 --- a/web/app/components/header/account-setting/api-based-extension-page/modal.tsx +++ b/web/app/components/header/account-setting/api-based-extension-page/modal.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import type { ApiBasedExtension } from '@/models/common' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx index d139ab39df6..2766dc016cc 100644 --- a/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx +++ b/web/app/components/header/account-setting/data-source-page/data-source-notion/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { DataSourceNotion as TDataSourceNotion } from '@/models/common' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx index b98dd7933dd..c57ef96bf9d 100644 --- a/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx +++ b/web/app/components/header/account-setting/data-source-page/panel/config-item.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import { RiDeleteBinLine, } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useTranslation } from 'react-i18next' import { cn } from '@/utils/classnames' diff --git a/web/app/components/header/account-setting/key-validator/hooks.ts b/web/app/components/header/account-setting/key-validator/hooks.ts index eeb7b63955a..4d4cf8d8a46 100644 --- a/web/app/components/header/account-setting/key-validator/hooks.ts +++ b/web/app/components/header/account-setting/key-validator/hooks.ts @@ -1,4 +1,4 @@ -import type { DebouncedFunc } from 'lodash-es' +import type { DebouncedFunc } from 'es-toolkit/compat' import type { ValidateCallback, ValidatedStatusState, ValidateValue } from './declarations' import { useDebounceFn } from 'ahooks' import { useState } from 'react' diff --git a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx index 7d609dba688..31f3fc0afee 100644 --- a/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/edit-workspace-modal/index.tsx @@ -1,6 +1,6 @@ 'use client' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' diff --git a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx index 9b80cd343d8..93d88fc4eae 100644 --- a/web/app/components/header/account-setting/members-page/invite-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invite-modal/index.tsx @@ -2,7 +2,7 @@ import type { InvitationResult } from '@/models/common' import { RiCloseLine, RiErrorWarningFill } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ReactMultiEmail } from 'react-multi-email' diff --git a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx index 5fb6c410c41..829eb364a4e 100644 --- a/web/app/components/header/account-setting/members-page/invited-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/invited-modal/index.tsx @@ -2,7 +2,7 @@ import type { InvitationResult } from '@/models/common' import { XMarkIcon } from '@heroicons/react/24/outline' import { CheckCircleIcon } from '@heroicons/react/24/solid' import { RiQuestionLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx index 2c6a33dc1fd..323e8f300c8 100644 --- a/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx +++ b/web/app/components/header/account-setting/members-page/transfer-ownership-modal/index.tsx @@ -1,5 +1,5 @@ import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' diff --git a/web/app/components/header/account-setting/menu-dialog.tsx b/web/app/components/header/account-setting/menu-dialog.tsx index 0b2d2208cd3..cc5adbc18f8 100644 --- a/web/app/components/header/account-setting/menu-dialog.tsx +++ b/web/app/components/header/account-setting/menu-dialog.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from 'react' import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { Fragment, useCallback, useEffect } from 'react' import { cn } from '@/utils/classnames' diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index ab567540d4c..34d192de416 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -5,8 +5,8 @@ import { RiRobot2Fill, RiRobot2Line, } from '@remixicon/react' +import { flatten } from 'es-toolkit/compat' import { produce } from 'immer' -import { flatten } from 'lodash-es' import { useParams } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/app-selector/index.tsx b/web/app/components/header/app-selector/index.tsx index c15ed3e79c3..a9aab59356d 100644 --- a/web/app/components/header/app-selector/index.tsx +++ b/web/app/components/header/app-selector/index.tsx @@ -2,7 +2,7 @@ import type { AppDetailResponse } from '@/models/app' import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react' import { ChevronDownIcon, PlusIcon } from '@heroicons/react/24/solid' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/dataset-nav/index.tsx b/web/app/components/header/dataset-nav/index.tsx index e1943a4ec28..4cdc259d677 100644 --- a/web/app/components/header/dataset-nav/index.tsx +++ b/web/app/components/header/dataset-nav/index.tsx @@ -6,7 +6,7 @@ import { RiBook2Fill, RiBook2Line, } from '@remixicon/react' -import { flatten } from 'lodash-es' +import { flatten } from 'es-toolkit/compat' import { useParams, useRouter } from 'next/navigation' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/header/nav/nav-selector/index.tsx b/web/app/components/header/nav/nav-selector/index.tsx index 905597f021f..c12be7e035c 100644 --- a/web/app/components/header/nav/nav-selector/index.tsx +++ b/web/app/components/header/nav/nav-selector/index.tsx @@ -6,7 +6,7 @@ import { RiArrowDownSLine, RiArrowRightSLine, } from '@remixicon/react' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import { useRouter } from 'next/navigation' import { Fragment, useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/base/deprecation-notice.tsx b/web/app/components/plugins/base/deprecation-notice.tsx index 76d64a45f47..efdd9038831 100644 --- a/web/app/components/plugins/base/deprecation-notice.tsx +++ b/web/app/components/plugins/base/deprecation-notice.tsx @@ -1,6 +1,6 @@ import type { FC } from 'react' import { RiAlertFill } from '@remixicon/react' -import { camelCase } from 'lodash-es' +import { camelCase } from 'es-toolkit/compat' import Link from 'next/link' import * as React from 'react' import { useMemo } from 'react' diff --git a/web/app/components/plugins/install-plugin/utils.ts b/web/app/components/plugins/install-plugin/utils.ts index 32d3e542258..549bdc62415 100644 --- a/web/app/components/plugins/install-plugin/utils.ts +++ b/web/app/components/plugins/install-plugin/utils.ts @@ -1,6 +1,6 @@ import type { Plugin, PluginDeclaration, PluginManifestInMarket } from '../types' import type { GitHubUrlInfo } from '@/app/components/plugins/types' -import { isEmpty } from 'lodash-es' +import { isEmpty } from 'es-toolkit/compat' export const pluginManifestToCardPluginProps = (pluginManifest: PluginDeclaration): Plugin => { return { diff --git a/web/app/components/plugins/marketplace/context.tsx b/web/app/components/plugins/marketplace/context.tsx index ec76d3440f2..4053c4a556e 100644 --- a/web/app/components/plugins/marketplace/context.tsx +++ b/web/app/components/plugins/marketplace/context.tsx @@ -10,7 +10,7 @@ import type { SearchParams, SearchParamsFromCollection, } from './types' -import { debounce, noop } from 'lodash-es' +import { debounce, noop } from 'es-toolkit/compat' import { useCallback, useEffect, diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx index b0625d1e825..d0326f6b439 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/create/common-modal.tsx @@ -3,7 +3,7 @@ import type { FormRefObject } from '@/app/components/base/form/types' import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types' import type { BuildTriggerSubscriptionPayload } from '@/service/use-triggers' import { RiLoader2Line } from '@remixicon/react' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx index 18bebf8d959..539544f4359 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { EncryptedBottom } from '@/app/components/base/encrypted-bottom' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx index 96c1b6e278d..404766ae436 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx index 9adee6cc342..53bb6aa69dc 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx @@ -2,7 +2,7 @@ import type { FormRefObject, FormSchema } from '@/app/components/base/form/types' import type { ParametersSchema, PluginDetail } from '@/app/components/plugins/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { BaseForm } from '@/app/components/base/form/components/base' diff --git a/web/app/components/plugins/plugin-page/context.tsx b/web/app/components/plugins/plugin-page/context.tsx index 8a560ac90a8..146353da4f2 100644 --- a/web/app/components/plugins/plugin-page/context.tsx +++ b/web/app/components/plugins/plugin-page/context.tsx @@ -2,7 +2,7 @@ import type { ReactNode, RefObject } from 'react' import type { FilterState } from './filter-management' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useMemo, useRef, diff --git a/web/app/components/plugins/plugin-page/empty/index.tsx b/web/app/components/plugins/plugin-page/empty/index.tsx index 4d8904d2935..90854bda5d8 100644 --- a/web/app/components/plugins/plugin-page/empty/index.tsx +++ b/web/app/components/plugins/plugin-page/empty/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/plugins/plugin-page/index.tsx b/web/app/components/plugins/plugin-page/index.tsx index 2c6f90caa58..afa85d70100 100644 --- a/web/app/components/plugins/plugin-page/index.tsx +++ b/web/app/components/plugins/plugin-page/index.tsx @@ -7,7 +7,7 @@ import { RiEqualizer2Line, } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, diff --git a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx index 60c18dca126..22ed35e95b1 100644 --- a/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx +++ b/web/app/components/plugins/plugin-page/install-plugin-dropdown.tsx @@ -1,7 +1,7 @@ 'use client' import { RiAddLine, RiArrowDownSLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' diff --git a/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx b/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx index 3c4faad439a..108f3a642f4 100644 --- a/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx +++ b/web/app/components/rag-pipeline/components/panel/input-field/field-list/field-list-container.tsx @@ -1,6 +1,6 @@ import type { SortableItem } from './types' import type { InputVar } from '@/models/pipeline' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx index b11bae24494..303a8caaf57 100644 --- a/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx +++ b/web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx @@ -2,7 +2,7 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { IconInfo } from '@/models/datasets' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' diff --git a/web/app/components/rag-pipeline/hooks/use-pipeline.tsx b/web/app/components/rag-pipeline/hooks/use-pipeline.tsx index 779a38df995..c84bba660d2 100644 --- a/web/app/components/rag-pipeline/hooks/use-pipeline.tsx +++ b/web/app/components/rag-pipeline/hooks/use-pipeline.tsx @@ -1,6 +1,6 @@ import type { DataSourceNodeType } from '../../workflow/nodes/data-source/types' import type { Node, ValueSelector } from '../../workflow/types' -import { uniqBy } from 'lodash-es' +import { uniqBy } from 'es-toolkit/compat' import { useCallback } from 'react' import { getOutgoers, useStoreApi } from 'reactflow' import { findUsedVarNodes, updateNodeVars } from '../../workflow/nodes/_base/components/variable/utils' diff --git a/web/app/components/tools/labels/selector.tsx b/web/app/components/tools/labels/selector.tsx index e3afea41a85..43143e5a058 100644 --- a/web/app/components/tools/labels/selector.tsx +++ b/web/app/components/tools/labels/selector.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { Label } from '@/app/components/tools/labels/constant' import { RiArrowDownSLine } from '@remixicon/react' import { useDebounceFn } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index eb8f280484e..c5cde65674c 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -5,7 +5,7 @@ import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AppIconType } from '@/types/app' import { RiCloseLine, RiEditLine } from '@remixicon/react' import { useHover } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/tools/setting/build-in/config-credentials.tsx b/web/app/components/tools/setting/build-in/config-credentials.tsx index 43383cdb510..033052e8a1e 100644 --- a/web/app/components/tools/setting/build-in/config-credentials.tsx +++ b/web/app/components/tools/setting/build-in/config-credentials.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import type { Collection } from '../../types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx index f90774cb32d..e1a7dff113f 100644 --- a/web/app/components/tools/workflow-tool/confirm-modal/index.tsx +++ b/web/app/components/tools/workflow-tool/confirm-modal/index.tsx @@ -1,7 +1,7 @@ 'use client' import { RiCloseLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback' diff --git a/web/app/components/workflow-app/hooks/use-workflow-run.ts b/web/app/components/workflow-app/hooks/use-workflow-run.ts index 16c7bab6d4e..031e2949cb9 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-run.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-run.ts @@ -2,8 +2,8 @@ import type AudioPlayer from '@/app/components/base/audio-btn/audio' import type { Node } from '@/app/components/workflow/types' import type { IOtherOptions } from '@/service/base' import type { VersionHistory } from '@/types/workflow' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import { usePathname } from 'next/navigation' import { useCallback, useRef } from 'react' import { diff --git a/web/app/components/workflow/block-selector/blocks.tsx b/web/app/components/workflow/block-selector/blocks.tsx index 51d08d15df9..388532f7a23 100644 --- a/web/app/components/workflow/block-selector/blocks.tsx +++ b/web/app/components/workflow/block-selector/blocks.tsx @@ -1,5 +1,5 @@ import type { NodeDefault } from '../types' -import { groupBy } from 'lodash-es' +import { groupBy } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx index 58724b46219..d6f3007b516 100644 --- a/web/app/components/workflow/block-selector/market-place-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/market-place-plugin/list.tsx @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { Plugin, PluginCategoryEnum } from '@/app/components/plugins/types' import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useEffect, useImperativeHandle, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/custom-edge.tsx b/web/app/components/workflow/custom-edge.tsx index 3b73e07536e..6427520d817 100644 --- a/web/app/components/workflow/custom-edge.tsx +++ b/web/app/components/workflow/custom-edge.tsx @@ -3,7 +3,7 @@ import type { Edge, OnSelectBlock, } from './types' -import { intersection } from 'lodash-es' +import { intersection } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/dsl-export-confirm-modal.tsx b/web/app/components/workflow/dsl-export-confirm-modal.tsx index b616ec5fb57..c5ae8e7cffb 100644 --- a/web/app/components/workflow/dsl-export-confirm-modal.tsx +++ b/web/app/components/workflow/dsl-export-confirm-modal.tsx @@ -1,7 +1,7 @@ 'use client' import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiCloseLine, RiLock2Line } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/hooks-store/store.ts b/web/app/components/workflow/hooks-store/store.ts index 79cfb7dbcec..3aa4ba3d912 100644 --- a/web/app/components/workflow/hooks-store/store.ts +++ b/web/app/components/workflow/hooks-store/store.ts @@ -12,7 +12,7 @@ import type { FlowType } from '@/types/common' import type { VarInInspect } from '@/types/workflow' import { noop, -} from 'lodash-es' +} from 'es-toolkit/compat' import { useContext } from 'react' import { useStore as useZustandStore, diff --git a/web/app/components/workflow/hooks/use-nodes-layout.ts b/web/app/components/workflow/hooks/use-nodes-layout.ts index 653b37008c4..2ad89ff1005 100644 --- a/web/app/components/workflow/hooks/use-nodes-layout.ts +++ b/web/app/components/workflow/hooks/use-nodes-layout.ts @@ -3,7 +3,7 @@ import type { Node, } from '../types' import ELK from 'elkjs/lib/elk.bundled.js' -import { cloneDeep } from 'lodash-es' +import { cloneDeep } from 'es-toolkit/compat' import { useCallback } from 'react' import { useReactFlow, diff --git a/web/app/components/workflow/hooks/use-workflow-history.ts b/web/app/components/workflow/hooks/use-workflow-history.ts index 3e6043a1fd2..0171271c3bb 100644 --- a/web/app/components/workflow/hooks/use-workflow-history.ts +++ b/web/app/components/workflow/hooks/use-workflow-history.ts @@ -1,5 +1,5 @@ import type { WorkflowHistoryEventMeta } from '../workflow-history-store' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import { useCallback, useRef, diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index c958bb6b83f..990c8c950d2 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -9,7 +9,7 @@ import type { Node, ValueSelector, } from '../types' -import { uniqBy } from 'lodash-es' +import { uniqBy } from 'es-toolkit/compat' import { useCallback, } from 'react' diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 1d0c594c234..2b2b1ee5434 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -13,8 +13,8 @@ import type { VarInInspect } from '@/types/workflow' import { useEventListener, } from 'ahooks' +import { isEqual } from 'es-toolkit/compat' import { setAutoFreeze } from 'immer' -import { isEqual } from 'lodash-es' import dynamic from 'next/dynamic' import { memo, diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx index d7592b3c6f1..a390ccec3c5 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy.tsx @@ -4,7 +4,7 @@ import type { NodeOutPutVar } from '../../../types' import type { ToolVarInputs } from '../../tool/types' import type { CredentialFormSchema, CredentialFormSchemaNumberInput, CredentialFormSchemaTextInput } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { PluginMeta } from '@/app/components/plugins/types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { memo } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx index fa7aef90a5b..ad5410986c2 100644 --- a/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/editor/code-editor/index.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' import Editor, { loader } from '@monaco-editor/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { diff --git a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx index b354d35276e..9498edba8fc 100644 --- a/web/app/components/workflow/nodes/_base/components/file-type-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-type-item.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx index 348bb23302c..e0bb6f6df55 100644 --- a/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx +++ b/web/app/components/workflow/nodes/_base/components/input-support-select-var.tsx @@ -5,7 +5,7 @@ import type { NodeOutPutVar, } from '@/app/components/workflow/types' import { useBoolean } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx index 0f23161261b..b9a4404dbbf 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/index.tsx @@ -1,7 +1,7 @@ import type { Node, } from '../../../../types' -import { isEqual } from 'lodash-es' +import { isEqual } from 'es-toolkit/compat' import { memo, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { diff --git a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx index 1b550f035de..906dad4bc9a 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx @@ -3,7 +3,7 @@ import type { OnSelectBlock, } from '@/app/components/workflow/types' import { RiMoreFill } from '@remixicon/react' -import { intersection } from 'lodash-es' +import { intersection } from 'es-toolkit/compat' import { useCallback, } from 'react' diff --git a/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx b/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx index 47e1c4b22b0..9236c022b9f 100644 --- a/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx +++ b/web/app/components/workflow/nodes/_base/components/panel-operator/change-block.tsx @@ -2,7 +2,7 @@ import type { Node, OnSelectBlock, } from '@/app/components/workflow/types' -import { intersection } from 'lodash-es' +import { intersection } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/_base/components/variable/utils.ts b/web/app/components/workflow/nodes/_base/components/variable/utils.ts index 5a5a9b826ab..a7dc04e5715 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/utils.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/utils.ts @@ -33,8 +33,8 @@ import type { import type { PromptItem } from '@/models/debug' import type { RAGPipelineVariable } from '@/models/pipeline' import type { SchemaTypeDefinition } from '@/service/use-common' +import { isArray, uniq } from 'es-toolkit/compat' import { produce } from 'immer' -import { isArray, uniq } from 'lodash-es' import { AGENT_OUTPUT_STRUCT, FILE_STRUCT, diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 05e2c913cea..e92fc9a3b7c 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -11,8 +11,8 @@ import { RiLoader4Line, RiMoreLine, } from '@remixicon/react' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx index dd0dfa86823..078ba1ef4f9 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-vars.tsx @@ -4,7 +4,7 @@ import type { StructuredOutput } from '../../../llm/types' import type { Field } from '@/app/components/workflow/nodes/llm/types' import type { NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types' import { useHover } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx index 63b392482f0..828f7e5ebee 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/variable-label/base/variable-label.tsx @@ -3,7 +3,7 @@ import { RiErrorWarningFill, RiMoreLine, } from '@remixicon/react' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo } from 'react' import Tooltip from '@/app/components/base/tooltip' import { cn } from '@/utils/classnames' diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 8e684afa87c..2e25d029b46 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -6,7 +6,7 @@ import { RiCloseLine, RiPlayLargeLine, } from '@remixicon/react' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { cloneElement, diff --git a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts index 59774ab96ae..d8fd4d2c57e 100644 --- a/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts +++ b/web/app/components/workflow/nodes/_base/hooks/use-one-step-run.ts @@ -1,9 +1,9 @@ import type { CommonNodeType, InputVar, TriggerNodeType, ValueSelector, Var, Variable } from '@/app/components/workflow/types' import type { FlowType } from '@/types/common' import type { NodeRunResult, NodeTracing } from '@/types/workflow' -import { produce } from 'immer' +import { noop, unionBy } from 'es-toolkit/compat' -import { noop, unionBy } from 'lodash-es' +import { produce } from 'immer' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { diff --git a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx index 422cd5a486b..f5ea45d60e7 100644 --- a/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/assigner/components/var-list/index.tsx @@ -3,8 +3,8 @@ import type { FC } from 'react' import type { AssignerNodeOperation } from '../../types' import type { ValueSelector, Var } from '@/app/components/workflow/types' import { RiDeleteBinLine } from '@remixicon/react' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/assigner/hooks.ts b/web/app/components/workflow/nodes/assigner/hooks.ts index d708222868d..a3a5db8bfbd 100644 --- a/web/app/components/workflow/nodes/assigner/hooks.ts +++ b/web/app/components/workflow/nodes/assigner/hooks.ts @@ -2,7 +2,7 @@ import type { Node, Var, } from '../../types' -import { uniqBy } from 'lodash-es' +import { uniqBy } from 'es-toolkit/compat' import { useCallback } from 'react' import { useNodes } from 'reactflow' import { diff --git a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx index c475f1234aa..90c230b6bb5 100644 --- a/web/app/components/workflow/nodes/http/components/edit-body/index.tsx +++ b/web/app/components/workflow/nodes/http/components/edit-body/index.tsx @@ -2,8 +2,8 @@ import type { FC } from 'react' import type { Body, BodyPayload, KeyValue as KeyValueType } from '../../types' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { uniqueId } from 'es-toolkit/compat' import { produce } from 'immer' -import { uniqueId } from 'lodash-es' import * as React from 'react' import { useCallback, useMemo } from 'react' import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor' diff --git a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts index b174b7e6de0..650ae471562 100644 --- a/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts +++ b/web/app/components/workflow/nodes/http/hooks/use-key-value-list.ts @@ -1,6 +1,6 @@ import type { KeyValue } from '../types' import { useBoolean } from 'ahooks' -import { uniqueId } from 'lodash-es' +import { uniqueId } from 'es-toolkit/compat' import { useCallback, useEffect, useState } from 'react' const UNIQUE_ID_PREFIX = 'key-value-' diff --git a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx index 29419be0110..9133271161e 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-number-input.tsx @@ -4,7 +4,7 @@ import type { } from '@/app/components/workflow/types' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx index cdcd7561db8..b829ebb0408 100644 --- a/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx +++ b/web/app/components/workflow/nodes/if-else/components/condition-wrap.tsx @@ -7,7 +7,7 @@ import { RiDeleteBinLine, RiDraggable, } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/iteration/use-config.ts b/web/app/components/workflow/nodes/iteration/use-config.ts index 50cee67f81d..31065770859 100644 --- a/web/app/components/workflow/nodes/iteration/use-config.ts +++ b/web/app/components/workflow/nodes/iteration/use-config.ts @@ -2,8 +2,8 @@ import type { ErrorHandleMode, ValueSelector, Var } from '../../types' import type { IterationNodeType } from './types' import type { Item } from '@/app/components/base/select' import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' +import { isEqual } from 'es-toolkit/compat' import { produce } from 'immer' -import { isEqual } from 'lodash-es' import { useCallback } from 'react' import { useAllBuiltInTools, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx index c930387c823..574501a27dc 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/condition-list/condition-value-method.tsx @@ -1,5 +1,5 @@ import { RiArrowDownSLine } from '@remixicon/react' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { useState } from 'react' import Button from '@/app/components/base/button' import { diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx index 9b541b9ea67..8a90a8bf5d5 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/metadata/metadata-filter/index.tsx @@ -1,5 +1,5 @@ import type { MetadataShape } from '@/app/components/workflow/nodes/knowledge-retrieval/types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useState, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx index 02ae01ba166..610aaf5d637 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/components/retrieval-config.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { ModelConfig } from '../../../types' import type { MultipleRetrievalConfig, SingleRetrievalConfig } from '../types' +import type { ModelParameterModalProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import type { DataSet } from '@/models/datasets' import type { DatasetConfigs } from '@/models/debug' import { RiEqualizer2Line } from '@remixicon/react' @@ -28,8 +29,8 @@ type Props = { onRetrievalModeChange: (mode: RETRIEVE_TYPE) => void onMultipleRetrievalConfigChange: (config: MultipleRetrievalConfig) => void singleRetrievalModelConfig?: ModelConfig - onSingleRetrievalModelChange?: (config: ModelConfig) => void - onSingleRetrievalModelParamsChange?: (config: ModelConfig) => void + onSingleRetrievalModelChange?: ModelParameterModalProps['setModel'] + onSingleRetrievalModelParamsChange?: ModelParameterModalProps['onCompletionParamsChange'] readonly?: boolean rerankModalOpen: boolean onRerankModelOpenChange: (open: boolean) => void diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx index ff5a9e22925..1471be9741e 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx +++ b/web/app/components/workflow/nodes/knowledge-retrieval/panel.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import type { KnowledgeRetrievalNodeType } from './types' import type { NodePanelProps } from '@/app/components/workflow/types' -import { intersectionBy } from 'lodash-es' +import { intersectionBy } from 'es-toolkit/compat' import { memo, useMemo, @@ -104,7 +104,7 @@ const Panel: FC> = ({ onRetrievalModeChange={handleRetrievalModeChange} onMultipleRetrievalConfigChange={handleMultipleRetrievalConfigChange} singleRetrievalModelConfig={inputs.single_retrieval_config?.model} - onSingleRetrievalModelChange={handleModelChanged as any} + onSingleRetrievalModelChange={handleModelChanged} onSingleRetrievalModelParamsChange={handleCompletionParamsChange} readonly={readOnly || !selectedDatasets.length} rerankModalOpen={rerankModelOpen} diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts index d0846b3a342..9a63bf96de6 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/use-config.ts @@ -9,8 +9,8 @@ import type { MultipleRetrievalConfig, } from './types' import type { DataSet } from '@/models/datasets' +import { isEqual } from 'es-toolkit/compat' import { produce } from 'immer' -import { isEqual } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts index 12cf8c053ce..d6cd69b39ac 100644 --- a/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts +++ b/web/app/components/workflow/nodes/knowledge-retrieval/utils.ts @@ -6,7 +6,7 @@ import type { import { uniq, xorBy, -} from 'lodash-es' +} from 'es-toolkit/compat' import { DATASET_DEFAULT } from '@/config' import { DEFAULT_WEIGHTED_SCORE, diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx index cfe63159d37..557cddfb61c 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/context.tsx @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext, diff --git a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts index 84c28b236ec..1673c80f4f6 100644 --- a/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts +++ b/web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/hooks.ts @@ -1,8 +1,8 @@ import type { VisualEditorProps } from '.' import type { Field } from '../../../types' import type { EditData } from './edit-card' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import Toast from '@/app/components/base/toast' import { ArrayType, Type } from '../../../types' import { findPropertyWithPath } from '../../../utils' diff --git a/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts index ae500074ff8..f7cb609f0ee 100644 --- a/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/llm/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { LLMNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, PromptItem, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx index 29419be0110..9133271161e 100644 --- a/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx +++ b/web/app/components/workflow/nodes/loop/components/condition-number-input.tsx @@ -4,7 +4,7 @@ import type { } from '@/app/components/workflow/types' import { RiArrowDownSLine } from '@remixicon/react' import { useBoolean } from 'ahooks' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts index abf187d6e50..c53df966882 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/parameter-extractor/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { ParameterExtractorNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx index 2af2f8036a9..a33ba03550a 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-item.tsx @@ -2,7 +2,7 @@ import type { FC } from 'react' import type { Topic } from '../types' import type { ValueSelector, Var } from '@/app/components/workflow/types' -import { uniqueId } from 'lodash-es' +import { uniqueId } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx index 8e61f918a52..8527ce3ad39 100644 --- a/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx +++ b/web/app/components/workflow/nodes/question-classifier/components/class-list.tsx @@ -3,8 +3,8 @@ import type { FC } from 'react' import type { Topic } from '@/app/components/workflow/nodes/question-classifier/types' import type { ValueSelector, Var } from '@/app/components/workflow/types' import { RiDraggable } from '@remixicon/react' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts index 095809eba29..9f2ad5fa39d 100644 --- a/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/question-classifier/use-single-run-form-params.ts @@ -2,7 +2,7 @@ import type { RefObject } from 'react' import type { QuestionClassifierNodeType } from './types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar, Var, Variable } from '@/app/components/workflow/types' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { InputVarType, VarType } from '@/app/components/workflow/types' diff --git a/web/app/components/workflow/nodes/start/components/var-item.tsx b/web/app/components/workflow/nodes/start/components/var-item.tsx index a506c51e31d..83676ed21f1 100644 --- a/web/app/components/workflow/nodes/start/components/var-item.tsx +++ b/web/app/components/workflow/nodes/start/components/var-item.tsx @@ -5,7 +5,7 @@ import { RiDeleteBinLine, } from '@remixicon/react' import { useBoolean, useHover } from 'ahooks' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback, useRef } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/tool/components/copy-id.tsx b/web/app/components/workflow/nodes/tool/components/copy-id.tsx index 8e53970749c..6a608fa0b63 100644 --- a/web/app/components/workflow/nodes/tool/components/copy-id.tsx +++ b/web/app/components/workflow/nodes/tool/components/copy-id.tsx @@ -1,7 +1,7 @@ 'use client' import { RiFileCopyLine } from '@remixicon/react' import copy from 'copy-to-clipboard' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx index 8b1bd46eebd..23f3868c593 100644 --- a/web/app/components/workflow/nodes/tool/components/input-var-list.tsx +++ b/web/app/components/workflow/nodes/tool/components/input-var-list.tsx @@ -4,8 +4,8 @@ import type { ToolVarInputs } from '../types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Tool } from '@/app/components/tools/types' import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx index 19ead7ead15..85ff2def9a9 100644 --- a/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx +++ b/web/app/components/workflow/nodes/variable-assigner/components/var-list/index.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' import type { ValueSelector, Var } from '@/app/components/workflow/types' +import { noop } from 'es-toolkit/compat' import { produce } from 'immer' -import { noop } from 'lodash-es' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/nodes/variable-assigner/hooks.ts b/web/app/components/workflow/nodes/variable-assigner/hooks.ts index b20cee79c7d..5c2fe369225 100644 --- a/web/app/components/workflow/nodes/variable-assigner/hooks.ts +++ b/web/app/components/workflow/nodes/variable-assigner/hooks.ts @@ -7,9 +7,9 @@ import type { VarGroupItem, VariableAssignerNodeType, } from './types' -import { produce } from 'immer' +import { uniqBy } from 'es-toolkit/compat' -import { uniqBy } from 'lodash-es' +import { produce } from 'immer' import { useCallback } from 'react' import { useNodes, diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx index 3f5472ad568..d1b178ec4c2 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx @@ -11,7 +11,7 @@ import { RiLinkUnlinkM, } from '@remixicon/react' import { useClickAway } from 'ahooks' -import { escape } from 'lodash-es' +import { escape } from 'es-toolkit/compat' import { memo, useEffect, diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts index 2c6e014b154..fb191bc05ab 100644 --- a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts @@ -5,11 +5,11 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext import { mergeRegister, } from '@lexical/utils' +import { escape } from 'es-toolkit/compat' import { CLICK_COMMAND, COMMAND_PRIORITY_LOW, } from 'lexical' -import { escape } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx index a43179026ef..9fec4f7d014 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-item.tsx @@ -1,6 +1,6 @@ import type { ConversationVariable } from '@/app/components/workflow/types' import { RiDeleteBinLine, RiEditLine } from '@remixicon/react' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo, useState } from 'react' import { BubbleX } from '@/app/components/base/icons/src/vender/line/others' import { cn } from '@/utils/classnames' diff --git a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx index 6e130180d08..4542e1fb906 100644 --- a/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/conversation-variable-modal.tsx @@ -5,7 +5,7 @@ import type { import { RiCloseLine } from '@remixicon/react' import { useMount } from 'ahooks' import copy from 'copy-to-clipboard' -import { capitalize, noop } from 'lodash-es' +import { capitalize, noop } from 'es-toolkit/compat' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/app/components/workflow/panel/debug-and-preview/hooks.ts b/web/app/components/workflow/panel/debug-and-preview/hooks.ts index 6eb1ea0b767..b771d97006e 100644 --- a/web/app/components/workflow/panel/debug-and-preview/hooks.ts +++ b/web/app/components/workflow/panel/debug-and-preview/hooks.ts @@ -5,8 +5,8 @@ import type { Inputs, } from '@/app/components/base/chat/types' import type { FileEntity } from '@/app/components/base/file-uploader/types' +import { uniqBy } from 'es-toolkit/compat' import { produce, setAutoFreeze } from 'immer' -import { uniqBy } from 'lodash-es' import { useCallback, useEffect, diff --git a/web/app/components/workflow/panel/debug-and-preview/index.tsx b/web/app/components/workflow/panel/debug-and-preview/index.tsx index 3005b68a9ce..2b63cf47515 100644 --- a/web/app/components/workflow/panel/debug-and-preview/index.tsx +++ b/web/app/components/workflow/panel/debug-and-preview/index.tsx @@ -1,7 +1,7 @@ import type { StartNodeType } from '../../nodes/start/types' import { RiCloseLine, RiEqualizer2Line } from '@remixicon/react' -import { debounce, noop } from 'lodash-es' +import { debounce, noop } from 'es-toolkit/compat' import { memo, useCallback, diff --git a/web/app/components/workflow/panel/env-panel/env-item.tsx b/web/app/components/workflow/panel/env-panel/env-item.tsx index 64d6610643e..582539b85b8 100644 --- a/web/app/components/workflow/panel/env-panel/env-item.tsx +++ b/web/app/components/workflow/panel/env-panel/env-item.tsx @@ -1,6 +1,6 @@ import type { EnvironmentVariable } from '@/app/components/workflow/types' import { RiDeleteBinLine, RiEditLine, RiLock2Line } from '@remixicon/react' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo, useState } from 'react' import { Env } from '@/app/components/base/icons/src/vender/line/others' import { useStore } from '@/app/components/workflow/store' diff --git a/web/app/components/workflow/panel/global-variable-panel/item.tsx b/web/app/components/workflow/panel/global-variable-panel/item.tsx index f82579dedbc..458dd276923 100644 --- a/web/app/components/workflow/panel/global-variable-panel/item.tsx +++ b/web/app/components/workflow/panel/global-variable-panel/item.tsx @@ -1,5 +1,5 @@ import type { GlobalVariable } from '@/app/components/workflow/types' -import { capitalize } from 'lodash-es' +import { capitalize } from 'es-toolkit/compat' import { memo } from 'react' import { GlobalVariable as GlobalVariableIcon } from '@/app/components/base/icons/src/vender/line/others' diff --git a/web/app/components/workflow/run/utils/format-log/agent/index.ts b/web/app/components/workflow/run/utils/format-log/agent/index.ts index a4c1ea5167d..f86e4b33bb8 100644 --- a/web/app/components/workflow/run/utils/format-log/agent/index.ts +++ b/web/app/components/workflow/run/utils/format-log/agent/index.ts @@ -1,5 +1,5 @@ import type { AgentLogItem, AgentLogItemWithChildren, NodeTracing } from '@/types/workflow' -import { cloneDeep } from 'lodash-es' +import { cloneDeep } from 'es-toolkit/compat' import { BlockEnum } from '@/app/components/workflow/types' const supportedAgentLogNodes = [BlockEnum.Agent, BlockEnum.Tool] diff --git a/web/app/components/workflow/run/utils/format-log/index.ts b/web/app/components/workflow/run/utils/format-log/index.ts index 2c89e915719..1dbe8f16823 100644 --- a/web/app/components/workflow/run/utils/format-log/index.ts +++ b/web/app/components/workflow/run/utils/format-log/index.ts @@ -1,5 +1,5 @@ import type { NodeTracing } from '@/types/workflow' -import { cloneDeep } from 'lodash-es' +import { cloneDeep } from 'es-toolkit/compat' import { BlockEnum } from '../../../types' import formatAgentNode from './agent' import { addChildrenToIterationNode } from './iteration' diff --git a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts index 855ac4c69d9..8b4416f5299 100644 --- a/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/iteration/index.spec.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import format from '.' import graphToLogStruct from '../graph-to-log-struct' diff --git a/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts b/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts index aee2a432c35..3d31e43ba3e 100644 --- a/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts +++ b/web/app/components/workflow/run/utils/format-log/loop/index.spec.ts @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import format from '.' import graphToLogStruct from '../graph-to-log-struct' diff --git a/web/app/components/workflow/store/workflow/workflow-draft-slice.ts b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts index 83792e84a60..68265a8eba6 100644 --- a/web/app/components/workflow/store/workflow/workflow-draft-slice.ts +++ b/web/app/components/workflow/store/workflow/workflow-draft-slice.ts @@ -5,7 +5,7 @@ import type { EnvironmentVariable, Node, } from '@/app/components/workflow/types' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' type DebouncedFunc = { (fn: () => void): void diff --git a/web/app/components/workflow/utils/elk-layout.ts b/web/app/components/workflow/utils/elk-layout.ts index 05f81872bbc..1a4bbf2d502 100644 --- a/web/app/components/workflow/utils/elk-layout.ts +++ b/web/app/components/workflow/utils/elk-layout.ts @@ -5,7 +5,7 @@ import type { Node, } from '@/app/components/workflow/types' import ELK from 'elkjs/lib/elk.bundled.js' -import { cloneDeep } from 'lodash-es' +import { cloneDeep } from 'es-toolkit/compat' import { CUSTOM_NODE, NODE_LAYOUT_HORIZONTAL_PADDING, diff --git a/web/app/components/workflow/utils/workflow-init.ts b/web/app/components/workflow/utils/workflow-init.ts index 18ba643d30d..fa211934e48 100644 --- a/web/app/components/workflow/utils/workflow-init.ts +++ b/web/app/components/workflow/utils/workflow-init.ts @@ -9,7 +9,7 @@ import type { } from '../types' import { cloneDeep, -} from 'lodash-es' +} from 'es-toolkit/compat' import { getConnectedEdges, } from 'reactflow' diff --git a/web/app/components/workflow/utils/workflow.ts b/web/app/components/workflow/utils/workflow.ts index 43fbd687c12..7fabc51a455 100644 --- a/web/app/components/workflow/utils/workflow.ts +++ b/web/app/components/workflow/utils/workflow.ts @@ -4,7 +4,7 @@ import type { } from '../types' import { uniqBy, -} from 'lodash-es' +} from 'es-toolkit/compat' import { getOutgoers, } from 'reactflow' diff --git a/web/app/components/workflow/variable-inspect/index.tsx b/web/app/components/workflow/variable-inspect/index.tsx index ced7861e004..775c761eca5 100644 --- a/web/app/components/workflow/variable-inspect/index.tsx +++ b/web/app/components/workflow/variable-inspect/index.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react' -import { debounce } from 'lodash-es' +import { debounce } from 'es-toolkit/compat' import { useCallback, useMemo, diff --git a/web/app/components/workflow/workflow-history-store.tsx b/web/app/components/workflow/workflow-history-store.tsx index 502cb733cbc..6729fe50e39 100644 --- a/web/app/components/workflow/workflow-history-store.tsx +++ b/web/app/components/workflow/workflow-history-store.tsx @@ -3,8 +3,8 @@ import type { TemporalState } from 'zundo' import type { StoreApi } from 'zustand' import type { WorkflowHistoryEventT } from './hooks' import type { Edge, Node } from './types' +import { noop } from 'es-toolkit/compat' import isDeepEqual from 'fast-deep-equal' -import { noop } from 'lodash-es' import { createContext, useContext, useMemo, useState } from 'react' import { temporal } from 'zundo' import { create } from 'zustand' diff --git a/web/app/education-apply/education-apply-page.tsx b/web/app/education-apply/education-apply-page.tsx index 5f2446352e4..efd74d42d51 100644 --- a/web/app/education-apply/education-apply-page.tsx +++ b/web/app/education-apply/education-apply-page.tsx @@ -1,7 +1,7 @@ 'use client' import { RiExternalLinkLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useRouter, useSearchParams, diff --git a/web/app/reset-password/page.tsx b/web/app/reset-password/page.tsx index c7e15f8b3fa..479f550ae92 100644 --- a/web/app/reset-password/page.tsx +++ b/web/app/reset-password/page.tsx @@ -1,6 +1,6 @@ 'use client' import { RiArrowLeftLine, RiLockPasswordLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 9ab2d9314c0..2e95ac8663a 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -1,5 +1,5 @@ import type { ResponseError } from '@/service/fetch' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useState } from 'react' diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index e3bbe420bce..c4923959ab6 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -1,7 +1,7 @@ 'use client' import type { Locale } from '@/i18n-config' import { RiAccountCircleLine } from '@remixicon/react' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useRouter, useSearchParams } from 'next/navigation' import { useCallback, useState } from 'react' diff --git a/web/app/signup/components/input-mail.tsx b/web/app/signup/components/input-mail.tsx index b001e1f8b01..26ac99d2c68 100644 --- a/web/app/signup/components/input-mail.tsx +++ b/web/app/signup/components/input-mail.tsx @@ -1,6 +1,6 @@ 'use client' import type { MailSendResponse } from '@/service/use-common' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import Link from 'next/link' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' diff --git a/web/context/app-context.tsx b/web/context/app-context.tsx index b7a47048f31..cb4cab65b77 100644 --- a/web/context/app-context.tsx +++ b/web/context/app-context.tsx @@ -3,7 +3,7 @@ import type { FC, ReactNode } from 'react' import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common' import { useQueryClient } from '@tanstack/react-query' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useCallback, useEffect, useMemo } from 'react' import { createContext, useContext, useContextSelector } from 'use-context-selector' import { setUserId, setUserProperties } from '@/app/components/base/amplitude' diff --git a/web/context/datasets-context.tsx b/web/context/datasets-context.tsx index 4ca7ad311e4..f35767bc215 100644 --- a/web/context/datasets-context.tsx +++ b/web/context/datasets-context.tsx @@ -1,7 +1,7 @@ 'use client' import type { DataSet } from '@/models/datasets' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext } from 'use-context-selector' export type DatasetsContextValue = { diff --git a/web/context/debug-configuration.ts b/web/context/debug-configuration.ts index 51ba4ab6264..2518af62600 100644 --- a/web/context/debug-configuration.ts +++ b/web/context/debug-configuration.ts @@ -22,7 +22,7 @@ import type { TextToSpeechConfig, } from '@/models/debug' import type { VisionSettings } from '@/types/app' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext } from 'use-context-selector' import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING, DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config' import { PromptMode } from '@/models/debug' diff --git a/web/context/explore-context.ts b/web/context/explore-context.ts index 688b9036f9e..1a7b35a09b1 100644 --- a/web/context/explore-context.ts +++ b/web/context/explore-context.ts @@ -1,5 +1,5 @@ import type { InstalledApp } from '@/models/explore' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext } from 'use-context-selector' type IExplore = { diff --git a/web/context/i18n.ts b/web/context/i18n.ts index 773569fa21e..92d66a1b2fb 100644 --- a/web/context/i18n.ts +++ b/web/context/i18n.ts @@ -1,5 +1,5 @@ import type { Locale } from '@/i18n-config' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext, diff --git a/web/context/mitt-context.tsx b/web/context/mitt-context.tsx index 6c6209b5a5e..0fc160613a2 100644 --- a/web/context/mitt-context.tsx +++ b/web/context/mitt-context.tsx @@ -1,4 +1,4 @@ -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { createContext, useContext, useContextSelector } from 'use-context-selector' import { useMitt } from '@/hooks/use-mitt' diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index 2afd1b7b2f0..5b417a64ffd 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -22,7 +22,7 @@ import type { ExternalDataTool, } from '@/models/common' import type { ModerationConfig, PromptVariable } from '@/models/debug' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import dynamic from 'next/dynamic' import { useSearchParams } from 'next/navigation' import { useCallback, useEffect, useState } from 'react' diff --git a/web/context/provider-context.tsx b/web/context/provider-context.tsx index eb2a034f3b8..3394ea20f6a 100644 --- a/web/context/provider-context.tsx +++ b/web/context/provider-context.tsx @@ -5,7 +5,7 @@ import type { Model, ModelProvider } from '@/app/components/header/account-setti import type { RETRIEVE_METHOD } from '@/types/app' import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' -import { noop } from 'lodash-es' +import { noop } from 'es-toolkit/compat' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { createContext, useContext, useContextSelector } from 'use-context-selector' diff --git a/web/i18n-config/i18next-config.ts b/web/i18n-config/i18next-config.ts index e82f5f2acb7..8dce79a5e5d 100644 --- a/web/i18n-config/i18next-config.ts +++ b/web/i18n-config/i18next-config.ts @@ -1,7 +1,7 @@ 'use client' import type { Locale } from '.' +import { camelCase, kebabCase } from 'es-toolkit/compat' import i18n from 'i18next' -import { camelCase, kebabCase } from 'lodash-es' import { initReactI18next } from 'react-i18next' import app from '../i18n/en-US/app' diff --git a/web/i18n-config/server.ts b/web/i18n-config/server.ts index 75a5f3943b3..c92a5a60258 100644 --- a/web/i18n-config/server.ts +++ b/web/i18n-config/server.ts @@ -1,9 +1,9 @@ import type { Locale } from '.' import type { KeyPrefix, Namespace } from './i18next-config' import { match } from '@formatjs/intl-localematcher' +import { camelCase } from 'es-toolkit/compat' import { createInstance } from 'i18next' import resourcesToBackend from 'i18next-resources-to-backend' -import { camelCase } from 'lodash-es' import Negotiator from 'negotiator' import { cookies, headers } from 'next/headers' import { initReactI18next } from 'react-i18next/initReactI18next' diff --git a/web/package.json b/web/package.json index f3cc0958118..a27ac234ad2 100644 --- a/web/package.json +++ b/web/package.json @@ -85,6 +85,7 @@ "echarts-for-react": "^3.0.5", "elkjs": "^0.9.3", "emoji-mart": "^5.6.0", + "es-toolkit": "^1.43.0", "fast-deep-equal": "^3.1.3", "html-entities": "^2.6.0", "html-to-image": "1.11.13", @@ -100,7 +101,6 @@ "lamejs": "^1.2.1", "lexical": "^0.38.2", "line-clamp": "^1.0.0", - "lodash-es": "^4.17.21", "mermaid": "~11.11.0", "mime": "^4.1.0", "mitt": "^3.0.1", @@ -169,7 +169,6 @@ "@testing-library/user-event": "^14.6.1", "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", - "@types/lodash-es": "^4.17.12", "@types/negotiator": "^0.6.4", "@types/node": "18.15.0", "@types/qs": "^6.14.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 24e710534f0..51f421e90bc 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: emoji-mart: specifier: ^5.6.0 version: 5.6.0 + es-toolkit: + specifier: ^1.43.0 + version: 1.43.0 fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -219,9 +222,6 @@ importers: line-clamp: specifier: ^1.0.0 version: 1.0.0 - lodash-es: - specifier: ^4.17.21 - version: 4.17.21 mermaid: specifier: ~11.11.0 version: 11.11.0 @@ -421,9 +421,6 @@ importers: '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 - '@types/lodash-es': - specifier: ^4.17.12 - version: 4.17.12 '@types/negotiator': specifier: ^0.6.4 version: 0.6.4 @@ -3575,12 +3572,6 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/lodash-es@4.17.12': - resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} - - '@types/lodash@4.17.21': - resolution: {integrity: sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5042,6 +5033,9 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-toolkit@1.43.0: + resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -12077,12 +12071,6 @@ snapshots: dependencies: '@types/node': 18.15.0 - '@types/lodash-es@4.17.12': - dependencies: - '@types/lodash': 4.17.21 - - '@types/lodash@4.17.21': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -13685,6 +13673,8 @@ snapshots: es-module-lexer@1.7.0: {} + es-toolkit@1.43.0: {} + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 diff --git a/web/service/knowledge/use-create-dataset.ts b/web/service/knowledge/use-create-dataset.ts index eb656c2994d..a0d55eeb99f 100644 --- a/web/service/knowledge/use-create-dataset.ts +++ b/web/service/knowledge/use-create-dataset.ts @@ -18,7 +18,7 @@ import type { ProcessRuleResponse, } from '@/models/datasets' import { useMutation } from '@tanstack/react-query' -import groupBy from 'lodash-es/groupBy' +import { groupBy } from 'es-toolkit/compat' import { post } from '../base' import { createDocument, createFirstDocument, fetchDefaultProcessRule, fetchFileIndexingEstimate } from '../datasets' diff --git a/web/service/use-flow.ts b/web/service/use-flow.ts index 30bec6dd23e..74aa78ec106 100644 --- a/web/service/use-flow.ts +++ b/web/service/use-flow.ts @@ -1,5 +1,5 @@ import type { FlowType } from '@/types/common' -import { curry } from 'lodash-es' +import { curry } from 'es-toolkit/compat' import { useDeleteAllInspectorVars as useDeleteAllInspectorVarsInner, useDeleteInspectVar as useDeleteInspectVarInner, diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 58454125edc..32ea4f35fd2 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -33,7 +33,7 @@ import { useQuery, useQueryClient, } from '@tanstack/react-query' -import { cloneDeep } from 'lodash-es' +import { cloneDeep } from 'es-toolkit/compat' import { useCallback, useEffect, useState } from 'react' import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list' import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils' diff --git a/web/utils/index.ts b/web/utils/index.ts index 263d4154795..ebb8b906454 100644 --- a/web/utils/index.ts +++ b/web/utils/index.ts @@ -1,4 +1,4 @@ -import { escape } from 'lodash-es' +import { escape } from 'es-toolkit/compat' export const sleep = (ms: number) => { return new Promise(resolve => setTimeout(resolve, ms)) From 9000fa1a8821bd6fed3291646ffee75b08d67aab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=80=89=E5=B6=8B=20=E5=B0=86=E7=9F=A2?= <66870512+krap730@users.noreply.github.com> Date: Thu, 25 Dec 2025 11:19:50 +0900 Subject: [PATCH 13/17] fix: handle list content type in Parameter Extraction node (#30070) --- .../nodes/parameter_extractor/parameter_extractor_node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py index 93db417b155..08e0542d61b 100644 --- a/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py +++ b/api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py @@ -281,7 +281,7 @@ class ParameterExtractorNode(Node[ParameterExtractorNodeData]): # handle invoke result - text = invoke_result.message.content or "" + text = invoke_result.message.get_text_content() if not isinstance(text, str): raise InvalidTextContentTypeError(f"Invalid text content type: {type(text)}. Expected str.") From a26b2d74d4c2ef1cc481ca7e5b14335808cbd80b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=80=89=E5=B6=8B=20=E5=B0=86=E7=9F=A2?= <66870512+krap730@users.noreply.github.com> Date: Thu, 25 Dec 2025 11:20:25 +0900 Subject: [PATCH 14/17] fix: allow None values in VariableMessage validation (#30082) --- api/core/tools/entities/tool_entities.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/core/tools/entities/tool_entities.py b/api/core/tools/entities/tool_entities.py index 353f3a646a9..583a3584f79 100644 --- a/api/core/tools/entities/tool_entities.py +++ b/api/core/tools/entities/tool_entities.py @@ -153,11 +153,11 @@ class ToolInvokeMessage(BaseModel): @classmethod def transform_variable_value(cls, values): """ - Only basic types and lists are allowed. + Only basic types, lists, and None are allowed. """ value = values.get("variable_value") - if not isinstance(value, dict | list | str | int | float | bool): - raise ValueError("Only basic types and lists are allowed.") + if value is not None and not isinstance(value, dict | list | str | int | float | bool): + raise ValueError("Only basic types, lists, and None are allowed.") # if stream is true, the value must be a string if values.get("stream"): From 29e7e822d73a88c05d6e5f9f636a599426ee6761 Mon Sep 17 00:00:00 2001 From: Shemol Date: Thu, 25 Dec 2025 10:40:04 +0800 Subject: [PATCH 15/17] test: Add comprehensive test suite for Chip component (#30119) Signed-off-by: SherlockShemol Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com> --- web/app/components/base/chip/index.spec.tsx | 394 ++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 web/app/components/base/chip/index.spec.tsx diff --git a/web/app/components/base/chip/index.spec.tsx b/web/app/components/base/chip/index.spec.tsx new file mode 100644 index 00000000000..c19cc77b396 --- /dev/null +++ b/web/app/components/base/chip/index.spec.tsx @@ -0,0 +1,394 @@ +import type { Item } from './index' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import Chip from './index' + +afterEach(cleanup) + +// Test data factory +const createTestItems = (): Item[] => [ + { value: 'all', name: 'All Items' }, + { value: 'active', name: 'Active' }, + { value: 'archived', name: 'Archived' }, +] + +describe('Chip', () => { + // Shared test props + let items: Item[] + let onSelect: (item: Item) => void + let onClear: () => void + + beforeEach(() => { + vi.clearAllMocks() + items = createTestItems() + onSelect = vi.fn() + onClear = vi.fn() + }) + + // Helper function to render Chip with default props + const renderChip = (props: Partial> = {}) => { + return render( + , + ) + } + + // Helper function to get the trigger element + const getTrigger = (container: HTMLElement) => { + return container.querySelector('[data-state]') + } + + // Helper function to open dropdown panel + const openPanel = (container: HTMLElement) => { + const trigger = getTrigger(container) + if (trigger) + fireEvent.click(trigger) + } + + describe('Rendering', () => { + it('should render without crashing', () => { + renderChip() + + expect(screen.getByText('All Items')).toBeInTheDocument() + }) + + it('should display current selected item name', () => { + renderChip({ value: 'active' }) + + expect(screen.getByText('Active')).toBeInTheDocument() + }) + + it('should display empty content when value does not match any item', () => { + const { container } = renderChip({ value: 'nonexistent' }) + + // When value doesn't match, no text should be displayed in trigger + const trigger = getTrigger(container) + // Check that there's no item name text (only icons should be present) + expect(trigger?.textContent?.trim()).toBeFalsy() + }) + }) + + describe('Props', () => { + it('should update displayed item name when value prop changes', () => { + const { rerender } = renderChip({ value: 'all' }) + expect(screen.getByText('All Items')).toBeInTheDocument() + + rerender( + , + ) + expect(screen.getByText('Archived')).toBeInTheDocument() + }) + + it('should show left icon by default', () => { + const { container } = renderChip() + + // The filter icon should be visible + const svg = container.querySelector('svg') + expect(svg).toBeInTheDocument() + }) + + it('should hide left icon when showLeftIcon is false', () => { + renderChip({ showLeftIcon: false }) + + // When showLeftIcon is false, there should be no filter icon before the text + const textElement = screen.getByText('All Items') + const parent = textElement.closest('div[data-state]') + const icons = parent?.querySelectorAll('svg') + + // Should only have the arrow icon, not the filter icon + expect(icons?.length).toBe(1) + }) + + it('should render custom left icon', () => { + const CustomIcon = () => ★ + + renderChip({ leftIcon: }) + + expect(screen.getByTestId('custom-icon')).toBeInTheDocument() + }) + + it('should apply custom className to trigger', () => { + const customClass = 'custom-chip-class' + + const { container } = renderChip({ className: customClass }) + + const chipElement = container.querySelector(`.${customClass}`) + expect(chipElement).toBeInTheDocument() + }) + + it('should apply custom panelClassName to dropdown panel', () => { + const customPanelClass = 'custom-panel-class' + + const { container } = renderChip({ panelClassName: customPanelClass }) + openPanel(container) + + // Panel is rendered in a portal, so check document.body + const panel = document.body.querySelector(`.${customPanelClass}`) + expect(panel).toBeInTheDocument() + }) + }) + + describe('State Management', () => { + it('should toggle dropdown panel on trigger click', () => { + const { container } = renderChip() + + // Initially closed - check data-state attribute + const trigger = getTrigger(container) + expect(trigger).toHaveAttribute('data-state', 'closed') + + // Open panel + openPanel(container) + expect(trigger).toHaveAttribute('data-state', 'open') + // Panel items should be visible + expect(screen.getAllByText('All Items').length).toBeGreaterThan(1) + + // Close panel + if (trigger) + fireEvent.click(trigger) + expect(trigger).toHaveAttribute('data-state', 'closed') + }) + + it('should close panel after selecting an item', () => { + const { container } = renderChip() + + openPanel(container) + const trigger = getTrigger(container) + expect(trigger).toHaveAttribute('data-state', 'open') + + // Click on an item in the dropdown panel + const activeItems = screen.getAllByText('Active') + // The second one should be in the dropdown + fireEvent.click(activeItems[activeItems.length - 1]) + + expect(trigger).toHaveAttribute('data-state', 'closed') + }) + }) + + describe('Event Handlers', () => { + it('should call onSelect with correct item when item is clicked', () => { + const { container } = renderChip() + + openPanel(container) + // Get all "Active" texts and click the one in the dropdown (should be the last one) + const activeItems = screen.getAllByText('Active') + fireEvent.click(activeItems[activeItems.length - 1]) + + expect(onSelect).toHaveBeenCalledTimes(1) + expect(onSelect).toHaveBeenCalledWith(items[1]) + }) + + it('should call onClear when clear button is clicked', () => { + const { container } = renderChip({ value: 'active' }) + + // Find the close icon (last SVG in the trigger) and click its parent + const trigger = getTrigger(container) + const svgs = trigger?.querySelectorAll('svg') + // The close icon should be the last SVG element + const closeIcon = svgs?.[svgs.length - 1] + const clearButton = closeIcon?.parentElement + + expect(clearButton).toBeInTheDocument() + if (clearButton) + fireEvent.click(clearButton) + + expect(onClear).toHaveBeenCalledTimes(1) + }) + + it('should stop event propagation when clear button is clicked', () => { + const { container } = renderChip({ value: 'active' }) + + const trigger = getTrigger(container) + expect(trigger).toHaveAttribute('data-state', 'closed') + + // Find the close icon (last SVG) and click its parent + const svgs = trigger?.querySelectorAll('svg') + const closeIcon = svgs?.[svgs.length - 1] + const clearButton = closeIcon?.parentElement + + if (clearButton) + fireEvent.click(clearButton) + + // Panel should remain closed + expect(trigger).toHaveAttribute('data-state', 'closed') + expect(onClear).toHaveBeenCalledTimes(1) + }) + + it('should handle multiple rapid clicks on trigger', () => { + const { container } = renderChip() + + const trigger = getTrigger(container) + + // Click 1: open + if (trigger) + fireEvent.click(trigger) + expect(trigger).toHaveAttribute('data-state', 'open') + + // Click 2: close + if (trigger) + fireEvent.click(trigger) + expect(trigger).toHaveAttribute('data-state', 'closed') + + // Click 3: open again + if (trigger) + fireEvent.click(trigger) + expect(trigger).toHaveAttribute('data-state', 'open') + }) + }) + + describe('Conditional Rendering', () => { + it('should show arrow down icon when no value is selected', () => { + const { container } = renderChip({ value: '' }) + + // Should have SVG icons (filter icon and arrow down icon) + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + + it('should show clear button when value is selected', () => { + const { container } = renderChip({ value: 'active' }) + + // When value is selected, there should be an icon (the close icon) + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + + it('should not show clear button when no value is selected', () => { + const { container } = renderChip({ value: '' }) + + const trigger = getTrigger(container) + + // When value is empty, the trigger should only have 2 SVGs (filter icon + arrow) + // When value is selected, it would have 2 SVGs (filter icon + close icon) + const svgs = trigger?.querySelectorAll('svg') + // Arrow icon should be present, close icon should not + expect(svgs?.length).toBe(2) + + // Verify onClear hasn't been called + expect(onClear).not.toHaveBeenCalled() + }) + + it('should show dropdown content only when panel is open', () => { + const { container } = renderChip() + + const trigger = getTrigger(container) + + // Closed by default + expect(trigger).toHaveAttribute('data-state', 'closed') + + openPanel(container) + expect(trigger).toHaveAttribute('data-state', 'open') + // Items should be duplicated (once in trigger, once in panel) + expect(screen.getAllByText('All Items').length).toBeGreaterThan(1) + }) + + it('should show check icon on selected item in dropdown', () => { + const { container } = renderChip({ value: 'active' }) + + openPanel(container) + + // Find the dropdown panel items + const allActiveTexts = screen.getAllByText('Active') + // The dropdown item should be the last one + const dropdownItem = allActiveTexts[allActiveTexts.length - 1] + const parentContainer = dropdownItem.parentElement + + // The check icon should be a sibling within the parent + const checkIcon = parentContainer?.querySelector('svg') + expect(checkIcon).toBeInTheDocument() + }) + + it('should render all items in dropdown when open', () => { + const { container } = renderChip() + + openPanel(container) + + // Each item should appear at least twice (once in potential selected state, once in dropdown) + // Use getAllByText to handle multiple occurrences + expect(screen.getAllByText('All Items').length).toBeGreaterThan(0) + expect(screen.getAllByText('Active').length).toBeGreaterThan(0) + expect(screen.getAllByText('Archived').length).toBeGreaterThan(0) + }) + }) + + describe('Edge Cases', () => { + it('should handle empty items array', () => { + const { container } = renderChip({ items: [], value: '' }) + + // Trigger should still render + const trigger = container.querySelector('[data-state]') + expect(trigger).toBeInTheDocument() + }) + + it('should handle value not in items list', () => { + const { container } = renderChip({ value: 'nonexistent' }) + + const trigger = getTrigger(container) + expect(trigger).toBeInTheDocument() + + // The trigger should not display any item name text + expect(trigger?.textContent?.trim()).toBeFalsy() + }) + + it('should allow selecting already selected item', () => { + const { container } = renderChip({ value: 'active' }) + + openPanel(container) + + // Click on the already selected item in the dropdown + const activeItems = screen.getAllByText('Active') + fireEvent.click(activeItems[activeItems.length - 1]) + + expect(onSelect).toHaveBeenCalledTimes(1) + expect(onSelect).toHaveBeenCalledWith(items[1]) + }) + + it('should handle numeric values', () => { + const numericItems: Item[] = [ + { value: 1, name: 'First' }, + { value: 2, name: 'Second' }, + { value: 3, name: 'Third' }, + ] + + const { container } = renderChip({ value: 2, items: numericItems }) + + expect(screen.getByText('Second')).toBeInTheDocument() + + // Open panel and select Third + openPanel(container) + + const thirdItems = screen.getAllByText('Third') + fireEvent.click(thirdItems[thirdItems.length - 1]) + + expect(onSelect).toHaveBeenCalledWith(numericItems[2]) + }) + + it('should handle items with additional properties', () => { + const itemsWithExtra: Item[] = [ + { value: 'a', name: 'Item A', customProp: 'extra1' }, + { value: 'b', name: 'Item B', customProp: 'extra2' }, + ] + + const { container } = renderChip({ value: 'a', items: itemsWithExtra }) + + expect(screen.getByText('Item A')).toBeInTheDocument() + + // Open panel and select Item B + openPanel(container) + + const itemBs = screen.getAllByText('Item B') + fireEvent.click(itemBs[itemBs.length - 1]) + + expect(onSelect).toHaveBeenCalledWith(itemsWithExtra[1]) + }) + }) +}) From d3b7d06be456b26508ffa4505a77beaa09145d16 Mon Sep 17 00:00:00 2001 From: -LAN- Date: Thu, 25 Dec 2025 11:22:54 +0800 Subject: [PATCH 16/17] ci: generate docker compose in autofix (#30105) --- .github/workflows/autofix.yml | 16 ++++++++++++++++ .github/workflows/style.yml | 30 ------------------------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/.github/workflows/autofix.yml b/.github/workflows/autofix.yml index dbced47988f..97027c22182 100644 --- a/.github/workflows/autofix.yml +++ b/.github/workflows/autofix.yml @@ -13,12 +13,28 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + + - name: Check Docker Compose inputs + id: docker-compose-changes + uses: tj-actions/changed-files@v46 + with: + files: | + docker/generate_docker_compose + docker/.env.example + docker/docker-compose-template.yaml + docker/docker-compose.yaml - uses: actions/setup-python@v5 with: python-version: "3.11" - uses: astral-sh/setup-uv@v6 + - name: Generate Docker Compose + if: steps.docker-compose-changes.outputs.any_changed == 'true' + run: | + cd docker + ./generate_docker_compose + - run: | cd api uv sync --dev diff --git a/.github/workflows/style.yml b/.github/workflows/style.yml index 2fb8121f74d..8710f422fc4 100644 --- a/.github/workflows/style.yml +++ b/.github/workflows/style.yml @@ -108,36 +108,6 @@ jobs: working-directory: ./web run: pnpm run type-check:tsgo - docker-compose-template: - name: Docker Compose Template - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Check changed files - id: changed-files - uses: tj-actions/changed-files@v46 - with: - files: | - docker/generate_docker_compose - docker/.env.example - docker/docker-compose-template.yaml - docker/docker-compose.yaml - - - name: Generate Docker Compose - if: steps.changed-files.outputs.any_changed == 'true' - run: | - cd docker - ./generate_docker_compose - - - name: Check for changes - if: steps.changed-files.outputs.any_changed == 'true' - run: git diff --exit-code - superlinter: name: SuperLinter runs-on: ubuntu-latest From e6e439f54c96a7cc517485b3525337f9bd4cff3d Mon Sep 17 00:00:00 2001 From: Shemol Date: Thu, 25 Dec 2025 11:25:21 +0800 Subject: [PATCH 17/17] feat(web): add unit tests for Badge component (#30096) Signed-off-by: SherlockShemol --- web/app/components/base/badge/index.spec.tsx | 360 +++++++++++++++++++ 1 file changed, 360 insertions(+) create mode 100644 web/app/components/base/badge/index.spec.tsx diff --git a/web/app/components/base/badge/index.spec.tsx b/web/app/components/base/badge/index.spec.tsx new file mode 100644 index 00000000000..74162841cf9 --- /dev/null +++ b/web/app/components/base/badge/index.spec.tsx @@ -0,0 +1,360 @@ +import { fireEvent, render, screen } from '@testing-library/react' +import Badge, { BadgeState, BadgeVariants } from './index' + +describe('Badge', () => { + describe('Rendering', () => { + it('should render as a div element with badge class', () => { + render(Test Badge) + + const badge = screen.getByText('Test Badge') + expect(badge).toHaveClass('badge') + expect(badge.tagName).toBe('DIV') + }) + + it.each([ + { children: undefined, label: 'no children' }, + { children: '', label: 'empty string' }, + ])('should render correctly when provided $label', ({ children }) => { + const { container } = render({children}) + + expect(container.firstChild).toHaveClass('badge') + }) + + it('should render React Node children correctly', () => { + render( + + 🔔 + , + ) + + expect(screen.getByTestId('badge-with-icon')).toBeInTheDocument() + expect(screen.getByTestId('custom-icon')).toBeInTheDocument() + }) + }) + + describe('size prop', () => { + it.each([ + { size: undefined, label: 'medium (default)' }, + { size: 's', label: 'small' }, + { size: 'm', label: 'medium' }, + { size: 'l', label: 'large' }, + ] as const)('should render with $label size', ({ size }) => { + render(Test) + + const expectedSize = size || 'm' + expect(screen.getByText('Test')).toHaveClass('badge', `badge-${expectedSize}`) + }) + }) + + describe('state prop', () => { + it.each([ + { state: BadgeState.Warning, label: 'warning', expectedClass: 'badge-warning' }, + { state: BadgeState.Accent, label: 'accent', expectedClass: 'badge-accent' }, + ])('should render with $label state', ({ state, expectedClass }) => { + render(State Test) + + expect(screen.getByText('State Test')).toHaveClass(expectedClass) + }) + + it.each([ + { state: undefined, label: 'default (undefined)' }, + { state: BadgeState.Default, label: 'default (explicit)' }, + ])('should use default styles when state is $label', ({ state }) => { + render(State Test) + + const badge = screen.getByText('State Test') + expect(badge).not.toHaveClass('badge-warning', 'badge-accent') + }) + }) + + describe('iconOnly prop', () => { + it.each([ + { size: 's', iconOnly: false, label: 'small with text' }, + { size: 's', iconOnly: true, label: 'small icon-only' }, + { size: 'm', iconOnly: false, label: 'medium with text' }, + { size: 'm', iconOnly: true, label: 'medium icon-only' }, + { size: 'l', iconOnly: false, label: 'large with text' }, + { size: 'l', iconOnly: true, label: 'large icon-only' }, + ] as const)('should render correctly for $label', ({ size, iconOnly }) => { + const { container } = render(🔔) + const badge = screen.getByText('🔔') + + // Verify badge renders with correct size + expect(badge).toHaveClass('badge', `badge-${size}`) + + // Verify the badge is in the DOM and contains the content + expect(badge).toBeInTheDocument() + expect(container.firstChild).toBe(badge) + }) + + it('should apply icon-only padding when iconOnly is true', () => { + render(🔔) + + // When iconOnly is true, the badge should have uniform padding (all sides equal) + const badge = screen.getByText('🔔') + expect(badge).toHaveClass('p-1') + }) + + it('should apply asymmetric padding when iconOnly is false', () => { + render(Badge) + + // When iconOnly is false, the badge should have different horizontal and vertical padding + const badge = screen.getByText('Badge') + expect(badge).toHaveClass('px-[5px]', 'py-[2px]') + }) + }) + + describe('uppercase prop', () => { + it.each([ + { uppercase: undefined, label: 'default (undefined)', expected: 'system-2xs-medium' }, + { uppercase: false, label: 'explicitly false', expected: 'system-2xs-medium' }, + { uppercase: true, label: 'true', expected: 'system-2xs-medium-uppercase' }, + ])('should apply $expected class when uppercase is $label', ({ uppercase, expected }) => { + render(Text) + + expect(screen.getByText('Text')).toHaveClass(expected) + }) + }) + + describe('styleCss prop', () => { + it('should apply custom inline styles correctly', () => { + const customStyles = { + backgroundColor: 'rgb(0, 0, 255)', + color: 'rgb(255, 255, 255)', + padding: '10px', + } + render(Styled Badge) + + expect(screen.getByText('Styled Badge')).toHaveStyle(customStyles) + }) + + it('should apply inline styles without overriding core classes', () => { + render(Custom) + + const badge = screen.getByText('Custom') + expect(badge).toHaveStyle({ backgroundColor: 'rgb(255, 0, 0)', margin: '5px' }) + expect(badge).toHaveClass('badge') + }) + }) + + describe('className prop', () => { + it.each([ + { + props: { className: 'custom-badge' }, + expected: ['badge', 'custom-badge'], + label: 'single custom class', + }, + { + props: { className: 'custom-class another-class', size: 'l' as const }, + expected: ['badge', 'badge-l', 'custom-class', 'another-class'], + label: 'multiple classes with size variant', + }, + ])('should merge $label with default classes', ({ props, expected }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveClass(...expected) + }) + }) + + describe('HTML attributes passthrough', () => { + it.each([ + { attr: 'data-testid', value: 'custom-badge-id', label: 'data attribute' }, + { attr: 'id', value: 'unique-badge', label: 'id attribute' }, + { attr: 'aria-label', value: 'Notification badge', label: 'aria-label' }, + { attr: 'title', value: 'Hover tooltip', label: 'title attribute' }, + { attr: 'role', value: 'status', label: 'ARIA role' }, + ])('should pass through $label correctly', ({ attr, value }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveAttribute(attr, value) + }) + + it('should support multiple HTML attributes simultaneously', () => { + render( + + Test + , + ) + + const badge = screen.getByTestId('multi-attr-badge') + expect(badge).toHaveAttribute('id', 'badge-123') + expect(badge).toHaveAttribute('aria-label', 'Status indicator') + expect(badge).toHaveAttribute('title', 'Current status') + }) + }) + + describe('Event handlers', () => { + it.each([ + { handler: 'onClick', trigger: fireEvent.click, label: 'click' }, + { handler: 'onMouseEnter', trigger: fireEvent.mouseEnter, label: 'mouse enter' }, + { handler: 'onMouseLeave', trigger: fireEvent.mouseLeave, label: 'mouse leave' }, + ])('should trigger $handler when $label occurs', ({ handler, trigger }) => { + const mockHandler = vi.fn() + render(Badge) + + trigger(screen.getByText('Badge')) + + expect(mockHandler).toHaveBeenCalledTimes(1) + }) + + it('should handle user interaction flow with multiple events', () => { + const handlers = { + onClick: vi.fn(), + onMouseEnter: vi.fn(), + onMouseLeave: vi.fn(), + } + render(Interactive) + + const badge = screen.getByText('Interactive') + fireEvent.mouseEnter(badge) + fireEvent.click(badge) + fireEvent.mouseLeave(badge) + + expect(handlers.onMouseEnter).toHaveBeenCalledTimes(1) + expect(handlers.onClick).toHaveBeenCalledTimes(1) + expect(handlers.onMouseLeave).toHaveBeenCalledTimes(1) + }) + + it('should pass event object to handler with correct properties', () => { + const handleClick = vi.fn() + render(Event Badge) + + fireEvent.click(screen.getByText('Event Badge')) + + expect(handleClick).toHaveBeenCalledWith(expect.objectContaining({ + type: 'click', + })) + }) + }) + + describe('Combined props', () => { + it('should correctly apply all props when used together', () => { + render( + + Full Featured + , + ) + + const badge = screen.getByTestId('combined-badge') + expect(badge).toHaveClass('badge', 'badge-l', 'badge-warning', 'system-2xs-medium-uppercase', 'custom-badge') + expect(badge).toHaveStyle({ backgroundColor: 'rgb(0, 0, 255)' }) + expect(badge).toHaveTextContent('Full Featured') + }) + + it.each([ + { + props: { size: 'l' as const, state: BadgeState.Accent }, + expected: ['badge', 'badge-l', 'badge-accent'], + label: 'size and state variants', + }, + { + props: { iconOnly: true, uppercase: true }, + expected: ['badge', 'system-2xs-medium-uppercase'], + label: 'iconOnly and uppercase', + }, + ])('should combine $label correctly', ({ props, expected }) => { + render(Test) + + expect(screen.getByText('Test')).toHaveClass(...expected) + }) + + it('should handle event handlers with combined props', () => { + const handleClick = vi.fn() + render( + + Test + , + ) + + const badge = screen.getByText('Test') + expect(badge).toHaveClass('badge', 'badge-s', 'badge-warning', 'interactive') + + fireEvent.click(badge) + expect(handleClick).toHaveBeenCalledTimes(1) + }) + }) + + describe('Edge cases', () => { + it.each([ + { children: 42, text: '42', label: 'numeric value' }, + { children: 0, text: '0', label: 'zero' }, + ])('should render $label correctly', ({ children, text }) => { + render({children}) + + expect(screen.getByText(text)).toBeInTheDocument() + }) + + it.each([ + { children: null, label: 'null' }, + { children: false, label: 'boolean false' }, + ])('should handle $label children without errors', ({ children }) => { + const { container } = render({children}) + + expect(container.firstChild).toHaveClass('badge') + }) + + it('should render complex nested content correctly', () => { + render( + + 🔔 + 5 + , + ) + + expect(screen.getByTestId('icon')).toBeInTheDocument() + expect(screen.getByTestId('count')).toBeInTheDocument() + }) + }) + + describe('Component metadata and exports', () => { + it('should have correct displayName for debugging', () => { + expect(Badge.displayName).toBe('Badge') + }) + + describe('BadgeState enum', () => { + it.each([ + { key: 'Warning', value: 'warning' }, + { key: 'Accent', value: 'accent' }, + { key: 'Default', value: '' }, + ])('should export $key state with value "$value"', ({ key, value }) => { + expect(BadgeState[key as keyof typeof BadgeState]).toBe(value) + }) + }) + + describe('BadgeVariants utility', () => { + it('should be a function', () => { + expect(typeof BadgeVariants).toBe('function') + }) + + it('should generate base badge class with default medium size', () => { + const result = BadgeVariants({}) + + expect(result).toContain('badge') + expect(result).toContain('badge-m') + }) + + it.each([ + { size: 's' }, + { size: 'm' }, + { size: 'l' }, + ] as const)('should generate correct classes for size=$size', ({ size }) => { + const result = BadgeVariants({ size }) + + expect(result).toContain('badge') + expect(result).toContain(`badge-${size}`) + }) + }) + }) +})