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 = ({
<>