mirror of
https://github.com/langgenius/dify.git
synced 2025-12-27 10:02:25 +00:00
input field in datasource
This commit is contained in:
parent
365157c37d
commit
69d1e3ec7d
@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import React, { useRef } from 'react'
|
||||
import React, { useCallback, useRef } from 'react'
|
||||
import { useHover } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@ -13,12 +13,13 @@ import cn from '@/utils/classnames'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { InputVarType } from '@/app/components/workflow/types'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
type FieldItemProps = {
|
||||
readonly?: boolean
|
||||
payload: InputVar
|
||||
onClickEdit: () => void
|
||||
onRemove: () => void
|
||||
onClickEdit: (id: string) => void
|
||||
onRemove: (id: string) => void
|
||||
}
|
||||
|
||||
const FieldItem = ({
|
||||
@ -32,6 +33,16 @@ const FieldItem = ({
|
||||
const ref = useRef(null)
|
||||
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 (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -67,19 +78,17 @@ const FieldItem = ({
|
||||
{(isHovering && !readonly)
|
||||
? (
|
||||
<div className='flex shrink-0 items-center gap-x-1'>
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer rounded-md p-1 hover:bg-state-base-hover'
|
||||
onClick={onClickEdit}
|
||||
<ActionButton
|
||||
className='mr-1'
|
||||
onClick={handleOnClickEdit}
|
||||
>
|
||||
<RiEditLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
<button
|
||||
onClick={onRemove}
|
||||
className='group cursor-pointer rounded-md p-1 hover:bg-state-destructive-hover'
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
onClick={handleRemove}
|
||||
>
|
||||
<RiDeleteBinLine className='size-4 text-text-tertiary group-hover:text-text-destructive' />
|
||||
</button>
|
||||
</ActionButton>
|
||||
</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 FieldItem from './field-item'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import InputFieldEditor from '../editor'
|
||||
import { ReactSortable } from 'react-sortablejs'
|
||||
import produce from 'immer'
|
||||
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 = {
|
||||
LabelRightContent: React.ReactNode
|
||||
@ -17,62 +16,21 @@ type FieldListProps = {
|
||||
|
||||
const FieldList = ({
|
||||
LabelRightContent,
|
||||
inputFields,
|
||||
handleInputFieldsChange,
|
||||
inputFields: initialInputFields,
|
||||
handleInputFieldsChange: onInputFieldsChange,
|
||||
readonly,
|
||||
labelClassName,
|
||||
}: FieldListProps) => {
|
||||
const [showInputFieldEditor, setShowInputFieldEditor] = useState(false)
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(-1)
|
||||
const [currentInputField, setCurrentInputField] = useState<InputVar>()
|
||||
|
||||
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 = () => {
|
||||
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)
|
||||
}, [])
|
||||
const {
|
||||
inputFields,
|
||||
handleSubmitField,
|
||||
handleListSortChange,
|
||||
handleRemoveField,
|
||||
handleCancelInputFieldEditor,
|
||||
handleOpenInputFieldEditor,
|
||||
showInputFieldEditor,
|
||||
editingField,
|
||||
} = useFieldList(initialInputFields, onInputFieldsChange)
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
@ -80,41 +38,27 @@ const FieldList = ({
|
||||
<div className='grow'>
|
||||
{LabelRightContent}
|
||||
</div>
|
||||
<button
|
||||
type='button'
|
||||
className='h-6 px-2 py-1 disabled:cursor-not-allowed'
|
||||
onClick={handleAddField}
|
||||
<ActionButton
|
||||
onClick={() => handleOpenInputFieldEditor()}
|
||||
disabled={readonly}
|
||||
aria-disabled={readonly}
|
||||
>
|
||||
<RiAddLine className='h-4 w-4 text-text-tertiary' />
|
||||
</button>
|
||||
</ActionButton>
|
||||
</div>
|
||||
<ReactSortable
|
||||
<FieldListContainer
|
||||
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) => (
|
||||
<FieldItem
|
||||
key={index}
|
||||
readonly={readonly}
|
||||
payload={item}
|
||||
onRemove={handleRemoveField.bind(null, index)}
|
||||
onClickEdit={handleEditField.bind(null, index)}
|
||||
/>
|
||||
))}
|
||||
</ReactSortable>
|
||||
inputFields={inputFields}
|
||||
onEditField={handleOpenInputFieldEditor}
|
||||
onRemoveField={handleRemoveField}
|
||||
onListSortChange={handleListSortChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
{showInputFieldEditor && (
|
||||
<InputFieldEditor
|
||||
show={showInputFieldEditor}
|
||||
initialData={currentInputField}
|
||||
onSubmit={handleSubmitChange}
|
||||
onClose={handleCloseEditor}
|
||||
initialData={editingField}
|
||||
onSubmit={handleSubmitField}
|
||||
onClose={handleCancelInputFieldEditor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export type SortableItem = {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
@ -11,6 +11,7 @@ const metaData = genNodeMetaData({
|
||||
const nodeDefault: NodeDefault<DataSourceNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
datasource_parameters: {},
|
||||
datasource_configurations: {},
|
||||
},
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useNodeDataUpdate } from '@/app/components/workflow/hooks'
|
||||
import type { InputVar } from '@/models/pipeline'
|
||||
import type { DataSourceNodeType } from '../types'
|
||||
|
||||
export const useConfig = (id: string) => {
|
||||
@ -28,7 +29,16 @@ export const useConfig = (id: string) => {
|
||||
})
|
||||
}, [handleNodeDataUpdate, getNodeData])
|
||||
|
||||
const handleInputFieldVariablesChange = useCallback((variables: InputVar[]) => {
|
||||
const nodeData = getNodeData()
|
||||
handleNodeDataUpdate({
|
||||
...nodeData?.data,
|
||||
variables,
|
||||
})
|
||||
}, [handleNodeDataUpdate, getNodeData])
|
||||
|
||||
return {
|
||||
handleFileExtensionsChange,
|
||||
handleInputFieldVariablesChange,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,25 +1,79 @@
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { memo } from 'react'
|
||||
import { RiAddLine } from '@remixicon/react'
|
||||
import type { DataSourceNodeType } from './types'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
import { BoxGroupField } from '@/app/components/workflow/nodes/_base/components/layout'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
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 { OUTPUT_VARIABLES_MAP } from './constants'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
|
||||
const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
||||
const { t } = useTranslation()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const {
|
||||
variables,
|
||||
provider_type,
|
||||
fileExtensions = [],
|
||||
} = data
|
||||
const { handleFileExtensionsChange } = useConfig(id)
|
||||
const {
|
||||
handleInputFieldVariablesChange,
|
||||
handleFileExtensionsChange,
|
||||
} = useConfig(id)
|
||||
const isLocalFile = provider_type === 'local_file'
|
||||
const {
|
||||
inputFields,
|
||||
handleListSortChange,
|
||||
handleRemoveField,
|
||||
handleOpenInputFieldEditor,
|
||||
showInputFieldEditor,
|
||||
editingField,
|
||||
handleSubmitField,
|
||||
handleCancelInputFieldEditor,
|
||||
} = useFieldList(variables, handleInputFieldVariablesChange)
|
||||
|
||||
return (
|
||||
<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 && (
|
||||
<BoxGroupField
|
||||
@ -64,6 +118,14 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
||||
)
|
||||
}
|
||||
</OutputVars>
|
||||
{showInputFieldEditor && (
|
||||
<InputFieldEditor
|
||||
show={showInputFieldEditor}
|
||||
initialData={editingField}
|
||||
onSubmit={handleSubmitField}
|
||||
onClose={handleCancelInputFieldEditor}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user