mirror of
https://github.com/langgenius/dify.git
synced 2026-08-31 01:36:38 +08:00
refactor(web): migrate pipeline DSL import form (#41232)
This commit is contained in:
@@ -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": {
|
||||
|
||||
+43
-146
@@ -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(<CreateFromDSLModal show={true} onClose={vi.fn()} />, { 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(<CreateFromDSLModal show={true} onClose={vi.fn()} />, { 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(<CreateFromDSLModal show={true} onClose={onClose} />, { 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(
|
||||
<CreateFromDSLModal
|
||||
show={true}
|
||||
onClose={vi.fn()}
|
||||
activeTab={CreateFromDSLModalTab.FROM_URL}
|
||||
/>,
|
||||
{ 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(<Header onClose={vi.fn()} />)
|
||||
expect(screen.getByText('app.importFromDSL'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render close icon', () => {
|
||||
render(<Header onClose={vi.fn()} />)
|
||||
// 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(<Header onClose={onClose} />)
|
||||
|
||||
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(<Tab currentTab={CreateFromDSLModalTab.FROM_FILE} setCurrentTab={vi.fn()} />)
|
||||
|
||||
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(<Tab currentTab={CreateFromDSLModalTab.FROM_URL} setCurrentTab={setCurrentTab} />)
|
||||
|
||||
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(<Tab currentTab={CreateFromDSLModalTab.FROM_FILE} setCurrentTab={setCurrentTab} />)
|
||||
|
||||
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(<TabItem isActive={false} label="Test Tab" onClick={vi.fn()} />)
|
||||
|
||||
expect(screen.getByText('Test Tab'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render active indicator when active', () => {
|
||||
render(<TabItem isActive={true} label="Test Tab" onClick={vi.fn()} />)
|
||||
|
||||
// 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(<TabItem isActive={false} label="Test Tab" onClick={vi.fn()} />)
|
||||
|
||||
const indicator = document.querySelector('[class*="bg-util-colors-blue"]')
|
||||
expect(indicator).toBeNull()
|
||||
})
|
||||
|
||||
it('should have active text color when active', () => {
|
||||
render(<TabItem isActive={true} label="Test Tab" onClick={vi.fn()} />)
|
||||
|
||||
const item = screen.getByText('Test Tab')
|
||||
expect(item.className).toContain('text-text-primary')
|
||||
})
|
||||
|
||||
it('should have inactive text color when inactive', () => {
|
||||
render(<TabItem isActive={false} label="Test Tab" onClick={vi.fn()} />)
|
||||
|
||||
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(<TabItem isActive={false} label="Test Tab" onClick={onClick} />)
|
||||
|
||||
fireEvent.click(screen.getByText('Test Tab'))
|
||||
expect(onClick).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Uploader Component Tests
|
||||
describe('Uploader', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
-25
@@ -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 (
|
||||
<div className="relative flex items-center justify-between pt-6 pr-14 pb-3 pl-6 title-2xl-semi-bold text-text-primary">
|
||||
{t(($) => $.importFromDSL, { ns: 'app' })}
|
||||
<div
|
||||
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<RiCloseLine className="size-4.5 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Header)
|
||||
+67
-26
@@ -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 = ({
|
||||
<>
|
||||
<Dialog open={show} onOpenChange={(open) => !open && !showConfirmModal && onClose()}>
|
||||
<DialogContent className="w-full max-w-120! overflow-hidden! rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-0! text-left align-middle shadow-xl">
|
||||
<Header onClose={onClose} />
|
||||
<Tab currentTab={currentTab} setCurrentTab={setCurrentTab} />
|
||||
<div className="px-6 py-4">
|
||||
{currentTab === CreateFromDSLModalTab.FROM_FILE && (
|
||||
<Uploader className="mt-0" file={currentFile} updateFile={handleFile} />
|
||||
)}
|
||||
{currentTab === CreateFromDSLModalTab.FROM_URL && (
|
||||
<div>
|
||||
<div className="leading6 mb-1 system-md-semibold text-text-secondary">DSL URL</div>
|
||||
<Input
|
||||
placeholder={t(($) => $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={(e) => setDslUrlValue(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex justify-end gap-x-2 p-6 pt-5">
|
||||
<Button onClick={onClose}>{t(($) => $['newApp.Cancel'], { ns: 'app' })}</Button>
|
||||
<Button disabled={buttonDisabled} variant="primary" onClick={handleCreateApp}>
|
||||
<span>{t(($) => $['newApp.import'], { ns: 'app' })}</span>
|
||||
</Button>
|
||||
<div className="relative flex items-center justify-between pt-6 pr-14 pb-3 pl-6">
|
||||
<DialogTitle className="title-2xl-semi-bold text-text-primary">
|
||||
{t(($) => $.importFromDSL, { ns: 'app' })}
|
||||
</DialogTitle>
|
||||
<IconButton
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
className="absolute top-5 right-5"
|
||||
size="lg"
|
||||
onClick={onClose}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4.5" />
|
||||
</IconButton>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
handleCreateApp()
|
||||
}}
|
||||
>
|
||||
<Tabs
|
||||
value={currentTab}
|
||||
onValueChange={(value) => {
|
||||
if (value !== null) setCurrentTab(value as CreateFromDSLModalTab)
|
||||
}}
|
||||
>
|
||||
<TabsList className="h-9 gap-6 border-b border-divider-subtle px-6">
|
||||
<TabsTab value={CreateFromDSLModalTab.FROM_FILE} className="h-full py-0">
|
||||
{t(($) => $.importFromDSLFile, { ns: 'app' })}
|
||||
</TabsTab>
|
||||
<TabsTab value={CreateFromDSLModalTab.FROM_URL} className="h-full py-0">
|
||||
{t(($) => $.importFromDSLUrl, { ns: 'app' })}
|
||||
</TabsTab>
|
||||
</TabsList>
|
||||
<TabsPanel
|
||||
value={CreateFromDSLModalTab.FROM_FILE}
|
||||
tabIndex={-1}
|
||||
className="px-6 py-4"
|
||||
>
|
||||
<Uploader className="mt-0" file={currentFile} updateFile={handleFile} />
|
||||
</TabsPanel>
|
||||
<TabsPanel value={CreateFromDSLModalTab.FROM_URL} tabIndex={-1} className="px-6 py-4">
|
||||
<Field name="dslUrl">
|
||||
<FieldLabel className="w-full py-0 text-sm leading-5 font-semibold">
|
||||
DSL URL
|
||||
</FieldLabel>
|
||||
<Input
|
||||
autoComplete="off"
|
||||
placeholder={t(($) => $.importFromDSLUrlPlaceholder, { ns: 'app' }) || ''}
|
||||
value={dslUrlValue}
|
||||
onChange={(e) => setDslUrlValue(e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
</TabsPanel>
|
||||
</Tabs>
|
||||
<div className="flex justify-end gap-2 p-6 pt-5">
|
||||
<Button type="button" onClick={onClose}>
|
||||
{t(($) => $['newApp.Cancel'], { ns: 'app' })}
|
||||
</Button>
|
||||
<Button type="submit" disabled={buttonDisabled} variant="primary">
|
||||
{t(($) => $['newApp.import'], { ns: 'app' })}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{showConfirmModal && (
|
||||
|
||||
-39
@@ -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 (
|
||||
<div className="flex h-9 items-center gap-x-6 border-b border-divider-subtle px-6 system-md-semibold text-text-tertiary">
|
||||
{tabs.map((tab) => (
|
||||
<Item
|
||||
key={tab.key}
|
||||
isActive={currentTab === tab.key}
|
||||
label={tab.label}
|
||||
onClick={setCurrentTab.bind(null, tab.key)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tab
|
||||
-27
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex h-full cursor-pointer items-center system-md-semibold text-text-tertiary',
|
||||
isActive && 'text-text-primary',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{label}
|
||||
{isActive && (
|
||||
<div className="absolute bottom-0 h-0.5 w-full bg-util-colors-blue-brand-blue-brand-600" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Item)
|
||||
Reference in New Issue
Block a user