feat: enhance FieldList component with sorting and dynamic input field management

This commit is contained in:
twwu 2025-04-22 12:56:22 +08:00
parent 5b8c43052e
commit efb27eb443
2 changed files with 87 additions and 37 deletions

View File

@ -2,12 +2,14 @@ import type { InputVar } from '@/app/components/workflow/types'
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import FieldItem from './field-item' import FieldItem from './field-item'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import InputFieldEditor from '../editor' import InputFieldEditor from '../editor'
import { ReactSortable } from 'react-sortablejs'
type FieldListProps = { type FieldListProps = {
LabelRightContent: React.ReactNode LabelRightContent: React.ReactNode
inputFields?: InputVar[] inputFields: InputVar[]
handleInputFieldsChange: (value: InputVar[]) => void
readonly?: boolean readonly?: boolean
labelClassName?: string labelClassName?: string
} }
@ -15,22 +17,44 @@ type FieldListProps = {
const FieldList = ({ const FieldList = ({
LabelRightContent, LabelRightContent,
inputFields, inputFields,
handleInputFieldsChange,
readonly, readonly,
labelClassName, labelClassName,
}: FieldListProps) => { }: FieldListProps) => {
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false) const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
const optionList = useMemo(() => {
return inputFields.map((content, index) => {
return ({
id: index,
name: content.variable,
})
})
}, [inputFields])
const handleListSortChange = useCallback((list: Array<{ id: number, name: string }>) => {
const newInputFields = list.map((item) => {
return inputFields.find(field => field.variable === item.name)
})
handleInputFieldsChange(newInputFields as InputVar[])
}, [handleInputFieldsChange, inputFields])
const handleRemoveField = useCallback((index: number) => {
const newInputFields = inputFields.filter((_, i) => i !== index)
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange, inputFields])
const handleAddField = () => { const handleAddField = () => {
setShowInputFieldEditor(true) setShowInputFieldEditor(true)
} }
const handleEditField = (index: number) => { const handleEditField = useCallback((index: number) => {
setShowInputFieldEditor(true) setShowInputFieldEditor(true)
} }, [])
const handleCloseEditor = () => { const handleCloseEditor = useCallback(() => {
setShowInputFieldEditor(false) setShowInputFieldEditor(false)
} }, [])
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
@ -48,19 +72,25 @@ const FieldList = ({
<RiAddLine className='h-4 w-4 text-text-tertiary' /> <RiAddLine className='h-4 w-4 text-text-tertiary' />
</button> </button>
</div> </div>
<div className='flex flex-col gap-y-1 px-4 pb-2'> <ReactSortable
className='flex flex-col gap-y-1 px-4 pb-2'
list={optionList}
setList={list => handleListSortChange(list)}
handle='.handle'
ghostClass="opacity-50"
animation={150}
disabled={readonly}
>
{inputFields?.map((item, index) => ( {inputFields?.map((item, index) => (
<FieldItem <FieldItem
key={index} key={index}
readonly={readonly} readonly={readonly}
payload={item} payload={item}
onRemove={() => { onRemove={handleRemoveField.bind(null, index)}
// Handle remove action
}}
onClickEdit={handleEditField.bind(null, index)} onClickEdit={handleEditField.bind(null, index)}
/> />
))} ))}
</div> </ReactSortable>
{showInputFieldEditor && ( {showInputFieldEditor && (
<InputFieldEditor <InputFieldEditor
show={showInputFieldEditor} show={showInputFieldEditor}

View File

@ -1,10 +1,12 @@
import { import {
memo, memo,
useCallback, useCallback,
useState,
} from 'react' } from 'react'
import { useStore } from '@/app/components/workflow/store' import { useStore } from '@/app/components/workflow/store'
import { RiCloseLine } from '@remixicon/react' import { RiCloseLine } from '@remixicon/react'
import { Jina } from '@/app/components/base/icons/src/public/llm' import { Jina } from '@/app/components/base/icons/src/public/llm'
import type { InputVar } from '@/app/components/workflow/types'
import { InputVarType } from '@/app/components/workflow/types' import { InputVarType } from '@/app/components/workflow/types'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import DialogWrapper from './dialog-wrapper' import DialogWrapper from './dialog-wrapper'
@ -13,13 +15,51 @@ import FooterTip from './footer-tip'
type InputFieldDialogProps = { type InputFieldDialogProps = {
readonly?: boolean readonly?: boolean
initialInputFieldsMap?: Record<string, InputVar[]>
} }
const InputFieldDialog = ({ const InputFieldDialog = ({
readonly = false, readonly = false,
initialInputFieldsMap,
}: InputFieldDialogProps) => { }: InputFieldDialogProps) => {
const showInputFieldDialog = useStore(state => state.showInputFieldDialog) const showInputFieldDialog = useStore(state => state.showInputFieldDialog)
const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog) const setShowInputFieldDialog = useStore(state => state.setShowInputFieldDialog)
// TODO: delete mock data
const [inputFieldsMap, setInputFieldsMap] = useState(initialInputFieldsMap || {
jina: [{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}, {
variable: 'num',
label: 'num',
type: InputVarType.number,
required: true,
}],
firecrawl: [{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}],
shared: [{
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}],
})
const updateInputFields = useCallback((key: string, value: InputVar[]) => {
setInputFieldsMap(prev => ({
...prev,
[key]: value,
}))
}, [])
const closePanel = useCallback(() => { const closePanel = useCallback(() => {
setShowInputFieldDialog?.(false) setShowInputFieldDialog?.(false)
@ -58,20 +98,10 @@ const InputFieldDialog = ({
<span className='system-sm-medium text-text-secondary'>Jina Reader</span> <span className='system-sm-medium text-text-secondary'>Jina Reader</span>
</div> </div>
)} )}
inputFields={[{ inputFields={inputFieldsMap.jina}
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}, {
variable: 'num',
label: 'num',
type: InputVarType.number,
required: true,
}]}
readonly={readonly} readonly={readonly}
labelClassName='pt-2 pb-1' labelClassName='pt-2 pb-1'
handleInputFieldsChange={updateInputFields.bind(null, 'jina')}
/> />
{/* Firecrawl Field List */} {/* Firecrawl Field List */}
<FieldList <FieldList
@ -83,15 +113,10 @@ const InputFieldDialog = ({
<span className='system-sm-medium text-text-secondary'>Firecrawl</span> <span className='system-sm-medium text-text-secondary'>Firecrawl</span>
</div> </div>
)} )}
inputFields={[{ inputFields={inputFieldsMap.firecrawl}
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}]}
readonly={readonly} readonly={readonly}
labelClassName='pt-2 pb-1' labelClassName='pt-2 pb-1'
handleInputFieldsChange={updateInputFields.bind(null, 'firecrawl')}
/> />
{/* Shared Inputs */} {/* Shared Inputs */}
<FieldList <FieldList
@ -104,15 +129,10 @@ const InputFieldDialog = ({
/> />
</div> </div>
)} )}
inputFields={[{ inputFields={inputFieldsMap.shared}
variable: 'name',
label: 'name',
type: InputVarType.textInput,
required: true,
max_length: 12,
}]}
readonly={readonly} readonly={readonly}
labelClassName='pt-1 pb-2' labelClassName='pt-1 pb-2'
handleInputFieldsChange={updateInputFields.bind(null, 'shared')}
/> />
</div> </div>
<FooterTip /> <FooterTip />