From 6736f4005b31bd8ec4eac7a47935a2e1c1fa0a01 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Thu, 23 Jul 2026 10:26:59 +0800 Subject: [PATCH] fix(web): improve import DSL dialog states (#39432) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- oxlint-suppressions.json | 7 +- .../__tests__/index.spec.tsx | 207 ++++-- .../__tests__/uploader.spec.tsx | 89 ++- .../dsl-confirm-modal.tsx | 17 +- .../app/create-from-dsl-modal/index.tsx | 590 +++++++++--------- .../app/create-from-dsl-modal/uploader.tsx | 167 +++-- .../base/icons/src/public/files/Yaml.json | 181 ------ .../base/icons/src/public/files/Yaml.tsx | 18 - .../base/icons/src/public/files/index.ts | 1 - .../__tests__/update-dsl-modal.spec.tsx | 2 +- .../components/update-dsl-modal.tsx | 2 +- .../import-snippet-dsl-dialog.spec.tsx | 2 +- .../snippets/import-snippet-dsl-dialog.tsx | 2 +- .../__tests__/update-dsl-modal.spec.tsx | 2 +- .../components/workflow/update-dsl-modal.tsx | 2 +- .../create-guide/ui/source-step.tsx | 2 +- .../create-release/ui/source-section.tsx | 2 +- web/utils/dsl-import-warning.ts | 4 +- 18 files changed, 663 insertions(+), 634 deletions(-) delete mode 100644 web/app/components/base/icons/src/public/files/Yaml.json delete mode 100644 web/app/components/base/icons/src/public/files/Yaml.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index e8696cf7d38..fc02eaf6753 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -663,11 +663,6 @@ "count": 1 } }, - "web/app/components/app/create-from-dsl-modal/index.tsx": { - "eslint-react/set-state-in-effect": { - "count": 1 - } - }, "web/app/components/app/duplicate-modal/index.tsx": { "no-restricted-imports": { "count": 1 @@ -1432,7 +1427,7 @@ }, "web/app/components/base/icons/src/public/files/index.ts": { "no-barrel-files/no-barrel-files": { - "count": 11 + "count": 10 } }, "web/app/components/base/icons/src/public/knowledge/dataset-card/index.ts": { diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index 968e20e1c34..b7f21fc4f27 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ /* oxlint-disable typescript/no-explicit-any */ import { act, fireEvent, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import { NEED_REFRESH_APP_LIST_KEY } from '@/app/components/apps/storage' import { DSLImportMode, DSLImportStatus } from '@/models/app' import { renderWithConsoleQuery as render } from '@/test/console/query-data' @@ -62,10 +63,40 @@ vi.mock('@/utils/create-app-tracking', () => ({ trackCreateApp: (...args: unknown[]) => mockTrackCreateApp(...args), })) -vi.mock('@/service/apps', () => ({ - importDSL: (...args: unknown[]) => mockImportDSL(...args), - importDSLConfirm: (...args: unknown[]) => mockImportDSLConfirm(...args), -})) +vi.mock('@/service/client', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + consoleClient: { + ...actual.consoleClient, + apps: { + ...actual.consoleClient.apps, + imports: { + post: ({ body }: { body: Record }) => mockImportDSL(body), + }, + }, + }, + consoleQuery: { + ...actual.consoleQuery, + systemFeatures: actual.consoleQuery.systemFeatures, + apps: { + ...actual.consoleQuery.apps, + imports: { + byImportId: { + confirm: { + post: { + mutationOptions: () => ({ + mutationFn: ({ params }: { params: { import_id: string } }) => + mockImportDSLConfirm({ import_id: params.import_id }), + }), + }, + }, + }, + }, + }, + }, + } +}) vi.mock('@/service/use-apps', () => ({ useInvalidateAppList: () => mockInvalidateAppList, })) @@ -135,20 +166,10 @@ describe('CreateFromDSLModal', () => { mockWorkspacePermissionKeys = ['app.create_and_management'] localStorage.clear() - class MockFileReader { - result = 'app: demo' - onload: ((event: { target: { result: string } }) => void) | null = null - readAsText() { - this.onload?.({ target: { result: this.result } }) - } - } - - // @ts-expect-error test-only file reader shim - globalThis.FileReader = MockFileReader - }) - - afterEach(() => { - vi.useRealTimers() + Object.defineProperty(File.prototype, 'text', { + configurable: true, + value: vi.fn().mockResolvedValue('app: demo'), + }) }) const getCreateButton = () => screen.getByRole('button', { name: /newApp\.Create/i }) @@ -190,6 +211,55 @@ describe('CreateFromDSLModal', () => { expect(handleClose).toHaveBeenCalledTimes(1) }) + it('should expose the URL as a named form field', () => { + render() + + const urlInput = screen.getByRole('textbox', { + name: /(?:^|\.)importFromDSLUrl(?=$|:)/, + }) + + expect(urlInput).toHaveAttribute('name', 'dslUrl') + expect(urlInput).toHaveAttribute('type', 'url') + expect(urlInput.closest('form')).toBeInTheDocument() + }) + + it('should initially focus Browse when the file import dialog opens', async () => { + render() + + await waitFor(() => { + expect( + screen.getByRole('button', { name: /(?:^|\.)dslUploader\.browse(?=$|:)/ }), + ).toHaveFocus() + }) + }) + + it('should move focus from each active tab directly to its first panel control', async () => { + const user = userEvent.setup() + render() + + const fileTab = screen.getByRole('tab', { + name: /(?:^|\.)importFromDSLFile(?=$|:)/, + }) + const browseButton = screen.getByRole('button', { + name: /(?:^|\.)dslUploader\.browse(?=$|:)/, + }) + + await user.click(fileTab) + await user.tab() + expect(browseButton).toHaveFocus() + + const urlTab = screen.getByRole('tab', { + name: /(?:^|\.)importFromDSLUrl(?=$|:)/, + }) + await user.click(urlTab) + const urlInput = screen.getByRole('textbox', { + name: /(?:^|\.)importFromDSLUrl(?=$|:)/, + }) + + await user.tab() + expect(urlInput).toHaveFocus() + }) + it('should render the import shortcut with kbd primitives', () => { render( { }) it('should show the DSL mismatch modal and confirm a pending import', async () => { - vi.useFakeTimers() mockImportDSL.mockResolvedValue({ id: 'import-3', status: DSLImportStatus.PENDING, @@ -395,10 +464,6 @@ describe('CreateFromDSLModal', () => { fireEvent.click(getCreateButton()) }) - await act(async () => { - vi.advanceTimersByTime(300) - }) - expect( screen.getAllByText(/(?:^|\.)newApp\.appCreateDSLErrorTitle(?=$|:)/)[0], )!.toBeInTheDocument() @@ -428,7 +493,6 @@ describe('CreateFromDSLModal', () => { }) it('should surface Agent warnings after confirming a pending import', async () => { - vi.useFakeTimers() mockImportDSL.mockResolvedValue({ id: 'agent-import-pending', status: DSLImportStatus.PENDING, @@ -467,9 +531,6 @@ describe('CreateFromDSLModal', () => { await act(async () => { fireEvent.click(getCreateButton()) }) - await act(async () => { - vi.advanceTimersByTime(300) - }) await act(async () => { fireEvent.click(screen.getAllByRole('button', { name: /newApp\.Confirm/ })[0]!) }) @@ -495,7 +556,6 @@ describe('CreateFromDSLModal', () => { }) it('should close the DSL mismatch modal when dialog requests close', async () => { - vi.useFakeTimers() mockImportDSL.mockResolvedValue({ id: 'import-close', status: DSLImportStatus.PENDING, @@ -516,13 +576,8 @@ describe('CreateFromDSLModal', () => { fireEvent.click(getCreateButton()) }) - await act(async () => { - vi.advanceTimersByTime(300) - }) - expect(screen.getByText(/(?:^|\.)newApp\.appCreateDSLErrorTitle(?=$|:)/))!.toBeInTheDocument() - vi.useRealTimers() fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) await waitFor(() => { @@ -533,7 +588,6 @@ describe('CreateFromDSLModal', () => { }) it('should close the DSL mismatch modal when cancel is clicked', async () => { - vi.useFakeTimers() mockImportDSL.mockResolvedValue({ id: 'import-cancel', status: DSLImportStatus.PENDING, @@ -554,13 +608,8 @@ describe('CreateFromDSLModal', () => { fireEvent.click(getCreateButton()) }) - await act(async () => { - vi.advanceTimersByTime(300) - }) - expect(screen.getByText(/(?:^|\.)newApp\.appCreateDSLErrorTitle(?=$|:)/))!.toBeInTheDocument() - vi.useRealTimers() fireEvent.click( screen.getAllByRole('button', { name: /(?:^|\.)newApp\.Cancel(?=$|:)/ }).at(-1)!, ) @@ -572,7 +621,7 @@ describe('CreateFromDSLModal', () => { }) }) - it('should ignore empty import responses and prevent duplicate submissions while a request is in flight', async () => { + it('should show import progress and lock dialog interactions while a request is in flight', async () => { let resolveImport!: (value: { id: string status: DSLImportStatus @@ -586,20 +635,32 @@ describe('CreateFromDSLModal', () => { resolveImport = resolve as typeof resolveImport }), ) + const handleClose = vi.fn() render( , ) fireEvent.click(getCreateButton()) + await waitFor(() => { + expect(mockImportDSL).toHaveBeenCalledTimes(1) + expect(getCreateButton()).toHaveAttribute('aria-disabled', 'true') + }) fireEvent.click(getCreateButton()) - expect(mockImportDSL).toHaveBeenCalledTimes(1) + expect(screen.getByText(/(?:^|\.)importFromDSLFile(?=$|:)/).closest('button')).toHaveAttribute( + 'aria-disabled', + 'true', + ) + + fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)newApp\.Cancel(?=$|:)/ })[0]!) + fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) + expect(handleClose).not.toHaveBeenCalled() await act(async () => { resolveImport({ @@ -610,15 +671,68 @@ describe('CreateFromDSLModal', () => { permission_keys: ['app.acl.view_layout'], }) }) + }) - mockImportDSL.mockResolvedValueOnce(undefined) + it('should show confirmation progress and prevent cancellation while confirming', async () => { + let resolveConfirm!: (value: { + id: string + status: DSLImportStatus + app_id: string + app_mode: string + }) => void + mockImportDSL.mockResolvedValue({ + id: 'import-confirming', + status: DSLImportStatus.PENDING, + imported_dsl_version: '1.0.0', + current_dsl_version: '2.0.0', + }) + mockImportDSLConfirm.mockImplementationOnce( + () => + new Promise((resolve) => { + resolveConfirm = resolve as typeof resolveConfirm + }), + ) + + render( + , + ) await act(async () => { fireEvent.click(getCreateButton()) }) - expect(mockImportDSL).toHaveBeenCalledTimes(2) - expect(toastMocks.error).not.toHaveBeenCalled() + const confirmButton = screen.getByRole('button', { + name: /(?:^|\.)newApp\.Confirm(?=$|:)/, + }) + fireEvent.click(confirmButton) + + await waitFor(() => { + expect(mockImportDSLConfirm).toHaveBeenCalledTimes(1) + expect(confirmButton).toHaveAttribute('aria-disabled', 'true') + }) + fireEvent.click(confirmButton) + expect(mockImportDSLConfirm).toHaveBeenCalledTimes(1) + + const cancelButton = screen + .getAllByRole('button', { name: /(?:^|\.)newApp\.Cancel(?=$|:)/ }) + .at(-1)! + expect(cancelButton).toBeDisabled() + fireEvent.click(cancelButton) + expect(screen.getByText(/(?:^|\.)newApp\.appCreateDSLErrorTitle(?=$|:)/)).toBeInTheDocument() + + await act(async () => { + resolveConfirm({ + id: 'import-confirming', + status: DSLImportStatus.COMPLETED, + app_id: 'app-confirming', + app_mode: AppModeEnum.WORKFLOW, + }) + }) }) it('should handle keyboard shortcut and quota guard', async () => { @@ -706,7 +820,6 @@ describe('CreateFromDSLModal', () => { }) it('should handle pending import confirmation failures and cancellation', async () => { - vi.useFakeTimers() mockImportDSL.mockResolvedValue({ id: 'import-4', status: DSLImportStatus.PENDING, @@ -731,7 +844,6 @@ describe('CreateFromDSLModal', () => { await act(async () => { fireEvent.click(getCreateButton()) - vi.advanceTimersByTime(300) }) fireEvent.click( @@ -743,7 +855,6 @@ describe('CreateFromDSLModal', () => { await act(async () => { fireEvent.click(getCreateButton()) - vi.advanceTimersByTime(300) }) await act(async () => { fireEvent.click(screen.getAllByRole('button', { name: /(?:^|\.)newApp\.Confirm(?=$|:)/ })[0]!) diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx index d09610563c9..97e2b364f11 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/uploader.spec.tsx @@ -1,6 +1,8 @@ import { toast } from '@langgenius/dify-ui/toast' import { fireEvent, render, screen } from '@testing-library/react' -import Uploader from '../uploader' +import userEvent from '@testing-library/user-event' +import { useState } from 'react' +import { Uploader } from '../uploader' vi.mock('@langgenius/dify-ui/toast', () => ({ toast: { @@ -16,7 +18,13 @@ describe('Uploader', () => { const getDropZone = (container: HTMLElement) => (container.firstChild as HTMLElement).querySelector('div') as HTMLElement - const getHiddenInput = () => document.getElementById('fileUploader') as HTMLInputElement + const getHiddenInput = () => document.querySelector('input[type="file"]')! + + function ControlledUploader({ initialFile }: { initialFile?: File }) { + const [file, setFile] = useState(initialFile) + + return + } it('should upload a single dropped file', () => { const updateFile = vi.fn() @@ -94,6 +102,83 @@ describe('Uploader', () => { expect(updateFile).toHaveBeenCalledWith(nextFile) }) + it('should move keyboard focus to the selected file row before the remove action', async () => { + const user = userEvent.setup() + const nextFile = new File(['next'], 'next.yml', { type: 'text/yaml' }) + render() + + const browseButton = screen.getByRole('button', { + name: /(?:^|\.)dslUploader\.browse(?=$|:)/, + }) + browseButton.focus() + await user.keyboard('{Enter}') + fireEvent.change(getHiddenInput(), { + target: { + files: [nextFile], + }, + }) + + const fileRow = screen.getByRole('group', { name: 'next.yml' }) + const deleteButton = screen.getByRole('button', { + name: /(?:^|\.)operation\.delete(?=$|:)/, + }) + + expect(fileRow).toHaveFocus() + await user.tab() + expect(deleteButton).toHaveFocus() + }) + + it('should preserve focus ownership after selecting a file with a pointer', async () => { + const user = userEvent.setup() + const nextFile = new File(['next'], 'next.yml', { type: 'text/yaml' }) + render() + + await user.click(screen.getByRole('button', { name: /(?:^|\.)dslUploader\.browse(?=$|:)/ })) + fireEvent.change(getHiddenInput(), { + target: { + files: [nextFile], + }, + }) + + expect(screen.getByRole('group', { name: 'next.yml' })).toHaveFocus() + }) + + it('should not move focus when a file is dropped', () => { + const nextFile = new File(['next'], 'next.yml', { type: 'text/yaml' }) + const { container } = render( + <> + + + , + ) + const outsideButton = screen.getByRole('button', { name: 'Outside' }) + const uploaderRoot = container.children[1] as HTMLElement + const dropZone = uploaderRoot.querySelector('input[type="file"]')?.nextElementSibling + outsideButton.focus() + + fireEvent.drop(dropZone as Element, { + dataTransfer: { + files: [nextFile], + }, + }) + + expect(outsideButton).toHaveFocus() + }) + + it('should restore keyboard focus to Browse after removing the file', async () => { + const user = userEvent.setup() + const file = new File(['name: demo'], 'demo.yml', { type: 'text/yaml' }) + render() + + const deleteButton = screen.getByRole('button', { + name: /(?:^|\.)operation\.delete(?=$|:)/, + }) + deleteButton.focus() + await user.keyboard('{Enter}') + + expect(screen.getByRole('button', { name: /(?:^|\.)dslUploader\.browse(?=$|:)/ })).toHaveFocus() + }) + it('should toggle drag styles and clear them when leaving the overlay', () => { const updateFile = vi.fn() const { container } = render() diff --git a/web/app/components/app/create-from-dsl-modal/dsl-confirm-modal.tsx b/web/app/components/app/create-from-dsl-modal/dsl-confirm-modal.tsx index fb70d585059..692e7317f79 100644 --- a/web/app/components/app/create-from-dsl-modal/dsl-confirm-modal.tsx +++ b/web/app/components/app/create-from-dsl-modal/dsl-confirm-modal.tsx @@ -17,20 +17,23 @@ type DSLConfirmModalProps = { onCancel: () => void onConfirm: () => void confirmDisabled?: boolean + confirmLoading?: boolean } -const DSLConfirmModal = ({ + +function DSLConfirmModal({ versions = { importedVersion: '', systemVersion: '' }, onCancel, onConfirm, confirmDisabled = false, -}: DSLConfirmModalProps) => { + confirmLoading = false, +}: DSLConfirmModalProps) { const { t } = useTranslation() return ( { - if (!open) onCancel() + if (!open && !confirmLoading) onCancel() }} > @@ -56,10 +59,14 @@ const DSLConfirmModal = ({ - + {t(($) => $['newApp.Cancel'], { ns: 'app' })} - + {t(($) => $['newApp.Confirm'], { ns: 'app' })} 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 60b8c6bb94d..fe1420cb386 100644 --- a/web/app/components/app/create-from-dsl-modal/index.tsx +++ b/web/app/components/app/create-from-dsl-modal/index.tsx @@ -1,18 +1,25 @@ 'use client' +import type { AppImportPayload, Import } from '@dify/contracts/api/console/apps/types.gen' import type { Hotkey } from '@tanstack/react-hotkeys' -import type { MouseEventHandler } from 'react' +import type { AppModeEnum } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' -import { Input } from '@langgenius/dify-ui/input' +import { + Dialog, + DialogBackdrop, + DialogPopup, + DialogPortal, + DialogTitle, +} from '@langgenius/dify-ui/dialog' +import { Field, FieldControl, FieldError, FieldLabel } from '@langgenius/dify-ui/field' +import { Form } from '@langgenius/dify-ui/form' import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd' +import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' import { toast } from '@langgenius/dify-ui/toast' import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys' -import { useSuspenseQuery } from '@tanstack/react-query' -import { useDebounceFn } from 'ahooks' +import { useMutation, useSuspenseQuery } from '@tanstack/react-query' import { useAtomValue } from 'jotai' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useSetNeedRefreshAppList } from '@/app/components/apps/storage' import AppsFull from '@/app/components/billing/apps-full-in-dialog' @@ -21,16 +28,17 @@ import { userProfileIdAtom } from '@/context/account-state' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useProviderContext } from '@/context/provider-context' import { systemFeaturesQueryOptions } from '@/features/system-features/client' -import { DSLImportMode, DSLImportStatus } from '@/models/app' import { useRouter } from '@/next/navigation' -import { importDSL, importDSLConfirm } from '@/service/apps' +import { consoleClient, consoleQuery } from '@/service/client' import { useInvalidateAppList } from '@/service/use-apps' +import { AppModeEnum as AppMode } from '@/types/app' import { getRedirection } from '@/utils/app-redirection' import { trackCreateApp } from '@/utils/create-app-tracking' import { getDSLImportWarningDescription } from '@/utils/dsl-import-warning' import { resolveImportedAppRedirectionTarget } from '@/utils/imported-app-redirection' +import DSLConfirmModal from './dsl-confirm-modal' import { CreateFromDSLModalTab } from './types' -import Uploader from './uploader' +import { Uploader } from './uploader' type CreateFromDSLModalProps = { show: boolean @@ -41,350 +49,320 @@ type CreateFromDSLModalProps = { droppedFile?: File } +type ImportFormValues = { + dslUrl?: string +} + +type PendingImport = { + id: string + importedVersion: string + systemVersion: string +} + +type ImportSource = + | { type: (typeof CreateFromDSLModalTab)['FROM_FILE']; file: File } + | { type: (typeof CreateFromDSLModalTab)['FROM_URL']; url: string } + const CREATE_FROM_DSL_HOTKEY = 'Mod+Enter' satisfies Hotkey -const CreateFromDSLModal = ({ +function getImportedAppMode(mode?: string | null): AppModeEnum | undefined { + switch (mode) { + case AppMode.COMPLETION: + return AppMode.COMPLETION + case AppMode.WORKFLOW: + return AppMode.WORKFLOW + case AppMode.CHAT: + return AppMode.CHAT + case AppMode.ADVANCED_CHAT: + return AppMode.ADVANCED_CHAT + case AppMode.AGENT_CHAT: + return AppMode.AGENT_CHAT + case AppMode.AGENT: + return AppMode.AGENT + default: + return undefined + } +} + +function CreateFromDSLModal({ show, onSuccess, onClose, activeTab = CreateFromDSLModalTab.FROM_FILE, dslUrl = '', droppedFile, -}: CreateFromDSLModalProps) => { +}: CreateFromDSLModalProps) { const { push } = useRouter() const { t } = useTranslation() + const formRef = useRef(null) + const browseButtonRef = useRef(null) const [currentFile, setCurrentFile] = useState(droppedFile) - const [fileContent, setFileContent] = useState() const [currentTab, setCurrentTab] = useState(activeTab) - const [dslUrlValue, setDslUrlValue] = useState(dslUrl) - const [showErrorModal, setShowErrorModal] = useState(false) - const [versions, setVersions] = useState<{ importedVersion: string; systemVersion: string }>() - const [importId, setImportId] = useState() + const [pendingImport, setPendingImport] = useState(null) + const importMutation = useMutation({ + mutationFn: async (source: ImportSource) => { + const body = + source.type === CreateFromDSLModalTab.FROM_FILE + ? ({ + mode: 'yaml-content', + yaml_content: await source.file.text(), + } satisfies AppImportPayload) + : ({ + mode: 'yaml-url', + yaml_url: source.url, + } satisfies AppImportPayload) + + return consoleClient.apps.imports.post({ body }) + }, + }) + const confirmImportMutation = useMutation( + consoleQuery.apps.imports.byImportId.confirm.post.mutationOptions(), + ) const { handleCheckPluginDependencies } = usePluginDependencies() const setNeedRefresh = useSetNeedRefreshAppList() + const invalidateAppList = useInvalidateAppList() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const isRbacEnabled = systemFeatures.rbac_enabled const currentUserId = useAtomValue(userProfileIdAtom) const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom) - - const readFile = useCallback((file: File) => { - const reader = new FileReader() - reader.onload = function (event) { - const content = event.target?.result - setFileContent(content as string) - } - reader.readAsText(file) - }, []) - - const handleFile = useCallback( - (file?: File) => { - setCurrentFile(file) - if (file) readFile(file) - if (!file) setFileContent('') - }, - [readFile], - ) - const { plan, enableBilling } = useProviderContext() const isAppsFull = enableBilling && plan.usage.buildApps >= plan.total.buildApps - const invalidateAppList = useInvalidateAppList() + const isImporting = importMutation.isPending + const isConfirming = confirmImportMutation.isPending - const isCreatingRef = useRef(false) + const handleCompletedImport = async (response: Import) => { + const appMode = getImportedAppMode(response.app_mode) - useEffect(() => { - if (droppedFile) readFile(droppedFile) - }, [droppedFile, readFile]) + if (appMode) trackCreateApp({ source: 'studio_upload', appMode }) + onSuccess?.() + onClose() - const onCreate = async (_e?: React.MouseEvent) => { - if (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) return - if (currentTab === CreateFromDSLModalTab.FROM_URL && !dslUrlValue) return - if (isCreatingRef.current) return - isCreatingRef.current = true - try { - let response + toast( + t(($) => $[response.status === 'completed' ? 'newApp.appCreated' : 'newApp.caution'], { + ns: 'app', + }), + { + type: response.status === 'completed' ? 'success' : 'warning', + description: + response.status === 'completed-with-warnings' + ? getDSLImportWarningDescription(response.warnings) || + t(($) => $['newApp.appCreateDSLWarning'], { ns: 'app' }) + : undefined, + }, + ) + setNeedRefresh('1') + invalidateAppList() - if (currentTab === CreateFromDSLModalTab.FROM_FILE) { - response = await importDSL({ - mode: DSLImportMode.YAML_CONTENT, - yaml_content: fileContent || '', - }) - } - if (currentTab === CreateFromDSLModalTab.FROM_URL) { - response = await importDSL({ - mode: DSLImportMode.YAML_URL, - yaml_url: dslUrlValue || '', - }) - } + if (!response.app_id || !appMode) return - if (!response) return - const { - id, - status, - app_id, - app_mode, - imported_dsl_version, - current_dsl_version, - permission_keys, - warnings, - } = response - if ( - status === DSLImportStatus.COMPLETED || - status === DSLImportStatus.COMPLETED_WITH_WARNINGS - ) { - trackCreateApp({ source: 'studio_upload', appMode: app_mode }) - - if (onSuccess) onSuccess() - if (onClose) onClose() - - toast( - t( - ($) => $[status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution'], - { ns: 'app' }, - ), - { - type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', - description: - status === DSLImportStatus.COMPLETED_WITH_WARNINGS - ? getDSLImportWarningDescription(warnings) || - t(($) => $['newApp.appCreateDSLWarning'], { ns: 'app' }) - : undefined, - }, - ) - setNeedRefresh('1') - invalidateAppList() - if (app_id) { - await handleCheckPluginDependencies(app_id) - const redirectionTarget = await resolveImportedAppRedirectionTarget({ - id: app_id, - mode: app_mode, - permission_keys, - }) - getRedirection(redirectionTarget, push, { - currentUserId, - resourceMaintainer: currentUserId, - workspacePermissionKeys, - isRbacEnabled, - }) - } - } else if (status === DSLImportStatus.PENDING) { - setVersions({ - importedVersion: imported_dsl_version ?? '', - systemVersion: current_dsl_version ?? '', - }) - setTimeout(() => { - setShowErrorModal(true) - }, 300) - setImportId(id) - } else { - toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) - } - } catch { - toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) - } - isCreatingRef.current = false + await handleCheckPluginDependencies(response.app_id) + const redirectionTarget = await resolveImportedAppRedirectionTarget({ + id: response.app_id, + mode: appMode, + permission_keys: response.permission_keys, + }) + getRedirection(redirectionTarget, push, { + currentUserId, + resourceMaintainer: currentUserId, + workspacePermissionKeys, + isRbacEnabled: systemFeatures.rbac_enabled, + }) } - const { run: handleCreateApp } = useDebounceFn(onCreate, { wait: 300 }) + const handleImportResponse = async (response: Import) => { + if (response.status === 'completed' || response.status === 'completed-with-warnings') { + await handleCompletedImport(response) + return + } - useHotkey( - CREATE_FROM_DSL_HOTKEY, - () => { - handleCreateApp(undefined) - }, - { - enabled: - show && - !isAppsFull && - ((currentTab === CreateFromDSLModalTab.FROM_FILE && !!currentFile) || - (currentTab === CreateFromDSLModalTab.FROM_URL && !!dslUrlValue)), - ignoreInputs: false, - }, - ) - - const onDSLConfirm: MouseEventHandler = async () => { - try { - if (!importId) return - const response = await importDSLConfirm({ - import_id: importId, + if (response.status === 'pending') { + setPendingImport({ + id: response.id, + importedVersion: response.imported_dsl_version ?? '', + systemVersion: response.current_dsl_version ?? '', }) + return + } - const { status, app_id, app_mode, permission_keys, warnings } = response + toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) + } - if ( - status === DSLImportStatus.COMPLETED || - status === DSLImportStatus.COMPLETED_WITH_WARNINGS - ) { - trackCreateApp({ source: 'studio_upload', appMode: app_mode }) - if (onSuccess) onSuccess() - if (onClose) onClose() + const handleSubmit = async (values: ImportFormValues) => { + if (isAppsFull || isImporting) return - toast( - t( - ($) => $[status === DSLImportStatus.COMPLETED ? 'newApp.appCreated' : 'newApp.caution'], - { ns: 'app' }, - ), - { - type: status === DSLImportStatus.COMPLETED ? 'success' : 'warning', - description: - status === DSLImportStatus.COMPLETED_WITH_WARNINGS - ? getDSLImportWarningDescription(warnings) || - t(($) => $['newApp.appCreateDSLWarning'], { ns: 'app' }) - : undefined, - }, - ) - if (app_id) await handleCheckPluginDependencies(app_id) - setNeedRefresh('1') - invalidateAppList() - if (app_id) { - const redirectionTarget = await resolveImportedAppRedirectionTarget({ - id: app_id, - mode: app_mode, - permission_keys, - }) - getRedirection(redirectionTarget, push, { - currentUserId, - resourceMaintainer: currentUserId, - workspacePermissionKeys, - isRbacEnabled, - }) - } - } else if (status === DSLImportStatus.FAILED) { - toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) + try { + let source: ImportSource + if (currentTab === CreateFromDSLModalTab.FROM_FILE) { + if (!currentFile) return + source = { type: CreateFromDSLModalTab.FROM_FILE, file: currentFile } + } else { + const yamlUrl = values.dslUrl?.trim() + if (!yamlUrl) return + source = { type: CreateFromDSLModalTab.FROM_URL, url: yamlUrl } } + + const response = await importMutation.mutateAsync(source) + await handleImportResponse(response) } catch { toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) } } - const tabs = [ - { - key: CreateFromDSLModalTab.FROM_FILE, - label: t(($) => $.importFromDSLFile, { ns: 'app' }), - }, - { - key: CreateFromDSLModalTab.FROM_URL, - label: t(($) => $.importFromDSLUrl, { ns: 'app' }), - }, - ] + const handleConfirm = async () => { + if (!pendingImport || isConfirming) return - const buttonDisabled = useMemo(() => { - if (isAppsFull) return true - if (currentTab === CreateFromDSLModalTab.FROM_FILE) return !currentFile - if (currentTab === CreateFromDSLModalTab.FROM_URL) return !dslUrlValue - return false - }, [isAppsFull, currentTab, currentFile, dslUrlValue]) + try { + const response = await confirmImportMutation.mutateAsync({ + params: { import_id: pendingImport.id }, + }) + if (response.status === 'completed' || response.status === 'completed-with-warnings') { + setPendingImport(null) + await handleCompletedImport(response) + return + } + + if (response.status === 'failed') + toast.error(response.error || t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) + } catch { + toast.error(t(($) => $['newApp.appCreateFailed'], { ns: 'app' })) + } + } + + const handleTabChange = (value: string | number) => { + if (value === CreateFromDSLModalTab.FROM_FILE) setCurrentTab(CreateFromDSLModalTab.FROM_FILE) + if (value === CreateFromDSLModalTab.FROM_URL) setCurrentTab(CreateFromDSLModalTab.FROM_URL) + } + + const createDisabled = + isAppsFull || (currentTab === CreateFromDSLModalTab.FROM_FILE && !currentFile) + + useHotkey(CREATE_FROM_DSL_HOTKEY, () => formRef.current?.requestSubmit(), { + enabled: show && !createDisabled && !isImporting && !pendingImport, + ignoreInputs: false, + }) return ( <> - !open && !showErrorModal && onClose()}> - -
- {t(($) => $.importApp, { ns: 'app' })} - -
-
- {tabs.map((tab) => ( - - ))} -
-
- {currentTab === CreateFromDSLModalTab.FROM_FILE && ( - - )} - {currentTab === CreateFromDSLModalTab.FROM_URL && ( -
-
DSL URL
- $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''} - value={dslUrlValue} - onChange={(e) => setDslUrlValue(e.target.value)} - /> -
- )} -
- {isAppsFull && ( -
- -
- )} -
- - -
-
-
{ - if (!open) setShowErrorModal(false) + if (!open && !isImporting && !pendingImport) onClose() }} > - -
-
- {t(($) => $['newApp.appCreateDSLErrorTitle'], { ns: 'app' })} + + + +
+ + {t(($) => $.importApp, { ns: 'app' })} + +
-
-
{t(($) => $['newApp.appCreateDSLErrorPart1'], { ns: 'app' })}
-
{t(($) => $['newApp.appCreateDSLErrorPart2'], { ns: 'app' })}
-
-
- {t(($) => $['newApp.appCreateDSLErrorPart3'], { ns: 'app' })} - {versions?.importedVersion} + ref={formRef} onFormSubmit={handleSubmit}> + + + + {t(($) => $.importFromDSLFile, { ns: 'app' })} + + + {t(($) => $.importFromDSLUrl, { ns: 'app' })} + + + + + + + + {t(($) => $.importFromDSLUrl, { ns: 'app' })} + $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''} + defaultValue={dslUrl} + /> + + + + + {isAppsFull && ( +
+ +
+ )} +
+ +
-
- {t(($) => $['newApp.appCreateDSLErrorPart4'], { ns: 'app' })} - {versions?.systemVersion} -
-
-
-
- - -
- + +
+
+ {pendingImport && ( + { + if (!isConfirming) setPendingImport(null) + }} + onConfirm={handleConfirm} + confirmLoading={isConfirming} + /> + )} ) } diff --git a/web/app/components/app/create-from-dsl-modal/uploader.tsx b/web/app/components/app/create-from-dsl-modal/uploader.tsx index d5bd88f558a..f45356794ae 100644 --- a/web/app/components/app/create-from-dsl-modal/uploader.tsx +++ b/web/app/components/app/create-from-dsl-modal/uploader.tsx @@ -1,55 +1,82 @@ 'use client' -import type { FC } from 'react' +import type { ChangeEvent, DragEvent, MouseEvent, RefObject } from 'react' +import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' -import { RiDeleteBinLine, RiUploadCloud2Line } from '@remixicon/react' -import * as React from 'react' -import { useEffect, useRef, useState } from 'react' +import { useId, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import ActionButton from '@/app/components/base/action-button' -import { Yaml as YamlIcon } from '@/app/components/base/icons/src/public/files' import { formatFileSize } from '@/utils/format' type Props = Readonly<{ file: File | undefined updateFile: (file?: File) => void + browseButtonRef?: RefObject className?: string accept?: string displayName?: string + disabled?: boolean }> -const Uploader: FC = ({ +export function Uploader({ file, updateFile, + browseButtonRef, className, accept = '.yaml,.yml', displayName = 'YAML', -}) => { + disabled = false, +}: Props) { const { t } = useTranslation() const [dragging, setDragging] = useState(false) - const dropRef = useRef(null) const dragRef = useRef(null) - const fileUploader = useRef(null) + const fileUploaderRef = useRef(null) + const internalBrowseButtonRef = useRef(null) + const resolvedBrowseButtonRef = browseButtonRef ?? internalBrowseButtonRef + const fileRowRef = useRef(null) + const removeButtonRef = useRef(null) + const fileNameId = useId() + const fileMetadataId = useId() + const pendingFocusRef = useRef<{ + target: 'browse' | 'file' + focusVisible: boolean + } | null>(null) - const handleDragEnter = (e: DragEvent) => { + useLayoutEffect(() => { + const pendingFocus = pendingFocusRef.current + if (!pendingFocus) return + + const focusTarget = file + ? pendingFocus.target === 'file' && fileRowRef.current + : pendingFocus.target === 'browse' && resolvedBrowseButtonRef.current + + if (!focusTarget) return + focusTarget.focus({ + preventScroll: true, + ...(pendingFocus.focusVisible && { focusVisible: true }), + }) + pendingFocusRef.current = null + }, [file, resolvedBrowseButtonRef]) + + const handleDragEnter = (e: DragEvent) => { e.preventDefault() e.stopPropagation() + if (disabled) return if (e.target !== dragRef.current) setDragging(true) } - const handleDragOver = (e: DragEvent) => { + const handleDragOver = (e: DragEvent) => { e.preventDefault() e.stopPropagation() } - const handleDragLeave = (e: DragEvent) => { + const handleDragLeave = (e: DragEvent) => { e.preventDefault() e.stopPropagation() if (e.target === dragRef.current) setDragging(false) } - const handleDrop = (e: DragEvent) => { + const handleDrop = (e: DragEvent) => { e.preventDefault() e.stopPropagation() setDragging(false) - if (!e.dataTransfer) return + if (disabled || !e.dataTransfer) return const files = Array.from(e.dataTransfer.files) if (files.length > 1) { toast.error(t(($) => $['stepOne.uploader.validation.count'], { ns: 'datasetCreation' })) @@ -57,48 +84,54 @@ const Uploader: FC = ({ } updateFile(files[0]) } - const selectHandle = () => { + const selectHandle = (e: MouseEvent) => { + if (disabled) return + pendingFocusRef.current = + document.activeElement === resolvedBrowseButtonRef.current + ? { target: 'file', focusVisible: e.detail === 0 } + : null const originalFile = file - if (fileUploader.current) { - fileUploader.current.value = '' - fileUploader.current.click() - // If no file is selected, restore the original file - fileUploader.current.oncancel = () => updateFile(originalFile) + if (fileUploaderRef.current) { + fileUploaderRef.current.value = '' + fileUploaderRef.current.click() + fileUploaderRef.current.oncancel = () => { + pendingFocusRef.current = null + updateFile(originalFile) + } } } - const removeFile = () => { - if (fileUploader.current) fileUploader.current.value = '' + const removeFile = (e: MouseEvent) => { + if (disabled) return + pendingFocusRef.current = + document.activeElement === removeButtonRef.current + ? { target: 'browse', focusVisible: e.detail === 0 } + : null + if (fileUploaderRef.current) fileUploaderRef.current.value = '' updateFile() } - const fileChangeHandle = (e: React.ChangeEvent) => { + const fileChangeHandle = (e: ChangeEvent) => { + if (disabled) return const currentFile = e.target.files?.[0] + if (!currentFile) pendingFocusRef.current = null updateFile(currentFile) } - useEffect(() => { - dropRef.current?.addEventListener('dragenter', handleDragEnter) - dropRef.current?.addEventListener('dragover', handleDragOver) - dropRef.current?.addEventListener('dragleave', handleDragLeave) - dropRef.current?.addEventListener('drop', handleDrop) - return () => { - dropRef.current?.removeEventListener('dragenter', handleDragEnter) - dropRef.current?.removeEventListener('dragover', handleDragOver) - dropRef.current?.removeEventListener('dragleave', handleDragLeave) - dropRef.current?.removeEventListener('drop', handleDrop) - } - }, []) - return (
-
+
{!file && (
= ({ )} >
- -
- {t(($) => $['dslUploader.button'], { ns: 'app' })} - +
{dragging &&
} @@ -125,28 +161,47 @@ const Uploader: FC = ({ )} {file && (
- +
- + {file.name} -
+
{displayName} ยท {formatFileSize(file.size)}
-
- - - +
+
)} @@ -154,5 +209,3 @@ const Uploader: FC = ({
) } - -export default React.memo(Uploader) diff --git a/web/app/components/base/icons/src/public/files/Yaml.json b/web/app/components/base/icons/src/public/files/Yaml.json deleted file mode 100644 index b55681c7303..00000000000 --- a/web/app/components/base/icons/src/public/files/Yaml.json +++ /dev/null @@ -1,181 +0,0 @@ -{ - "icon": { - "type": "element", - "isRootNode": true, - "name": "svg", - "attributes": { - "fill": "none", - "height": "26", - "viewBox": "0 0 24 26", - "width": "24", - "xmlns": "http://www.w3.org/2000/svg", - "xmlns:xlink": "http://www.w3.org/1999/xlink" - }, - "children": [ - { - "type": "element", - "name": "filter", - "attributes": { - "id": "a", - "color-interpolation-filters": "sRGB", - "filterUnits": "userSpaceOnUse", - "height": "26", - "width": "22", - "x": "1", - "y": "0" - }, - "children": [ - { - "type": "element", - "name": "feFlood", - "attributes": { - "flood-opacity": "0", - "result": "BackgroundImageFix" - }, - "children": [] - }, - { - "type": "element", - "name": "feColorMatrix", - "attributes": { - "in": "SourceAlpha", - "result": "hardAlpha", - "type": "matrix", - "values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" - }, - "children": [] - }, - { - "type": "element", - "name": "feOffset", - "attributes": { - "dy": "1" - }, - "children": [] - }, - { - "type": "element", - "name": "feGaussianBlur", - "attributes": { - "stdDeviation": "1" - }, - "children": [] - }, - { - "type": "element", - "name": "feColorMatrix", - "attributes": { - "type": "matrix", - "values": "0 0 0 0 0.0627451 0 0 0 0 0.0941176 0 0 0 0 0.156863 0 0 0 0.05 0" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "in2": "BackgroundImageFix", - "mode": "normal", - "result": "effect1_dropShadow_7605_8828" - }, - "children": [] - }, - { - "type": "element", - "name": "feBlend", - "attributes": { - "in": "SourceGraphic", - "in2": "effect1_dropShadow_7605_8828", - "mode": "normal", - "result": "shape" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "g", - "attributes": { - "filter": "url(#a)" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "m3 5.8c0-1.68016 0-2.52024.32698-3.16197.28762-.56449.74656-1.02343 1.31105-1.31105.64173-.32698 1.48181-.32698 3.16197-.32698h6.2l7 7v10.2c0 1.6802 0 2.5202-.327 3.162-.2876.5645-.7465 1.0234-1.311 1.311-.6418.327-1.4818.327-3.162.327h-8.4c-1.68016 0-2.52024 0-3.16197-.327-.56449-.2876-1.02343-.7465-1.31105-1.311-.32698-.6418-.32698-1.4818-.32698-3.162z", - "fill": "#e8eaed" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "m16.2 22.75h-8.4c-.8442 0-1.46232-.0002-1.95004-.04-.48479-.0397-.81868-.1172-1.09843-.2597-.51745-.2637-.93815-.6844-1.2018-1.2018-.14254-.2798-.22008-.6137-.25969-1.0985-.03985-.4877-.04004-1.1058-.04004-1.95v-12.4c0-.8442.00019-1.46232.04004-1.95004.03961-.48479.11715-.81868.25969-1.09843.26365-.51745.68435-.93815 1.2018-1.2018.27975-.14254.61364-.22008 1.09843-.25969.48772-.03985 1.10584-.04004 1.95004-.04004h6.0964l6.8536 6.85355v10.09645c0 .8442-.0002 1.4623-.04 1.95-.0397.4848-.1172.8187-.2597 1.0985-.2637.5174-.6844.9381-1.2018 1.2018-.2798.1425-.6137.22-1.0985.2597-.4877.0398-1.1058.04-1.95.04z", - "stroke": "#000", - "stroke-opacity": ".03", - "stroke-width": ".5" - }, - "children": [] - } - ] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "m14 1 7 7h-5c-1.1046 0-2-.89543-2-2z", - "fill": "#fff", - "opacity": ".5" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "m11.5264 9-2.15191 3.2267v2.0455h-1.31897v-2.0455l-2.05552-3.2267h1.48242l1.30707 2.0776 1.31781-2.0776z", - "fill": "#000" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "m13.7426 13.1121h-2.3874l-.4855 1.1724h-1.0572l2.2355-5.27223h1.0813l2.1448 5.27223h-1.1297zm-.3966-1.0526-.7318-1.9348-.8165 1.9348z", - "fill": "#cb171e" - }, - "children": [] - }, - { - "type": "element", - "name": "g", - "attributes": { - "fill": "#000" - }, - "children": [ - { - "type": "element", - "name": "path", - "attributes": { - "d": "m8.05469 14.8635v5.1673h1.10866v-3.5643l1.16025 2.3957h.8727l1.1999-2.4799v3.6474h1.0636v-5.1662h-1.4522l-1.2885 2.3369-1.22722-2.3369z" - }, - "children": [] - }, - { - "type": "element", - "name": "path", - "attributes": { - "d": "m17.9994 18.9079h-2.7272v-4.0456h-1.1296v5.1451h3.8568z" - }, - "children": [] - } - ] - } - ] - }, - "name": "Yaml" -} diff --git a/web/app/components/base/icons/src/public/files/Yaml.tsx b/web/app/components/base/icons/src/public/files/Yaml.tsx deleted file mode 100644 index 5869aaa8068..00000000000 --- a/web/app/components/base/icons/src/public/files/Yaml.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// GENERATE BY script -// DON NOT EDIT IT MANUALLY - -import type { IconData } from '@/app/components/base/icons/IconBase' -import * as React from 'react' -import IconBase from '@/app/components/base/icons/IconBase' -import data from './Yaml.json' - -const Icon = ({ - ref, - ...props -}: React.SVGProps & { - ref?: React.RefObject> -}) => - -Icon.displayName = 'Yaml' - -export default Icon diff --git a/web/app/components/base/icons/src/public/files/index.ts b/web/app/components/base/icons/src/public/files/index.ts index f38c28cbdb5..c6f082cc0c8 100644 --- a/web/app/components/base/icons/src/public/files/index.ts +++ b/web/app/components/base/icons/src/public/files/index.ts @@ -8,4 +8,3 @@ export { default as Pdf } from './Pdf' export { default as Txt } from './Txt' export { default as Unknown } from './Unknown' export { default as Xlsx } from './Xlsx' -export { default as Yaml } from './Yaml' diff --git a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx index 52e9702fb55..fd93558bb17 100644 --- a/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/rag-pipeline/components/__tests__/update-dsl-modal.spec.tsx @@ -90,7 +90,7 @@ vi.mock('@/service/workflow', () => ({ })) vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({ - default: ({ updateFile }: { updateFile: (file?: File) => void }) => ( + Uploader: ({ updateFile }: { updateFile: (file?: File) => void }) => (
({ })) vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({ - default: ({ file, updateFile }: { file?: File; updateFile: (file?: File) => void }) => ( + Uploader: ({ file, updateFile }: { file?: File; updateFile: (file?: File) => void }) => ( diff --git a/web/app/components/snippets/import-snippet-dsl-dialog.tsx b/web/app/components/snippets/import-snippet-dsl-dialog.tsx index 486e3867d40..5994a2e481e 100644 --- a/web/app/components/snippets/import-snippet-dsl-dialog.tsx +++ b/web/app/components/snippets/import-snippet-dsl-dialog.tsx @@ -19,7 +19,7 @@ import { toast } from '@langgenius/dify-ui/toast' import { useAtomValue } from 'jotai' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -import Uploader from '@/app/components/app/create-from-dsl-modal/uploader' +import { Uploader } from '@/app/components/app/create-from-dsl-modal/uploader' import { workspacePermissionKeysAtom } from '@/context/permission-state' import { useRouter } from '@/next/navigation' import { diff --git a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx index 265155acea9..ced8efcbb53 100644 --- a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx @@ -66,7 +66,7 @@ vi.mock('@/app/components/app/store', () => ({ })) vi.mock('@/app/components/app/create-from-dsl-modal/uploader', () => ({ - default: ({ updateFile }: { updateFile: (file?: File) => void }) => ( + Uploader: ({ updateFile }: { updateFile: (file?: File) => void }) => ( { +export const getDSLImportWarningDescription = (warnings: DslImportWarning[] = []) => { const messages = [...new Set(warnings.map((warning) => warning.message.trim()).filter(Boolean))] if (!messages.length) return