feat: add File Upload node functionality and related components

- Implemented File Upload node with support for uploading files to the sandbox.
- Added necessary UI components including node panel and default configurations.
- Enhanced workflow constants and enums to include File Upload.
- Updated error handling for file upload operations.
- Integrated File Upload into existing workflow structure, ensuring compatibility with variable handling and output management.
- Added translations for new File Upload features in workflow.json.
This commit is contained in:
Harry
2026-02-10 20:46:38 +08:00
parent a5271baea0
commit 2da770cdbd
26 changed files with 633 additions and 37 deletions

View File

@ -0,0 +1,35 @@
import type { NodeDefault } from '../../types'
import type { FileUploadNodeType } from './types'
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { genNodeMetaData } from '@/app/components/workflow/utils'
const i18nPrefix = 'errorMsg'
const metaData = genNodeMetaData({
classification: BlockClassificationEnum.Utilities,
sort: 3,
type: BlockEnum.FileUpload,
})
const nodeDefault: NodeDefault<FileUploadNodeType> = {
metaData,
defaultValue: {
variable_selector: [],
is_array_file: false,
},
checkValid(payload: FileUploadNodeType, t: (key: string, options?: Record<string, unknown>) => string) {
let errorMessages = ''
const { variable_selector: variable } = payload
if (!errorMessages && !variable?.length)
errorMessages = t(`${i18nPrefix}.fieldRequired`, { ns: 'workflow', field: t(`${i18nPrefix}.fields.fileVariable`, { ns: 'workflow' }) })
return {
isValid: !errorMessages,
errorMessage: errorMessages,
}
},
}
export default nodeDefault

View File

@ -0,0 +1,12 @@
import type { FC } from 'react'
import type { FileUploadNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
import * as React from 'react'
const Node: FC<NodeProps<FileUploadNodeType>> = () => {
return (
<div></div>
)
}
export default React.memo(Node)

View File

@ -0,0 +1,65 @@
import type { FC } from 'react'
import type { FileUploadNodeType } from './types'
import type { NodePanelProps } from '@/app/components/workflow/types'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import useConfig from './use-config'
const i18nPrefix = 'nodes.fileUpload'
const Panel: FC<NodePanelProps<FileUploadNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
const {
readOnly,
inputs,
handleVarChanges,
filterVar,
} = useConfig(id, data)
return (
<div className="mt-2">
<div className="space-y-4 px-4 pb-4">
<Field
title={t(`${i18nPrefix}.inputVar`, { ns: 'workflow' })}
required
>
<VarReferencePicker
readonly={readOnly}
nodeId={id}
isShowNodeName
value={inputs.variable_selector || []}
onChange={handleVarChanges}
filterVar={filterVar}
typePlaceHolder="File | Array[File]"
/>
</Field>
</div>
<Split />
<div>
<OutputVars>
<>
<VarItem
name="sandbox_path"
type={inputs.is_array_file ? 'array[string]' : 'string'}
description={t(`${i18nPrefix}.outputVars.sandboxPath`, { ns: 'workflow' })}
/>
<VarItem
name="file_name"
type={inputs.is_array_file ? 'array[string]' : 'string'}
description={t(`${i18nPrefix}.outputVars.fileName`, { ns: 'workflow' })}
/>
</>
</OutputVars>
</div>
</div>
)
}
export default React.memo(Panel)

View File

@ -0,0 +1,6 @@
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
export type FileUploadNodeType = CommonNodeType & {
variable_selector: ValueSelector
is_array_file: boolean
}

View File

@ -0,0 +1,65 @@
import type { ValueSelector, Var } from '../../types'
import type { FileUploadNodeType } from './types'
import { produce } from 'immer'
import { useCallback, useMemo } from 'react'
import { useStoreApi } from 'reactflow'
import {
useIsChatMode,
useNodesReadOnly,
useWorkflow,
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { VarType } from '../../types'
const useConfig = (id: string, payload: FileUploadNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { inputs, setInputs } = useNodeCrud<FileUploadNodeType>(id, payload)
const filterVar = useCallback((varPayload: Var) => {
return varPayload.type === VarType.file || varPayload.type === VarType.arrayFile
}, [])
const isChatMode = useIsChatMode()
const store = useStoreApi()
const { getBeforeNodesInSameBranch } = useWorkflow()
const { getNodes } = store.getState()
const currentNode = getNodes().find(n => n.id === id)
const isInIteration = payload.isInIteration
const iterationNode = isInIteration ? getNodes().find(n => n.id === currentNode!.parentId) : null
const isInLoop = payload.isInLoop
const loopNode = isInLoop ? getNodes().find(n => n.id === currentNode!.parentId) : null
const availableNodes = useMemo(() => {
return getBeforeNodesInSameBranch(id)
}, [getBeforeNodesInSameBranch, id])
const { getCurrentVariableType } = useWorkflowVariables()
const getType = useCallback((variable?: ValueSelector) => {
const varType = getCurrentVariableType({
parentNode: isInIteration ? iterationNode : loopNode,
valueSelector: variable || [],
availableNodes,
isChatMode,
isConstant: false,
})
return varType
}, [getCurrentVariableType, isInIteration, iterationNode, loopNode, availableNodes, isChatMode])
const handleVarChanges = useCallback((variable: ValueSelector | string) => {
const newInputs = produce(inputs, (draft) => {
draft.variable_selector = variable as ValueSelector
draft.is_array_file = getType(draft.variable_selector) === VarType.arrayFile
})
setInputs(newInputs)
}, [inputs, setInputs, getType])
return {
readOnly,
inputs,
filterVar,
handleVarChanges,
}
}
export default useConfig

View File

@ -0,0 +1,66 @@
import type { RefObject } from 'react'
import type { FileUploadNodeType } from './types'
import type { InputVar, Variable } from '@/app/components/workflow/types'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { InputVarType } from '@/app/components/workflow/types'
const i18nPrefix = 'nodes.fileUpload'
type Params = {
id: string
payload: FileUploadNodeType
runInputData: Record<string, unknown>
runInputDataRef: RefObject<Record<string, unknown>>
getInputVars: (textList: string[]) => InputVar[]
setRunInputData: (data: Record<string, unknown>) => void
toVarInputs: (variables: Variable[]) => InputVar[]
}
const useSingleRunFormParams = ({
payload,
runInputData,
setRunInputData,
}: Params) => {
const { t } = useTranslation()
const files = runInputData.files
const setFiles = useCallback((newFiles: []) => {
setRunInputData({
...runInputData,
files: newFiles,
})
}, [runInputData, setRunInputData])
const forms = useMemo(() => {
return [
{
inputs: [{
label: t(`${i18nPrefix}.inputVar`, { ns: 'workflow' })!,
variable: 'files',
type: payload.is_array_file ? InputVarType.multiFiles : InputVarType.singleFile,
required: true,
}],
values: { files },
onChange: (keyValue: Record<string, unknown>) => setFiles((keyValue.files as []) || []),
},
]
}, [files, payload.is_array_file, setFiles, t])
const getDependentVars = () => {
return [payload.variable_selector]
}
const getDependentVar = (variable: string) => {
if (variable === 'files')
return payload.variable_selector
}
return {
forms,
getDependentVars,
getDependentVar,
}
}
export default useSingleRunFormParams