input field in datasource

pull/21398/head
zxhlyh 12 months ago
parent 365157c37d
commit 69d1e3ec7d

@ -1,5 +1,5 @@
'use client' 'use client'
import React, { useRef } from 'react' import React, { useCallback, useRef } from 'react'
import { useHover } from 'ahooks' import { useHover } from 'ahooks'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { import {
@ -13,12 +13,13 @@ import cn from '@/utils/classnames'
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import type { InputVar } from '@/models/pipeline' import type { InputVar } from '@/models/pipeline'
import type { InputVarType } from '@/app/components/workflow/types' import type { InputVarType } from '@/app/components/workflow/types'
import ActionButton from '@/app/components/base/action-button'
type FieldItemProps = { type FieldItemProps = {
readonly?: boolean readonly?: boolean
payload: InputVar payload: InputVar
onClickEdit: () => void onClickEdit: (id: string) => void
onRemove: () => void onRemove: (id: string) => void
} }
const FieldItem = ({ const FieldItem = ({
@ -32,6 +33,16 @@ const FieldItem = ({
const ref = useRef(null) const ref = useRef(null)
const isHovering = useHover(ref) const isHovering = useHover(ref)
const handleOnClickEdit = useCallback(() => {
if (readonly) return
onClickEdit(payload.variable)
}, [onClickEdit, payload.variable, readonly])
const handleRemove = useCallback(() => {
if (readonly) return
onRemove(payload.variable)
}, [onRemove, payload.variable, readonly])
return ( return (
<div <div
ref={ref} ref={ref}
@ -67,19 +78,17 @@ const FieldItem = ({
{(isHovering && !readonly) {(isHovering && !readonly)
? ( ? (
<div className='flex shrink-0 items-center gap-x-1'> <div className='flex shrink-0 items-center gap-x-1'>
<button <ActionButton
type='button' className='mr-1'
className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover' onClick={handleOnClickEdit}
onClick={onClickEdit}
> >
<RiEditLine className='size-4 text-text-tertiary' /> <RiEditLine className='size-4 text-text-tertiary' />
</button> </ActionButton>
<button <ActionButton
onClick={onRemove} onClick={handleRemove}
className='group cursor-pointer rounded-md p-1 hover:bg-state-destructive-hover'
> >
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' /> <RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
</button> </ActionButton>
</div> </div>
) )
: ( : (

@ -0,0 +1,59 @@
import {
memo,
useMemo,
} from 'react'
import { ReactSortable } from 'react-sortablejs'
import cn from '@/utils/classnames'
import type { InputVar } from '@/models/pipeline'
import FieldItem from './field-item'
import type { SortableItem } from './types'
type FieldListContainerProps = {
className?: string
inputFields: InputVar[]
onListSortChange: (list: SortableItem[]) => void
onRemoveField: (id: string) => void
onEditField: (id: string) => void
readonly?: boolean
}
const FieldListContainer = ({
className,
inputFields,
onListSortChange,
onRemoveField,
onEditField,
readonly,
}: FieldListContainerProps) => {
const list = useMemo(() => {
return inputFields.map((content) => {
return ({
id: content.variable,
name: content.variable,
})
})
}, [inputFields])
return (
<ReactSortable<SortableItem>
className={cn(className)}
list={list}
setList={onListSortChange}
handle='.handle'
ghostClass='opacity-50'
animation={150}
disabled={readonly}
>
{inputFields?.map((item, index) => (
<FieldItem
key={index}
readonly={readonly}
payload={item}
onRemove={onRemoveField}
onClickEdit={onEditField}
/>
))}
</ReactSortable>
)
}
export default memo(FieldListContainer)

@ -0,0 +1,69 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { produce } from 'immer'
import type { InputVar } from '@/models/pipeline'
import type { SortableItem } from './types'
export const useFieldList = (
initialInputFields: InputVar[],
onInputFieldsChange: (value: InputVar[]) => void,
) => {
const [inputFields, setInputFields] = useState<InputVar[]>(initialInputFields)
const inputFieldsRef = useRef<InputVar[]>(inputFields)
const handleInputFieldsChange = useCallback((newInputFields: InputVar[]) => {
setInputFields(newInputFields)
inputFieldsRef.current = newInputFields
onInputFieldsChange(newInputFields)
}, [onInputFieldsChange])
const handleListSortChange = useCallback((list: SortableItem[]) => {
const newInputFields = list.map((item) => {
return inputFieldsRef.current.find(field => field.variable === item.name)
})
handleInputFieldsChange(newInputFields as InputVar[])
}, [handleInputFieldsChange])
const [editingField, setEditingField] = useState<InputVar | undefined>()
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
const handleOpenInputFieldEditor = useCallback((id?: string) => {
const fieldToEdit = inputFieldsRef.current.find(field => field.variable === id)
setEditingField(fieldToEdit)
setShowInputFieldEditor(true)
}, [])
const handleCancelInputFieldEditor = useCallback(() => {
setShowInputFieldEditor(false)
setEditingField(undefined)
}, [])
const handleRemoveField = useCallback((id: string) => {
const newInputFields = inputFieldsRef.current.filter(field => field.variable !== id)
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange])
const handleSubmitField = useCallback((data: InputVar) => {
const newInputFields = produce(inputFieldsRef.current, (draft) => {
const currentIndex = draft.findIndex(field => field.variable === data.variable)
if (currentIndex === -1) {
draft.push(data)
return
}
draft[currentIndex] = data
})
handleInputFieldsChange(newInputFields)
}, [handleInputFieldsChange])
return {
inputFields,
handleInputFieldsChange,
handleListSortChange,
handleRemoveField,
handleSubmitField,
editingField,
showInputFieldEditor,
handleOpenInputFieldEditor,
handleCancelInputFieldEditor,
}
}

@ -1,11 +1,10 @@
import { RiAddLine } from '@remixicon/react' import { RiAddLine } from '@remixicon/react'
import FieldItem from './field-item'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useCallback, useMemo, useState } from 'react'
import InputFieldEditor from '../editor' import InputFieldEditor from '../editor'
import { ReactSortable } from 'react-sortablejs'
import produce from 'immer'
import type { InputVar } from '@/models/pipeline' import type { InputVar } from '@/models/pipeline'
import ActionButton from '@/app/components/base/action-button'
import { useFieldList } from './hooks'
import FieldListContainer from './field-list-container'
type FieldListProps = { type FieldListProps = {
LabelRightContent: React.ReactNode LabelRightContent: React.ReactNode
@ -17,62 +16,21 @@ type FieldListProps = {
const FieldList = ({ const FieldList = ({
LabelRightContent, LabelRightContent,
inputFields, inputFields: initialInputFields,
handleInputFieldsChange, handleInputFieldsChange: onInputFieldsChange,
readonly, readonly,
labelClassName, labelClassName,
}: FieldListProps) => { }: FieldListProps) => {
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false) const {
const [currentIndex, setCurrentIndex] = useState<number>(-1) inputFields,
const [currentInputField, setCurrentInputField] = useState<InputVar>() handleSubmitField,
handleListSortChange,
const optionList = useMemo(() => { handleRemoveField,
return inputFields.map((content, index) => { handleCancelInputFieldEditor,
return ({ handleOpenInputFieldEditor,
id: index, showInputFieldEditor,
name: content.variable, editingField,
}) } = useFieldList(initialInputFields, onInputFieldsChange)
})
}, [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 = () => {
setCurrentIndex(-1) // -1 means add new field
setCurrentInputField(undefined)
setShowInputFieldEditor(true)
}
const handleEditField = useCallback((index: number) => {
setCurrentIndex(index)
setCurrentInputField(inputFields[index])
setShowInputFieldEditor(true)
}, [inputFields])
const handleSubmitChange = useCallback((data: InputVar) => {
const newInputFields = produce(inputFields, (draft) => {
if (currentIndex === -1) {
draft.push(data)
return
}
draft[currentIndex] = data
})
handleInputFieldsChange(newInputFields)
}, [currentIndex, handleInputFieldsChange, inputFields])
const handleCloseEditor = useCallback(() => {
setShowInputFieldEditor(false)
}, [])
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>
@ -80,41 +38,27 @@ const FieldList = ({
<div className='grow'> <div className='grow'>
{LabelRightContent} {LabelRightContent}
</div> </div>
<button <ActionButton
type='button' onClick={() => handleOpenInputFieldEditor()}
className='h-6 px-2 py-1 disabled:cursor-not-allowed'
onClick={handleAddField}
disabled={readonly} disabled={readonly}
aria-disabled={readonly}
> >
<RiAddLine className='h-4 w-4 text-text-tertiary' /> <RiAddLine className='h-4 w-4 text-text-tertiary' />
</button> </ActionButton>
</div> </div>
<ReactSortable <FieldListContainer
className='flex flex-col gap-y-1 px-4 pb-2' className='flex flex-col gap-y-1 px-4 pb-2'
list={optionList} inputFields={inputFields}
setList={list => handleListSortChange(list)} onEditField={handleOpenInputFieldEditor}
handle='.handle' onRemoveField={handleRemoveField}
ghostClass='opacity-50' onListSortChange={handleListSortChange}
animation={150}
disabled={readonly}
>
{inputFields?.map((item, index) => (
<FieldItem
key={index}
readonly={readonly} readonly={readonly}
payload={item}
onRemove={handleRemoveField.bind(null, index)}
onClickEdit={handleEditField.bind(null, index)}
/> />
))}
</ReactSortable>
{showInputFieldEditor && ( {showInputFieldEditor && (
<InputFieldEditor <InputFieldEditor
show={showInputFieldEditor} show={showInputFieldEditor}
initialData={currentInputField} initialData={editingField}
onSubmit={handleSubmitChange} onSubmit={handleSubmitField}
onClose={handleCloseEditor} onClose={handleCancelInputFieldEditor}
/> />
)} )}
</div> </div>

@ -0,0 +1,4 @@
export type SortableItem = {
id: string
name: string
}

@ -11,6 +11,7 @@ const metaData = genNodeMetaData({
const nodeDefault: NodeDefault<DataSourceNodeType> = { const nodeDefault: NodeDefault<DataSourceNodeType> = {
metaData, metaData,
defaultValue: { defaultValue: {
variables: [],
datasource_parameters: {}, datasource_parameters: {},
datasource_configurations: {}, datasource_configurations: {},
}, },

@ -1,6 +1,7 @@
import { useCallback } from 'react' import { useCallback } from 'react'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import { useNodeDataUpdate } from '@/app/components/workflow/hooks' import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
import type { InputVar } from '@/models/pipeline'
import type { DataSourceNodeType } from '../types' import type { DataSourceNodeType } from '../types'
export const useConfig = (id: string) => { export const useConfig = (id: string) => {
@ -28,7 +29,16 @@ export const useConfig = (id: string) => {
}) })
}, [handleNodeDataUpdate, getNodeData]) }, [handleNodeDataUpdate, getNodeData])
const handleInputFieldVariablesChange = useCallback((variables: InputVar[]) => {
const nodeData = getNodeData()
handleNodeDataUpdate({
...nodeData?.data,
variables,
})
}, [handleNodeDataUpdate, getNodeData])
return { return {
handleFileExtensionsChange, handleFileExtensionsChange,
handleInputFieldVariablesChange,
} }
} }

@ -1,25 +1,79 @@
import type { FC } from 'react' import type { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { memo } from 'react' import { memo } from 'react'
import { RiAddLine } from '@remixicon/react'
import type { DataSourceNodeType } from './types' import type { DataSourceNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout' import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import TagInput from '@/app/components/base/tag-input' import TagInput from '@/app/components/base/tag-input'
import FieldList from '@/app/components/rag-pipeline/components/input-field/field-list/field-list-container'
import { useFieldList } from '@/app/components/rag-pipeline/components/input-field/field-list/hooks'
import InputFieldEditor from '@/app/components/rag-pipeline/components/input-field/editor'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useConfig } from './hooks/use-config' import { useConfig } from './hooks/use-config'
import { OUTPUT_VARIABLES_MAP } from './constants' import { OUTPUT_VARIABLES_MAP } from './constants'
import ActionButton from '@/app/components/base/action-button'
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => { const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation() const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()
const { const {
variables,
provider_type, provider_type,
fileExtensions = [], fileExtensions = [],
} = data } = data
const { handleFileExtensionsChange } = useConfig(id) const {
handleInputFieldVariablesChange,
handleFileExtensionsChange,
} = useConfig(id)
const isLocalFile = provider_type === 'local_file' const isLocalFile = provider_type === 'local_file'
const {
inputFields,
handleListSortChange,
handleRemoveField,
handleOpenInputFieldEditor,
showInputFieldEditor,
editingField,
handleSubmitField,
handleCancelInputFieldEditor,
} = useFieldList(variables, handleInputFieldVariablesChange)
return ( return (
<div > <div >
{
!isLocalFile && (
<BoxGroupField
boxGroupProps={{
boxProps: { withBorderBottom: true },
}}
fieldProps={{
fieldTitleProps: {
title: t('workflow.nodes.common.inputVars'),
operation: (
<ActionButton
onClick={(e) => {
e.stopPropagation()
handleOpenInputFieldEditor()
}}
>
<RiAddLine className='h-4 w-4' />
</ActionButton>
),
},
supportCollapse: true,
}}
>
<FieldList
inputFields={inputFields}
readonly={nodesReadOnly}
onListSortChange={handleListSortChange}
onRemoveField={handleRemoveField}
onEditField={handleOpenInputFieldEditor}
/>
</BoxGroupField>
)
}
{ {
isLocalFile && ( isLocalFile && (
<BoxGroupField <BoxGroupField
@ -64,6 +118,14 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
) )
} }
</OutputVars> </OutputVars>
{showInputFieldEditor && (
<InputFieldEditor
show={showInputFieldEditor}
initialData={editingField}
onSubmit={handleSubmitField}
onClose={handleCancelInputFieldEditor}
/>
)}
</div> </div>
) )
} }

Loading…
Cancel
Save