refactor(web): migrate pipeline publishing input form (#41129)

This commit is contained in:
yyh
2026-08-24 04:05:39 +00:00
committed by GitHub
parent 429edf4d82
commit 7b3d97cb42
3 changed files with 99 additions and 85 deletions
-13
View File
@@ -2929,14 +2929,6 @@
"count": 25
}
},
"web/app/components/rag-pipeline/components/__tests__/publish-as-knowledge-pipeline-modal.spec.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
},
"jsx-a11y/no-static-element-interactions": {
"count": 1
}
},
"web/app/components/rag-pipeline/components/panel/input-field/editor/form/__tests__/hidden-fields.spec.tsx": {
"eslint-react/static-components": {
"count": 4
@@ -3034,11 +3026,6 @@
"count": 4
}
},
"web/app/components/rag-pipeline/components/publish-as-knowledge-pipeline-modal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/rag-pipeline/components/publish-toast.tsx": {
"jsx-a11y/click-events-have-key-events": {
"count": 1
@@ -1,4 +1,5 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vite-plus/test'
import PublishAsKnowledgePipelineModal from '../publish-as-knowledge-pipeline-modal'
@@ -16,25 +17,8 @@ vi.mock('@/app/components/workflow/store', () => ({
}),
}))
vi.mock('@/app/components/base/input', () => ({
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
<input
data-testid="name-input"
value={value as string}
onChange={onChange as () => void}
{...props}
/>
),
}))
vi.mock('@/app/components/base/app-icon', () => ({
default: ({ onClick }: { onClick?: () => void }) => (
<div data-testid="app-icon" onClick={onClick} />
),
}))
vi.mock('es-toolkit/function', () => ({
noop: () => {},
default: () => <div />,
}))
describe('PublishAsKnowledgePipelineModal', () => {
@@ -45,6 +29,12 @@ describe('PublishAsKnowledgePipelineModal', () => {
onCancel: mockOnCancel,
onConfirm: mockOnConfirm,
}
const getNameInput = () =>
screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.name' })
const getIconButton = () =>
screen.getByRole('button', {
name: 'common.operation.edit pipeline.common.publishAsPipeline.name',
})
beforeEach(() => {
vi.clearAllMocks()
@@ -57,14 +47,13 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should render modal with title', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
expect(screen.getByRole('dialog')).toBeInTheDocument()
expect(screen.getByText('pipeline.common.publishAs')).toBeInTheDocument()
expect(screen.getByRole('dialog', { name: 'pipeline.common.publishAs' })).toBeInTheDocument()
})
it('should initialize with knowledgeName from store', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const nameInput = screen.getByTestId('name-input') as HTMLInputElement
const nameInput = getNameInput() as HTMLInputElement
expect(nameInput.value).toBe('Test Pipeline')
})
@@ -93,6 +82,15 @@ describe('PublishAsKnowledgePipelineModal', () => {
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call onCancel when Escape is pressed', async () => {
const user = userEvent.setup()
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
await user.keyboard('{Escape}')
expect(mockOnCancel).toHaveBeenCalled()
})
it('should call onConfirm with name, icon, and description when confirm clicked', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
@@ -108,7 +106,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should update pipeline name when input changes', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const nameInput = screen.getByTestId('name-input')
const nameInput = getNameInput()
fireEvent.change(nameInput, { target: { value: 'New Name' } })
expect((nameInput as HTMLInputElement).value).toBe('New Name')
@@ -125,14 +123,20 @@ describe('PublishAsKnowledgePipelineModal', () => {
expect((textarea as HTMLTextAreaElement).value).toBe('My description')
})
it('should disable confirm button when name is empty', () => {
it('should not submit with Enter when name is empty', async () => {
const user = userEvent.setup()
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const nameInput = screen.getByTestId('name-input')
fireEvent.change(nameInput, { target: { value: '' } })
const nameInput = getNameInput()
await user.clear(nameInput)
const confirmBtn = screen.getByText('workflow.common.publish')
expect(confirmBtn).toBeDisabled()
await user.click(nameInput)
await user.keyboard('{Enter}')
expect(mockOnConfirm).not.toHaveBeenCalled()
})
it('should disable confirm button when confirmDisabled is true', () => {
@@ -155,7 +159,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
expect(screen.queryByPlaceholderText('Search emojis...')).not.toBeInTheDocument()
fireEvent.click(screen.getByTestId('app-icon'))
fireEvent.click(getIconButton())
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
})
@@ -163,7 +167,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should update icon when emoji style is selected', async () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('app-icon'))
fireEvent.click(getIconButton())
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.ok/ }))
@@ -175,7 +179,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should keep icon picker open until confirmation', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('app-icon'))
fireEvent.click(getIconButton())
fireEvent.click(screen.getByRole('button', { name: '#E4FBCC' }))
expect(screen.getByPlaceholderText('Search emojis...')).toBeInTheDocument()
@@ -184,7 +188,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should close icon picker when cancel is clicked', async () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
fireEvent.click(screen.getByTestId('app-icon'))
fireEvent.click(getIconButton())
fireEvent.click(screen.getByRole('button', { name: /iconPicker\.cancel/ }))
await waitFor(() => {
@@ -192,18 +196,21 @@ describe('PublishAsKnowledgePipelineModal', () => {
})
})
it('should trim name and description before submitting', () => {
it('should trim name and description when submitted with Enter', async () => {
const user = userEvent.setup()
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const nameInput = screen.getByTestId('name-input')
fireEvent.change(nameInput, { target: { value: ' Trimmed Name ' } })
const nameInput = getNameInput()
await user.clear(nameInput)
await user.type(nameInput, ' Trimmed Name ')
const textarea = screen.getByRole('textbox', {
name: 'pipeline.common.publishAsPipeline.description',
})
fireEvent.change(textarea, { target: { value: ' Some desc ' } })
await user.type(textarea, ' Some desc ')
fireEvent.click(screen.getByText('workflow.common.publish'))
await user.click(nameInput)
await user.keyboard('{Enter}')
expect(mockOnConfirm).toHaveBeenCalledWith('Trimmed Name', expect.any(Object), 'Some desc')
})
@@ -2,14 +2,15 @@
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { IconInfo } from '@/models/datasets'
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 { Textarea } from '@langgenius/dify-ui/textarea'
import { RiCloseLine } from '@remixicon/react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input'
import { useWorkflowStore } from '@/app/components/workflow/store'
type PublishAsKnowledgePipelineModalProps = {
@@ -50,33 +51,47 @@ const PublishAsKnowledgePipelineModal = ({
}, [])
const handleConfirm = () => {
if (confirmDisabled) return
const name = pipelineName?.trim()
if (!name || confirmDisabled) return
onConfirm(pipelineName?.trim() || '', pipelineIcon, description?.trim())
onConfirm(name, pipelineIcon, description?.trim())
}
return (
<>
<Dialog open>
<DialogContent className="w-full max-w-120! overflow-hidden! border-none p-0! text-left align-middle">
<div className="relative flex items-center p-6 pr-14 pb-3 title-2xl-semi-bold text-text-primary">
{t(($) => $['common.publishAs'], { ns: 'pipeline' })}
<button
type="button"
<Dialog
open
onOpenChange={(open) => {
if (!open) onCancel()
}}
>
<DialogContent className="w-full max-w-120! overflow-hidden! border-none p-0! text-left align-middle">
<form
onSubmit={(event) => {
event.preventDefault()
handleConfirm()
}}
>
<div className="relative flex items-center p-6 pr-14 pb-3">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t(($) => $['common.publishAs'], { ns: 'pipeline' })}
</DialogTitle>
<IconButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
className="absolute top-5 right-5 flex size-8 cursor-pointer items-center justify-center border-none bg-transparent p-0"
size="lg"
className="absolute top-5 right-5"
onClick={onCancel}
>
<RiCloseLine className="size-4 text-text-tertiary" aria-hidden="true" />
</button>
<span aria-hidden="true" className="i-ri-close-line size-4" />
</IconButton>
</div>
<div className="px-6 py-3">
<div className="mb-5 flex">
<div className="mr-3 grow">
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
<Field className="mr-3 grow" name="name">
<FieldLabel>
{t(($) => $['common.publishAsPipeline.name'], { ns: 'pipeline' })}
</div>
</FieldLabel>
<Input
autoComplete="off"
value={pipelineName}
onChange={(e) => setPipelineName(e.target.value)}
placeholder={
@@ -84,26 +99,31 @@ const PublishAsKnowledgePipelineModal = ({
''
}
/>
</div>
<AppIcon
size="xxl"
</Field>
<button
type="button"
aria-label={`${t(($) => $['operation.edit'], { ns: 'common' })} ${t(($) => $['common.publishAsPipeline.name'], { ns: 'pipeline' })}`}
onClick={() => {
setShowAppIconPicker(true)
}}
className="mt-2 shrink-0 cursor-pointer"
iconType={pipelineIcon?.icon_type}
icon={pipelineIcon?.icon}
background={pipelineIcon?.icon_background}
imageUrl={pipelineIcon?.icon_url}
/>
className="mt-2 shrink-0 cursor-pointer rounded-2xl focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
>
<AppIcon
size="xxl"
iconType={pipelineIcon?.icon_type}
icon={pipelineIcon?.icon}
background={pipelineIcon?.icon_background}
imageUrl={pipelineIcon?.icon_url}
/>
</button>
</div>
<div>
<div className="mb-1 flex h-6 items-center system-sm-medium text-text-secondary">
<Field name="description">
<FieldLabel>
{t(($) => $['common.publishAsPipeline.description'], { ns: 'pipeline' })}
</div>
</FieldLabel>
<Textarea
autoComplete="off"
className="resize-none"
aria-label={t(($) => $['common.publishAsPipeline.description'], { ns: 'pipeline' })}
placeholder={
t(($) => $['common.publishAsPipeline.descriptionPlaceholder'], {
ns: 'pipeline',
@@ -112,16 +132,16 @@ const PublishAsKnowledgePipelineModal = ({
value={description}
onValueChange={(value) => setDescription(value)}
/>
</div>
</Field>
</div>
<div className="flex items-center justify-end px-6 py-5">
<Button className="mr-2" onClick={onCancel}>
<div className="flex items-center justify-end gap-2 px-6 py-5">
<Button type="button" onClick={onCancel}>
{t(($) => $['operation.cancel'], { ns: 'common' })}
</Button>
<Button
type="submit"
disabled={!pipelineName?.trim() || confirmDisabled}
variant="primary"
onClick={() => handleConfirm()}
>
{t(($) => $['common.publish'], { ns: 'workflow' })}
</Button>
@@ -138,9 +158,9 @@ const PublishAsKnowledgePipelineModal = ({
onSelect={handleSelectIcon}
/>
)}
</DialogContent>
</Dialog>
</>
</form>
</DialogContent>
</Dialog>
)
}