mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
feat(web): snippet input field panel layout
This commit is contained in:
parent
9636472db7
commit
985c3db4fd
@ -79,6 +79,13 @@ vi.mock('@/hooks/use-breakpoints', () => ({
|
|||||||
MediaType: { mobile: 'mobile', desktop: 'desktop' },
|
MediaType: { mobile: 'mobile', desktop: 'desktop' },
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/hooks', () => ({
|
||||||
|
useFloatingRight: () => ({
|
||||||
|
floatingRight: false,
|
||||||
|
floatingRightWidth: 400,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
vi.mock('@/app/components/workflow', () => ({
|
vi.mock('@/app/components/workflow', () => ({
|
||||||
default: ({ children }: { children: React.ReactNode }) => (
|
default: ({ children }: { children: React.ReactNode }) => (
|
||||||
<div data-testid="workflow-default-context">{children}</div>
|
<div data-testid="workflow-default-context">{children}</div>
|
||||||
|
|||||||
@ -0,0 +1,80 @@
|
|||||||
|
import type { InputVar } from '@/models/pipeline'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { PipelineInputVarType } from '@/models/pipeline'
|
||||||
|
import SnippetInputFieldEditor from '../input-field-editor'
|
||||||
|
|
||||||
|
const mockUseFloatingRight = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/hooks', () => ({
|
||||||
|
useFloatingRight: (...args: unknown[]) => mockUseFloatingRight(...args),
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/app/components/rag-pipeline/components/panel/input-field/editor/form', () => ({
|
||||||
|
default: ({ isEditMode }: { isEditMode: boolean }) => (
|
||||||
|
<div data-testid="snippet-input-field-form">{isEditMode ? 'edit' : 'create'}</div>
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const createField = (overrides: Partial<InputVar> = {}): InputVar => ({
|
||||||
|
type: PipelineInputVarType.textInput,
|
||||||
|
label: 'Blog URL',
|
||||||
|
variable: 'blog_url',
|
||||||
|
required: true,
|
||||||
|
options: [],
|
||||||
|
placeholder: 'Paste a source article URL',
|
||||||
|
max_length: 256,
|
||||||
|
...overrides,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('SnippetInputFieldEditor', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockUseFloatingRight.mockReturnValue({
|
||||||
|
floatingRight: false,
|
||||||
|
floatingRightWidth: 400,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Verifies the default desktop layout keeps the editor inline with the panel.
|
||||||
|
describe('Rendering', () => {
|
||||||
|
it('should render the add title without floating positioning by default', () => {
|
||||||
|
render(
|
||||||
|
<SnippetInputFieldEditor
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const title = screen.getByText('datasetPipeline.inputFieldPanel.addInputField')
|
||||||
|
const editor = title.parentElement
|
||||||
|
|
||||||
|
expect(title).toBeInTheDocument()
|
||||||
|
expect(editor).not.toHaveClass('absolute')
|
||||||
|
expect(editor).toHaveStyle({ width: 'min(400px, calc(100vw - 24px))' })
|
||||||
|
expect(mockUseFloatingRight).toHaveBeenCalledWith(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should float over the panel when there is not enough room', () => {
|
||||||
|
mockUseFloatingRight.mockReturnValue({
|
||||||
|
floatingRight: true,
|
||||||
|
floatingRightWidth: 320,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(
|
||||||
|
<SnippetInputFieldEditor
|
||||||
|
field={createField()}
|
||||||
|
onClose={vi.fn()}
|
||||||
|
onSubmit={vi.fn()}
|
||||||
|
/>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const title = screen.getByText('datasetPipeline.inputFieldPanel.editInputField')
|
||||||
|
const editor = title.parentElement
|
||||||
|
|
||||||
|
expect(title).toBeInTheDocument()
|
||||||
|
expect(editor).toHaveClass('absolute', 'right-0', 'z-[100]')
|
||||||
|
expect(editor).toHaveStyle({ width: 'min(320px, calc(100vw - 24px))' })
|
||||||
|
expect(screen.getByTestId('snippet-input-field-form')).toHaveTextContent('edit')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
import type { FormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
|
import type { FormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/types'
|
||||||
import type { SnippetInputField } from '@/models/snippet'
|
import type { SnippetInputField } from '@/models/snippet'
|
||||||
|
import { RiCloseLine } from '@remixicon/react'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import InputFieldForm from '@/app/components/rag-pipeline/components/panel/input-field/editor/form'
|
import InputFieldForm from '@/app/components/rag-pipeline/components/panel/input-field/editor/form'
|
||||||
import { convertFormDataToINputField, convertToInputFieldFormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/utils'
|
import { convertFormDataToINputField, convertToInputFieldFormData } from '@/app/components/rag-pipeline/components/panel/input-field/editor/utils'
|
||||||
|
import { useFloatingRight } from '@/app/components/rag-pipeline/components/panel/input-field/hooks'
|
||||||
|
import { cn } from '@/utils/classnames'
|
||||||
|
|
||||||
type SnippetInputFieldEditorProps = {
|
type SnippetInputFieldEditorProps = {
|
||||||
field?: SnippetInputField | null
|
field?: SnippetInputField | null
|
||||||
@ -19,6 +22,7 @@ const SnippetInputFieldEditor = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: SnippetInputFieldEditorProps) => {
|
}: SnippetInputFieldEditorProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { floatingRight, floatingRightWidth } = useFloatingRight(400)
|
||||||
|
|
||||||
const initialData = useMemo(() => {
|
const initialData = useMemo(() => {
|
||||||
return convertToInputFieldFormData(field || undefined)
|
return convertToInputFieldFormData(field || undefined)
|
||||||
@ -29,7 +33,16 @@ const SnippetInputFieldEditor = ({
|
|||||||
}, [onSubmit])
|
}, [onSubmit])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative mr-1 flex h-fit max-h-full w-[min(400px,calc(100vw-24px))] flex-col overflow-y-auto rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9">
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative mr-1 flex h-fit max-h-full flex-col overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-2xl shadow-shadow-shadow-9',
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
floatingRight && 'absolute right-0 z-[100]',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
width: `min(${floatingRightWidth}px, calc(100vw - 24px))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary system-xl-semibold">
|
<div className="flex items-center pb-1 pl-4 pr-11 pt-3.5 text-text-primary system-xl-semibold">
|
||||||
{field ? t('inputFieldPanel.editInputField', { ns: 'datasetPipeline' }) : t('inputFieldPanel.addInputField', { ns: 'datasetPipeline' })}
|
{field ? t('inputFieldPanel.editInputField', { ns: 'datasetPipeline' }) : t('inputFieldPanel.addInputField', { ns: 'datasetPipeline' })}
|
||||||
</div>
|
</div>
|
||||||
@ -38,7 +51,7 @@ const SnippetInputFieldEditor = ({
|
|||||||
className="absolute right-2.5 top-2.5 flex h-8 w-8 items-center justify-center"
|
className="absolute right-2.5 top-2.5 flex h-8 w-8 items-center justify-center"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<span aria-hidden className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||||
</button>
|
</button>
|
||||||
<InputFieldForm
|
<InputFieldForm
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
|
|||||||
@ -74,9 +74,16 @@ const SnippetChildren = ({
|
|||||||
onSortChange={onSortChange}
|
onSortChange={onSortChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isInputPanelOpen && (
|
{(isInputPanelOpen || isEditorOpen) && (
|
||||||
<div className="pointer-events-none absolute inset-y-3 right-3 z-30 flex justify-end">
|
<div className="pointer-events-none absolute bottom-1 right-1 top-14 z-30 flex justify-end">
|
||||||
<div className="pointer-events-auto h-full xl:hidden">
|
<div className="pointer-events-auto flex h-full xl:hidden">
|
||||||
|
{isEditorOpen && (
|
||||||
|
<SnippetInputFieldEditor
|
||||||
|
field={editingField}
|
||||||
|
onClose={onCloseEditor}
|
||||||
|
onSubmit={onSubmitField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SnippetInputFieldPanel
|
<SnippetInputFieldPanel
|
||||||
fields={fields}
|
fields={fields}
|
||||||
onClose={onCloseInputPanel}
|
onClose={onCloseInputPanel}
|
||||||
@ -89,17 +96,6 @@ const SnippetChildren = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isEditorOpen && (
|
|
||||||
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center bg-black/10 px-3 xl:hidden">
|
|
||||||
<div className="pointer-events-auto w-full max-w-md">
|
|
||||||
<SnippetInputFieldEditor
|
|
||||||
field={editingField}
|
|
||||||
onClose={onCloseEditor}
|
|
||||||
onSubmit={onSubmitField}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user