Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
This commit is contained in:
zxhlyh
2024-10-21 10:32:37 +08:00
committed by GitHub
parent 4fd2743efa
commit 7a1d6fe509
445 changed files with 11759 additions and 6922 deletions

View File

@@ -12,6 +12,7 @@ import type { Var } from '@/app/components/workflow/types'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
import BaseInput from '@/app/components/base/input'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http.authorization'
@@ -146,9 +147,7 @@ const Authorization: FC<Props> = ({
</Field>
{tempPayload.config?.type === APIType.custom && (
<Field title={t(`${i18nPrefix}.header`)} isRequired>
<input
type='text'
className='w-full h-8 leading-8 px-2.5 rounded-lg border-0 bg-gray-100 text-gray-900 text-[13px] placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
<BaseInput
value={tempPayload.config?.header || ''}
onChange={handleAPIKeyOrHeaderChange('header')}
/>

View File

@@ -1,17 +1,20 @@
'use client'
import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import React, { useCallback, useMemo } from 'react'
import produce from 'immer'
import type { Body } from '../../types'
import { BodyType } from '../../types'
import useKeyValueList from '../../hooks/use-key-value-list'
import { uniqueId } from 'lodash-es'
import type { Body, BodyPayload, KeyValue as KeyValueType } from '../../types'
import { BodyPayloadValueType, BodyType } from '../../types'
import KeyValue from '../key-value'
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
import VarReferencePicker from '../../../_base/components/variable/var-reference-picker'
import cn from '@/utils/classnames'
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
import type { Var } from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
const UNIQUE_ID_PREFIX = 'key-value-'
type Props = {
readonly: boolean
nodeId: string
@@ -23,15 +26,17 @@ const allTypes = [
BodyType.none,
BodyType.formData,
BodyType.xWwwFormUrlencoded,
BodyType.rawText,
BodyType.json,
BodyType.rawText,
BodyType.binary,
]
const bodyTextMap = {
[BodyType.none]: 'none',
[BodyType.formData]: 'form-data',
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
[BodyType.rawText]: 'raw text',
[BodyType.rawText]: 'raw',
[BodyType.json]: 'JSON',
[BodyType.binary]: 'binary',
}
const EditBody: FC<Props> = ({
@@ -40,7 +45,15 @@ const EditBody: FC<Props> = ({
payload,
onChange,
}) => {
const { type } = payload
const { type, data } = payload
const bodyPayload = useMemo(() => {
if (typeof data === 'string') { // old data
return []
}
return data
}, [data])
const stringValue = [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(type) ? '' : (bodyPayload[0]?.value || '')
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
@@ -50,49 +63,69 @@ const EditBody: FC<Props> = ({
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newType = e.target.value as BodyType
const hasKeyValue = [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(newType)
onChange({
type: newType,
data: '',
data: hasKeyValue
? [
{
id: uniqueId(UNIQUE_ID_PREFIX),
type: BodyPayloadValueType.text,
key: '',
value: '',
},
]
: [],
})
// eslint-disable-next-line @typescript-eslint/no-use-before-define
setBody([])
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [onChange])
const isCurrentKeyValue = type === BodyType.formData || type === BodyType.xWwwFormUrlencoded
const {
list: body,
setList: setBody,
addItem: addBody,
} = useKeyValueList(payload.data, (value) => {
if (!isCurrentKeyValue)
return
const newBody = produce(payload, (draft: Body) => {
draft.data = value
const handleAddBody = useCallback(() => {
const newPayload = produce(payload, (draft) => {
(draft.data as BodyPayload).push({
id: uniqueId(UNIQUE_ID_PREFIX),
type: BodyPayloadValueType.text,
key: '',
value: '',
})
})
onChange(newBody)
}, type === BodyType.json)
onChange(newPayload)
}, [onChange, payload])
useEffect(() => {
if (!isCurrentKeyValue)
return
const newBody = produce(payload, (draft: Body) => {
draft.data = body.map((item) => {
if (!item.key && !item.value)
return ''
return `${item.key}:${item.value}`
}).join('\n')
const handleBodyPayloadChange = useCallback((newList: KeyValueType[]) => {
const newPayload = produce(payload, (draft) => {
draft.data = newList as BodyPayload
})
onChange(newBody)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isCurrentKeyValue])
onChange(newPayload)
}, [onChange, payload])
const filterOnlyFileVariable = (varPayload: Var) => {
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
}
const handleBodyValueChange = useCallback((value: string) => {
const newBody = produce(payload, (draft: Body) => {
draft.data = value
if ((draft.data as BodyPayload).length === 0) {
(draft.data as BodyPayload).push({
id: uniqueId(UNIQUE_ID_PREFIX),
type: BodyPayloadValueType.text,
key: '',
value: '',
})
}
(draft.data as BodyPayload)[0].value = value
})
onChange(newBody)
}, [onChange, payload])
const handleFileChange = useCallback((value: ValueSelector | string) => {
const newBody = produce(payload, (draft: Body) => {
if ((draft.data as BodyPayload).length === 0) {
(draft.data as BodyPayload).push({
id: uniqueId(UNIQUE_ID_PREFIX),
type: BodyPayloadValueType.file,
})
}
(draft.data as BodyPayload)[0].file = value as ValueSelector
})
onChange(newBody)
}, [onChange, payload])
@@ -122,9 +155,10 @@ const EditBody: FC<Props> = ({
<KeyValue
readonly={readonly}
nodeId={nodeId}
list={body}
onChange={setBody}
onAdd={addBody}
list={bodyPayload as KeyValueType[]}
onChange={handleBodyPayloadChange}
onAdd={handleAddBody}
isSupportFile={type === BodyType.formData}
/>
)}
@@ -133,7 +167,7 @@ const EditBody: FC<Props> = ({
instanceId={'http-body-raw'}
title={<div className='uppercase'>Raw text</div>}
onChange={handleBodyValueChange}
value={payload.data}
value={stringValue}
justVar
nodesOutputVars={availableVars}
availableNodes={availableNodes}
@@ -145,7 +179,7 @@ const EditBody: FC<Props> = ({
<InputWithVar
instanceId={'http-body-json'}
title='JSON'
value={payload.data}
value={stringValue}
onChange={handleBodyValueChange}
justVar
nodesOutputVars={availableVars}
@@ -153,6 +187,16 @@ const EditBody: FC<Props> = ({
readOnly={readonly}
/>
)}
{type === BodyType.binary && (
<VarReferencePicker
nodeId={nodeId}
readonly={readonly}
value={bodyPayload[0]?.file || []}
onChange={handleFileChange}
filterVar={filterOnlyFileVariable}
/>
)}
</div>
</div>
)

View File

@@ -10,6 +10,7 @@ type Props = {
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
isSupportFile?: boolean
// toggleKeyValueEdit: () => void
}
@@ -19,6 +20,7 @@ const KeyValueList: FC<Props> = ({
list,
onChange,
onAdd,
isSupportFile,
// toggleKeyValueEdit,
}) => {
// const handleBulkValueChange = useCallback((value: string) => {
@@ -48,6 +50,7 @@ const KeyValueList: FC<Props> = ({
list={list}
onChange={onChange}
onAdd={onAdd}
isSupportFile={isSupportFile}
// onSwitchToBulkEdit={toggleKeyValueEdit}
/>
// : <BulkEdit

View File

@@ -5,8 +5,7 @@ import produce from 'immer'
import { useTranslation } from 'react-i18next'
import type { KeyValue } from '../../../types'
import KeyValueItem from './item'
// import TooltipPlus from '@/app/components/base/tooltip-plus'
// import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
import cn from '@/utils/classnames'
const i18nPrefix = 'workflow.nodes.http'
@@ -16,6 +15,7 @@ type Props = {
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
isSupportFile?: boolean
// onSwitchToBulkEdit: () => void
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
@@ -27,6 +27,7 @@ const KeyValueList: FC<Props> = ({
list,
onChange,
onAdd,
isSupportFile,
// onSwitchToBulkEdit,
keyNotSupportVar,
insertVarTipToLeft,
@@ -55,23 +56,11 @@ const KeyValueList: FC<Props> = ({
return null
return (
<div className='border border-gray-200 rounded-lg overflow-hidden'>
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
<div>{t(`${i18nPrefix}.value`)}</div>
{/* {!readonly && (
<TooltipPlus
popupContent={t(`${i18nPrefix}.bulkEdit`)}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800'
onClick={onSwitchToBulkEdit}
>
<EditList className='w-3 h-3' />
</div>
</TooltipPlus>)} */}
</div>
<div className='border border-divider-regular rounded-lg overflow-hidden'>
<div className={cn('flex items-center h-7 leading-7 text-text-tertiary system-xs-medium-uppercase')}>
<div className={cn('h-full pl-3 border-r border-divider-regular', isSupportFile ? 'w-[140px]' : 'w-1/2')}>{t(`${i18nPrefix}.key`)}</div>
{isSupportFile && <div className='shrink-0 w-[70px] h-full pl-3 border-r border-divider-regular'>{t(`${i18nPrefix}.type`)}</div>}
<div className={cn('h-full pl-3 pr-1 items-center justify-between', isSupportFile ? 'grow' : 'w-1/2')}>{t(`${i18nPrefix}.value`)}</div>
</div>
{
list.map((item, index) => (
@@ -86,6 +75,7 @@ const KeyValueList: FC<Props> = ({
onAdd={onAdd}
readonly={readonly}
canRemove={list.length > 1}
isSupportFile={isSupportFile}
keyNotSupportVar={keyNotSupportVar}
insertVarTipToLeft={insertVarTipToLeft}
/>

View File

@@ -18,6 +18,7 @@ type Props = {
onRemove?: () => void
placeholder?: string
readOnly?: boolean
isSupportFile?: boolean
insertVarTipToLeft?: boolean
}
@@ -31,6 +32,7 @@ const InputItem: FC<Props> = ({
onRemove,
placeholder,
readOnly,
isSupportFile,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
@@ -41,7 +43,11 @@ const InputItem: FC<Props> = ({
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: (varPayload: Var) => {
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
const supportVarTypes = [VarType.string, VarType.number, VarType.secret]
if (isSupportFile)
supportVarTypes.push(...[VarType.file, VarType.arrayFile])
return supportVarTypes.includes(varPayload.type)
},
})

View File

@@ -4,9 +4,13 @@ import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type { KeyValue } from '../../../types'
import VarReferencePicker from '../../../../_base/components/variable/var-reference-picker'
import InputItem from './input-item'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { PortalSelect } from '@/app/components/base/select'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
// import Input from '@/app/components/base/input'
const i18nPrefix = 'workflow.nodes.http'
@@ -21,6 +25,7 @@ type Props = {
onRemove: () => void
isLastItem: boolean
onAdd: () => void
isSupportFile?: boolean
keyNotSupportVar?: boolean
insertVarTipToLeft?: boolean
}
@@ -36,26 +41,29 @@ const KeyValueItem: FC<Props> = ({
onRemove,
isLastItem,
onAdd,
isSupportFile,
keyNotSupportVar,
insertVarTipToLeft,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((key: string) => {
return (value: string) => {
return (value: string | ValueSelector) => {
const newPayload = produce(payload, (draft: any) => {
draft[key] = value
})
onChange(newPayload)
if (key === 'value' && isLastItem)
onAdd()
}
}, [onChange, onAdd, isLastItem, payload])
}, [onChange, payload])
const filterOnlyFileVariable = (varPayload: Var) => {
return [VarType.file, VarType.arrayFile].includes(varPayload.type)
}
return (
// group class name is for hover row show remove button
<div className={cn(className, 'group flex h-min-7 border-t border-gray-200')}>
<div className='w-1/2 border-r border-gray-200'>
<div className={cn('shrink-0 border-r border-divider-regular', isSupportFile ? 'w-[140px]' : 'w-1/2')}>
{!keyNotSupportVar
? (
<InputItem
@@ -70,25 +78,56 @@ const KeyValueItem: FC<Props> = ({
/>
)
: (
<Input
className='rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
<input
className='appearance-none outline-none rounded-none bg-white border-none system-sm-regular focus:ring-0 focus:bg-gray-100! hover:bg-gray-50'
value={payload.key}
onChange={e => handleChange('key')(e.target.value)}
/>
)}
</div>
<div className='w-1/2'>
<InputItem
instanceId={`http-value-${instanceId}`}
nodeId={nodeId}
value={payload.value}
onChange={handleChange('value')}
hasRemove={!readonly && canRemove}
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
readOnly={readonly}
insertVarTipToLeft={insertVarTipToLeft}
/>
{isSupportFile && (
<div className='shrink-0 w-[70px] border-r border-divider-regular'>
<PortalSelect
value={payload.type!}
onSelect={item => handleChange('type')(item.value as string)}
items={[
{ name: 'text', value: 'text' },
{ name: 'file', value: 'file' },
]}
readonly={readonly}
triggerClassName='rounded-none h-7'
triggerClassNameFn={isOpen => isOpen ? 'bg-state-base-hover' : 'bg-transparent'}
popupClassName='w-[80px] h-7'
/>
</div>)}
<div className={cn(isSupportFile ? 'grow' : 'w-1/2')} onClick={() => isLastItem && onAdd()}>
{(isSupportFile && payload.type === 'file')
? (
<VarReferencePicker
nodeId={nodeId}
readonly={readonly}
value={payload.file || []}
onChange={handleChange('file')}
filterVar={filterOnlyFileVariable}
isInTable
onRemove={onRemove}
/>
)
: (
<InputItem
instanceId={`http-value-${instanceId}`}
nodeId={nodeId}
value={payload.value}
onChange={handleChange('value')}
hasRemove={!readonly && canRemove}
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
readOnly={readonly}
isSupportFile={isSupportFile}
insertVarTipToLeft={insertVarTipToLeft}
/>
)}
</div>
</div>
)

View File

@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import type { Timeout as TimeoutPayloadType } from '../../types'
import cn from '@/utils/classnames'
import Input from '@/app/components/base/input'
import { ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
type Props = {
@@ -32,10 +33,18 @@ const InputField: FC<{
<span className="text-[13px] font-medium text-gray-900">{title}</span>
<span className="text-xs font-normal text-gray-500">{description}</span>
</div>
<input className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200" value={value} onChange={(e) => {
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
onChange(value)
}} placeholder={placeholder} type='number' readOnly={readOnly} min={min} max={max} />
<Input
type='number'
value={value}
onChange={(e) => {
const value = Math.max(min, Math.min(max, parseInt(e.target.value, 10)))
onChange(value)
}}
placeholder={placeholder}
readOnly={readOnly}
min={min}
max={max}
/>
</div>
)
}