From eb7a1829fe1ee772ca28b3852cc4f253bf05434e Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 25 Aug 2026 09:41:36 +0000 Subject: [PATCH] refactor(web): migrate pipeline DSL import form (#41232) --- oxlint-suppressions.json | 19 -- .../__tests__/index.spec.tsx | 189 ++++-------------- .../create-from-dsl-modal/header.tsx | 25 --- .../create-from-dsl-modal/index.tsx | 93 ++++++--- .../create-from-dsl-modal/tab/index.tsx | 39 ---- .../create-from-dsl-modal/tab/item.tsx | 27 --- 6 files changed, 110 insertions(+), 282 deletions(-) delete mode 100644 web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx delete mode 100644 web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/index.tsx delete mode 100644 web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/item.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 7f0f305e772..620c9c5a234 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1847,14 +1847,6 @@ "count": 3 } }, - "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/hooks/use-dsl-import.ts": { "erasable-syntax-only/enums": { "count": 1 @@ -1863,17 +1855,6 @@ "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/index.tsx": { "no-barrel-files/no-barrel-files": { "count": 1 - }, - "no-restricted-imports": { - "count": 1 - } - }, - "web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 } }, "web/app/components/datasets/create-from-pipeline/list/create-card.tsx": { diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx index 748efeecdd6..6a8344b5f9f 100644 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/__tests__/index.spec.tsx @@ -1,10 +1,8 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' import DSLConfirmModal from '../dsl-confirm-modal' -import Header from '../header' import CreateFromDSLModal, { CreateFromDSLModalTab } from '../index' -import Tab from '../tab' -import TabItem from '../tab/item' import Uploader from '../uploader' const mockPush = vi.fn() @@ -124,8 +122,15 @@ describe('CreateFromDSLModal', () => { it('should render file tab by default', () => { render(, { wrapper: createWrapper() }) - expect(screen.getByText('app.importFromDSLFile'))!.toBeInTheDocument() - expect(screen.getByText('app.importFromDSLUrl'))!.toBeInTheDocument() + expect(screen.getByRole('dialog', { name: 'app.importFromDSL' }))!.toBeInTheDocument() + expect(screen.getByRole('tab', { name: 'app.importFromDSLFile' }))!.toHaveAttribute( + 'aria-selected', + 'true', + ) + expect(screen.getByRole('tab', { name: 'app.importFromDSLUrl' }))!.toHaveAttribute( + 'aria-selected', + 'false', + ) }) it('should render cancel and import buttons', () => { @@ -158,8 +163,7 @@ describe('CreateFromDSLModal', () => { { wrapper: createWrapper() }, ) - expect(screen.getByText('DSL URL'))!.toBeInTheDocument() - expect(screen.getByPlaceholderText('app.importFromDSLUrlPlaceholder'))!.toBeInTheDocument() + expect(screen.getByRole('textbox', { name: 'DSL URL' }))!.toBeInTheDocument() }) }) @@ -210,18 +214,17 @@ describe('CreateFromDSLModal', () => { }) describe('State Management', () => { - it('should switch between tabs', () => { + it('should move focus from the URL tab directly into its field', async () => { + const user = userEvent.setup() render(, { wrapper: createWrapper() }) - // Initially file tab is active - // Initially file tab is active - expect(screen.getByText('app.dslUploader.button'))!.toBeInTheDocument() + const urlTab = screen.getByRole('tab', { name: 'app.importFromDSLUrl' }) + await user.click(urlTab) - fireEvent.click(screen.getByText('app.importFromDSLUrl')) + const input = screen.getByRole('textbox', { name: 'DSL URL' }) + await user.tab() - // URL input should be visible - // URL input should be visible - expect(screen.getByText('DSL URL'))!.toBeInTheDocument() + expect(input)!.toHaveFocus() }) it('should update URL value when typing', () => { @@ -513,13 +516,32 @@ describe('CreateFromDSLModal', () => { const onClose = vi.fn() render(, { wrapper: createWrapper() }) - // Find and click the close icon in header - const closeIcon = document.querySelector('[class*="cursor-pointer"]') + fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' })) - if (closeIcon) { - fireEvent.click(closeIcon) - expect(onClose).toHaveBeenCalled() - } + expect(onClose).toHaveBeenCalled() + }) + + it('should submit a URL import when Enter is pressed in the URL field', async () => { + const user = userEvent.setup() + mockImportDSL.mockResolvedValue(createImportDSLResponse()) + render( + , + { wrapper: createWrapper() }, + ) + + const input = screen.getByRole('textbox', { name: 'DSL URL' }) + await user.type(input, 'https://example.com/test.pipeline{Enter}') + + await waitFor(() => { + expect(mockImportDSL).toHaveBeenCalledWith({ + mode: 'yaml-url', + yaml_url: 'https://example.com/test.pipeline', + }) + }) }) it('should close modal on ESC key press', () => { @@ -1141,131 +1163,6 @@ describe('CreateFromDSLModal', () => { }) }) -// Header Component Tests -describe('Header', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Rendering', () => { - it('should render title', () => { - render(
) - expect(screen.getByText('app.importFromDSL'))!.toBeInTheDocument() - }) - - it('should render close icon', () => { - render(
) - // Check for close icon container - const closeButton = document.querySelector('[class*="cursor-pointer"]') - expect(closeButton)!.toBeInTheDocument() - }) - }) - - describe('Event Handlers', () => { - it('should call onClose when close icon is clicked', () => { - const onClose = vi.fn() - render(
) - - const closeButton = document.querySelector('[class*="cursor-pointer"]')! - fireEvent.click(closeButton) - - expect(onClose).toHaveBeenCalled() - }) - }) -}) - -// Tab Component Tests -describe('Tab', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Rendering', () => { - it('should render both tabs', () => { - render() - - expect(screen.getByText('app.importFromDSLFile'))!.toBeInTheDocument() - expect(screen.getByText('app.importFromDSLUrl'))!.toBeInTheDocument() - }) - }) - - describe('Event Handlers', () => { - it('should call setCurrentTab when clicking file tab', () => { - const setCurrentTab = vi.fn() - render() - - fireEvent.click(screen.getByText('app.importFromDSLFile')) - // Tab uses bind() which passes the key as first argument and event as second - expect(setCurrentTab).toHaveBeenCalled() - expect(setCurrentTab.mock.calls[0]![0]).toBe(CreateFromDSLModalTab.FROM_FILE) - }) - - it('should call setCurrentTab when clicking URL tab', () => { - const setCurrentTab = vi.fn() - render() - - fireEvent.click(screen.getByText('app.importFromDSLUrl')) - // Tab uses bind() which passes the key as first argument and event as second - expect(setCurrentTab).toHaveBeenCalled() - expect(setCurrentTab.mock.calls[0]![0]).toBe(CreateFromDSLModalTab.FROM_URL) - }) - }) -}) - -// Tab Item Component Tests -describe('TabItem', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - describe('Rendering', () => { - it('should render label', () => { - render() - - expect(screen.getByText('Test Tab'))!.toBeInTheDocument() - }) - - it('should render active indicator when active', () => { - render() - - // Active indicator is the bottom border div - const indicator = document.querySelector('[class*="bg-util-colors-blue"]') - expect(indicator)!.toBeInTheDocument() - }) - - it('should not render active indicator when inactive', () => { - render() - - const indicator = document.querySelector('[class*="bg-util-colors-blue"]') - expect(indicator).toBeNull() - }) - - it('should have active text color when active', () => { - render() - - const item = screen.getByText('Test Tab') - expect(item.className).toContain('text-text-primary') - }) - - it('should have inactive text color when inactive', () => { - render() - - const item = screen.getByText('Test Tab') - expect(item.className).toContain('text-text-tertiary') - }) - }) - - describe('Event Handlers', () => { - it('should call onClick when clicked', () => { - const onClick = vi.fn() - render() - - fireEvent.click(screen.getByText('Test Tab')) - expect(onClick).toHaveBeenCalled() - }) - }) -}) - // Uploader Component Tests describe('Uploader', () => { beforeEach(() => { diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx deleted file mode 100644 index 0fd93854d78..00000000000 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/header.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { RiCloseLine } from '@remixicon/react' -import * as React from 'react' -import { useTranslation } from 'react-i18next' - -type HeaderProps = { - onClose: () => void -} - -const Header = ({ onClose }: HeaderProps) => { - const { t } = useTranslation() - - return ( -
- {t(($) => $.importFromDSL, { ns: 'app' })} -
- -
-
- ) -} - -export default React.memo(Header) 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 45279993372..f5f2521e9a1 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,12 +1,13 @@ 'use client' import { Button } from '@langgenius/dify-ui/button' -import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' +import { Field, FieldLabel } from '@langgenius/dify-ui/field' +import { IconButton } from '@langgenius/dify-ui/icon-button' +import { Input } from '@langgenius/dify-ui/input' +import { Tabs, TabsList, TabsPanel, TabsTab } from '@langgenius/dify-ui/tabs' import { useTranslation } from 'react-i18next' -import Input from '@/app/components/base/input' import DSLConfirmModal from './dsl-confirm-modal' -import Header from './header' import { CreateFromDSLModalTab, useDSLImport } from './hooks/use-dsl-import' -import Tab from './tab' import Uploader from './uploader' export { CreateFromDSLModalTab } @@ -53,29 +54,69 @@ const CreateFromDSLModal = ({ <> !open && !showConfirmModal && onClose()}> -
- -
- {currentTab === CreateFromDSLModalTab.FROM_FILE && ( - - )} - {currentTab === CreateFromDSLModalTab.FROM_URL && ( -
-
DSL URL
- $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''} - value={dslUrlValue} - onChange={(e) => setDslUrlValue(e.target.value)} - /> -
- )} -
-
- - +
+ + {t(($) => $.importFromDSL, { ns: 'app' })} + + $['operation.close'], { ns: 'common' })} + className="absolute top-5 right-5" + size="lg" + onClick={onClose} + > +
+
{ + event.preventDefault() + handleCreateApp() + }} + > + { + if (value !== null) setCurrentTab(value as CreateFromDSLModalTab) + }} + > + + + {t(($) => $.importFromDSLFile, { ns: 'app' })} + + + {t(($) => $.importFromDSLUrl, { ns: 'app' })} + + + + + + + + + DSL URL + + $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''} + value={dslUrlValue} + onChange={(e) => setDslUrlValue(e.target.value)} + /> + + + +
+ + +
+
{showConfirmModal && ( diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/index.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/index.tsx deleted file mode 100644 index 5af2e53cb4a..00000000000 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react' -import { useTranslation } from 'react-i18next' -import { CreateFromDSLModalTab } from '../hooks/use-dsl-import' -import Item from './item' - -type TabProps = { - currentTab: CreateFromDSLModalTab - setCurrentTab: (tab: CreateFromDSLModalTab) => void -} - -const Tab = ({ currentTab, setCurrentTab }: TabProps) => { - const { t } = useTranslation() - - const tabs = [ - { - key: CreateFromDSLModalTab.FROM_FILE, - label: t(($) => $.importFromDSLFile, { ns: 'app' }), - }, - { - key: CreateFromDSLModalTab.FROM_URL, - label: t(($) => $.importFromDSLUrl, { ns: 'app' }), - }, - ] - - return ( -
- {tabs.map((tab) => ( - - ))} -
- ) -} - -export default Tab diff --git a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/item.tsx b/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/item.tsx deleted file mode 100644 index 2e076d85866..00000000000 --- a/web/app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/tab/item.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { cn } from '@langgenius/dify-ui/cn' -import * as React from 'react' - -type ItemProps = { - isActive: boolean - label: string - onClick: () => void -} - -const Item = ({ isActive, label, onClick }: ItemProps) => { - return ( -
- {label} - {isActive && ( -
- )} -
- ) -} - -export default React.memo(Item)