FEAT: NEW WORKFLOW ENGINE (#3160)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: Yeuoly <admin@srmxy.cn> Co-authored-by: JzoNg <jzongcode@gmail.com> Co-authored-by: StyleZhang <jasonapring2015@outlook.com> Co-authored-by: jyong <jyong@dify.ai> Co-authored-by: nite-knite <nkCoding@gmail.com> Co-authored-by: jyong <718720800@qq.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Method } from '../types'
|
||||
import Selector from '../../_base/components/selector'
|
||||
import useAvailableVarList from '../../_base/hooks/use-available-var-list'
|
||||
import { VarType } from '../../../types'
|
||||
import type { Var } from '../../../types'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
const MethodOptions = [
|
||||
{ label: 'GET', value: Method.get },
|
||||
{ label: 'POST', value: Method.post },
|
||||
{ label: 'HEAD', value: Method.head },
|
||||
{ label: 'PATCH', value: Method.patch },
|
||||
{ label: 'PUT', value: Method.put },
|
||||
{ label: 'DELETE', value: Method.delete },
|
||||
]
|
||||
type Props = {
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
method: Method
|
||||
onMethodChange: (method: Method) => void
|
||||
url: string
|
||||
onUrlChange: (url: string) => void
|
||||
}
|
||||
|
||||
const ApiInput: FC<Props> = ({
|
||||
nodeId,
|
||||
readonly,
|
||||
method,
|
||||
onMethodChange,
|
||||
url,
|
||||
onUrlChange,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex items-start space-x-1'>
|
||||
<Selector
|
||||
value={method}
|
||||
onChange={onMethodChange}
|
||||
options={MethodOptions}
|
||||
trigger={
|
||||
<div className={cn(readonly && 'cursor-pointer', 'h-8 shrink-0 flex items-center px-2.5 bg-gray-100 border-black/5 rounded-lg')} >
|
||||
<div className='w-12 pl-0.5 leading-[18px] text-xs font-medium text-gray-900 uppercase'>{method}</div>
|
||||
{!readonly && <ChevronDown className='ml-1 w-3.5 h-3.5 text-gray-700' />}
|
||||
</div>
|
||||
}
|
||||
popupClassName='top-[34px] w-[108px]'
|
||||
showChecked
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
<Input
|
||||
instanceId='http-api-url'
|
||||
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||
value={url}
|
||||
onChange={onUrlChange}
|
||||
readOnly={readonly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={!readonly ? t('workflow.nodes.http.apiPlaceholder')! : ''}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(ApiInput)
|
@@ -0,0 +1,150 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback } from 'react'
|
||||
import produce from 'immer'
|
||||
import type { Authorization as AuthorizationPayloadType } from '../../types'
|
||||
import { APIType, AuthorizationType } from '../../types'
|
||||
import RadioGroup from './radio-group'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http.authorization'
|
||||
|
||||
type Props = {
|
||||
payload: AuthorizationPayloadType
|
||||
onChange: (payload: AuthorizationPayloadType) => void
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const Field = ({ title, isRequired, children }: { title: string; isRequired?: boolean; children: JSX.Element }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='leading-8 text-[13px] font-medium text-gray-700'>
|
||||
{title}
|
||||
{isRequired && <span className='ml-0.5 text-[#D92D20]'>*</span>}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Authorization: FC<Props> = ({
|
||||
payload,
|
||||
onChange,
|
||||
isShow,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [tempPayload, setTempPayload] = React.useState<AuthorizationPayloadType>(payload)
|
||||
const handleAuthTypeChange = useCallback((type: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
draft.type = type as AuthorizationType
|
||||
if (draft.type === AuthorizationType.apiKey && !draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleAuthAPITypeChange = useCallback((type: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config.type = type as APIType
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleAPIKeyOrHeaderChange = useCallback((type: 'api_key' | 'header') => {
|
||||
return (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config[type] = e.target.value
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onChange(tempPayload)
|
||||
onHide()
|
||||
}, [tempPayload, onChange, onHide])
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.authorization`)}
|
||||
wrapperClassName='z-50 w-400'
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
>
|
||||
<div>
|
||||
<div className='space-y-2'>
|
||||
<Field title={t(`${i18nPrefix}.authorizationType`)}>
|
||||
<RadioGroup
|
||||
options={[
|
||||
{ value: AuthorizationType.none, label: t(`${i18nPrefix}.no-auth`) },
|
||||
{ value: AuthorizationType.apiKey, label: t(`${i18nPrefix}.api-key`) },
|
||||
]}
|
||||
value={tempPayload.type}
|
||||
onChange={handleAuthTypeChange}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{tempPayload.type === AuthorizationType.apiKey && (
|
||||
<>
|
||||
<Field title={t(`${i18nPrefix}.auth-type`)}>
|
||||
<RadioGroup
|
||||
options={[
|
||||
{ value: APIType.basic, label: t(`${i18nPrefix}.basic`) },
|
||||
{ value: APIType.bearer, label: t(`${i18nPrefix}.bearer`) },
|
||||
{ value: APIType.custom, label: t(`${i18nPrefix}.custom`) },
|
||||
]}
|
||||
value={tempPayload.config?.type || APIType.basic}
|
||||
onChange={handleAuthAPITypeChange}
|
||||
/>
|
||||
</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'
|
||||
value={tempPayload.config?.header || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('header')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field title={t(`${i18nPrefix}.api-key-title`)} 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'
|
||||
value={tempPayload.config?.api_key || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('api_key')}
|
||||
/>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-6 flex justify-end space-x-2'>
|
||||
<Button onClick={onHide} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.cancel')}</Button>
|
||||
<Button type='primary' onClick={handleConfirm} className='flex items-center !h-8 leading-[18px] !text-[13px] !font-medium'>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(Authorization)
|
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
type Option = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type ItemProps = {
|
||||
title: string
|
||||
onClick: () => void
|
||||
isSelected: boolean
|
||||
}
|
||||
const Item: FC<ItemProps> = ({
|
||||
title,
|
||||
onClick,
|
||||
isSelected,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
isSelected ? 'border-[2px] border-primary-400 bg-white shadow-xs' : 'border border-gray-100 bg-gray-25',
|
||||
'w-0 grow flex items-center justify-center h-8 cursor-pointer rounded-lg text-[13px] font-normal text-gray-900')
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
options: Option[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
const RadioGroup: FC<Props> = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
}) => {
|
||||
const handleChange = useCallback((value: string) => {
|
||||
return () => onChange(value)
|
||||
}, [onChange])
|
||||
return (
|
||||
<div className='flex space-x-2'>
|
||||
{options.map(option => (
|
||||
<Item
|
||||
key={option.value}
|
||||
title={option.label}
|
||||
onClick={handleChange(option.value)}
|
||||
isSelected={option.value === value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(RadioGroup)
|
@@ -0,0 +1,157 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
import produce from 'immer'
|
||||
import cn from 'classnames'
|
||||
import type { Body } from '../../types'
|
||||
import { BodyType } from '../../types'
|
||||
import useKeyValueList from '../../hooks/use-key-value-list'
|
||||
import KeyValue from '../key-value'
|
||||
import useAvailableVarList from '../../../_base/hooks/use-available-var-list'
|
||||
import InputWithVar from '@/app/components/workflow/nodes/_base/components/prompt/editor'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
payload: Body
|
||||
onChange: (payload: Body) => void
|
||||
}
|
||||
|
||||
const allTypes = [
|
||||
BodyType.none,
|
||||
BodyType.formData,
|
||||
BodyType.xWwwFormUrlencoded,
|
||||
BodyType.rawText,
|
||||
BodyType.json,
|
||||
]
|
||||
const bodyTextMap = {
|
||||
[BodyType.none]: 'none',
|
||||
[BodyType.formData]: 'form-data',
|
||||
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
|
||||
[BodyType.rawText]: 'raw text',
|
||||
[BodyType.json]: 'JSON',
|
||||
}
|
||||
|
||||
const EditBody: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
const { type } = payload
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const handleTypeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newType = e.target.value as BodyType
|
||||
onChange({
|
||||
type: newType,
|
||||
data: '',
|
||||
})
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
setBody([])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [onChange])
|
||||
|
||||
const {
|
||||
list: body,
|
||||
setList: setBody,
|
||||
addItem: addBody,
|
||||
} = useKeyValueList(payload.data, (value) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
})
|
||||
onChange(newBody)
|
||||
}, type === BodyType.json)
|
||||
|
||||
const isCurrentKeyValue = type === BodyType.formData || type === BodyType.xWwwFormUrlencoded
|
||||
|
||||
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')
|
||||
})
|
||||
onChange(newBody)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isCurrentKeyValue])
|
||||
|
||||
const handleBodyValueChange = useCallback((value: string) => {
|
||||
const newBody = produce(payload, (draft: Body) => {
|
||||
draft.data = value
|
||||
})
|
||||
onChange(newBody)
|
||||
}, [onChange, payload])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* body type */}
|
||||
<div className='flex flex-wrap'>
|
||||
{allTypes.map(t => (
|
||||
<label key={t} htmlFor={`body-type-${t}`} className='mr-4 flex items-center h-7 space-x-2'>
|
||||
<input
|
||||
type="radio"
|
||||
id={`body-type-${t}`}
|
||||
value={t}
|
||||
checked={type === t}
|
||||
onChange={handleTypeChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='leading-[18px] text-[13px] font-normal text-gray-700'>{bodyTextMap[t]}</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{/* body value */}
|
||||
<div className={cn(type !== BodyType.none && 'mt-1')}>
|
||||
{type === BodyType.none && null}
|
||||
{(type === BodyType.formData || type === BodyType.xWwwFormUrlencoded) && (
|
||||
<KeyValue
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
list={body}
|
||||
onChange={setBody}
|
||||
onAdd={addBody}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.rawText && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-raw'}
|
||||
title={<div className='uppercase'>Raw text</div>}
|
||||
onChange={handleBodyValueChange}
|
||||
value={payload.data}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.json && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-json'}
|
||||
title='JSON'
|
||||
value={payload.data}
|
||||
onChange={handleBodyValueChange}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditBody)
|
@@ -0,0 +1,61 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import TextEditor from '@/app/components/workflow/nodes/_base/components/editor/text-editor'
|
||||
import { LayoutGrid02 } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
onSwitchToKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
const BulkEdit: FC<Props> = ({
|
||||
value,
|
||||
onChange,
|
||||
onSwitchToKeyValueEdit,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [tempValue, setTempValue] = React.useState(value)
|
||||
|
||||
const handleChange = useCallback((value: string) => {
|
||||
setTempValue(value)
|
||||
}, [])
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
onChange(tempValue)
|
||||
}, [tempValue, onChange])
|
||||
|
||||
const handleSwitchToKeyValueEdit = useCallback(() => {
|
||||
onChange(tempValue)
|
||||
onSwitchToKeyValueEdit()
|
||||
}, [tempValue, onChange, onSwitchToKeyValueEdit])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TextEditor
|
||||
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
|
||||
value={tempValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
headerRight={
|
||||
<div className='flex items-center h-[18px]'>
|
||||
<div
|
||||
className='flex items-center space-x-1 cursor-pointer'
|
||||
onClick={handleSwitchToKeyValueEdit}
|
||||
>
|
||||
<LayoutGrid02 className='w-3 h-3 text-gray-500' />
|
||||
<div className='leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.keyValueEdit`)}</div>
|
||||
</div>
|
||||
<div className='ml-3 mr-1.5 w-px h-3 bg-gray-200'></div>
|
||||
</div>
|
||||
}
|
||||
minHeight={150}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(BulkEdit)
|
@@ -0,0 +1,59 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { KeyValue } from '../../types'
|
||||
import KeyValueEdit from './key-value-edit'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
// toggleKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
// toggleKeyValueEdit,
|
||||
}) => {
|
||||
// const handleBulkValueChange = useCallback((value: string) => {
|
||||
// const newList = value.split('\n').map((item) => {
|
||||
// const [key, value] = item.split(':')
|
||||
// return {
|
||||
// key: key ? key.trim() : '',
|
||||
// value: value ? value.trim() : '',
|
||||
// }
|
||||
// })
|
||||
// onChange(newList)
|
||||
// }, [onChange])
|
||||
|
||||
// const bulkList = (() => {
|
||||
// const res = list.map((item) => {
|
||||
// if (!item.key && !item.value)
|
||||
// return ''
|
||||
// if (!item.value)
|
||||
// return item.key
|
||||
// return `${item.key}:${item.value}`
|
||||
// }).join('\n')
|
||||
// return res
|
||||
// })()
|
||||
return <KeyValueEdit
|
||||
readonly={readonly}
|
||||
nodeId={nodeId}
|
||||
list={list}
|
||||
onChange={onChange}
|
||||
onAdd={onAdd}
|
||||
// onSwitchToBulkEdit={toggleKeyValueEdit}
|
||||
/>
|
||||
// : <BulkEdit
|
||||
// value={bulkList}
|
||||
// onChange={handleBulkValueChange}
|
||||
// onSwitchToKeyValueEdit={toggleKeyValueEdit}
|
||||
// />
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
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'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
// onSwitchToBulkEdit: () => void
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
// onSwitchToBulkEdit,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((index: number) => {
|
||||
return (newItem: KeyValue) => {
|
||||
const newList = produce(list, (draft: any) => {
|
||||
draft[index] = newItem
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
const handleRemove = useCallback((index: number) => {
|
||||
return () => {
|
||||
const newList = produce(list, (draft: any) => {
|
||||
draft.splice(index, 1)
|
||||
})
|
||||
onChange(newList)
|
||||
}
|
||||
}, [list, onChange])
|
||||
|
||||
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>
|
||||
{
|
||||
list.map((item, index) => (
|
||||
<KeyValueItem
|
||||
key={item.id}
|
||||
instanceId={item.id!}
|
||||
nodeId={nodeId}
|
||||
payload={item}
|
||||
onChange={handleChange(index)}
|
||||
onRemove={handleRemove(index)}
|
||||
isLastItem={index === list.length - 1}
|
||||
onAdd={onAdd}
|
||||
readonly={readonly}
|
||||
canRemove={list.length > 1}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAvailableVarList from '../../../../_base/hooks/use-available-var-list'
|
||||
import RemoveButton from '@/app/components/workflow/nodes/_base/components/remove-button'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
import type { Var } from '@/app/components/workflow/types'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
type Props = {
|
||||
className?: string
|
||||
instanceId?: string
|
||||
nodeId: string
|
||||
value: string
|
||||
onChange: (newValue: string) => void
|
||||
hasRemove: boolean
|
||||
onRemove?: () => void
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
}
|
||||
|
||||
const InputItem: FC<Props> = ({
|
||||
className,
|
||||
instanceId,
|
||||
nodeId,
|
||||
value,
|
||||
onChange,
|
||||
hasRemove,
|
||||
onRemove,
|
||||
placeholder,
|
||||
readOnly,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const hasValue = !!value
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodes } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const handleRemove = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onRemove?.()
|
||||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'hover:bg-gray-50 hover:cursor-text', 'relative flex h-full items-center')}>
|
||||
{(!readOnly)
|
||||
? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'bg-gray-100' : 'bg-width', 'w-0 grow px-3 py-1')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)
|
||||
: <div
|
||||
className="pl-0.5 w-full h-[18px] leading-[18px]"
|
||||
>
|
||||
{!hasValue && <div className='text-gray-300 text-xs font-normal'>{placeholder}</div>}
|
||||
{hasValue && (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'shadow-xs bg-gray-50 border-gray-300' : 'bg-gray-100 border-gray-100', 'w-0 grow rounded-lg px-3 py-[6px] border')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>}
|
||||
{hasRemove && !isFocus && (
|
||||
<RemoveButton
|
||||
className='group-hover:block hidden absolute right-1 top-0.5'
|
||||
onClick={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputItem)
|
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import produce from 'immer'
|
||||
import type { KeyValue } from '../../../types'
|
||||
import InputItem from './input-item'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
instanceId: string
|
||||
className?: string
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
canRemove: boolean
|
||||
payload: KeyValue
|
||||
onChange: (newPayload: KeyValue) => void
|
||||
onRemove: () => void
|
||||
isLastItem: boolean
|
||||
onAdd: () => void
|
||||
}
|
||||
|
||||
const KeyValueItem: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
nodeId,
|
||||
readonly,
|
||||
canRemove,
|
||||
payload,
|
||||
onChange,
|
||||
onRemove,
|
||||
isLastItem,
|
||||
onAdd,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: string) => {
|
||||
const newPayload = produce(payload, (draft: any) => {
|
||||
draft[key] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
if (key === 'value' && isLastItem)
|
||||
onAdd()
|
||||
}
|
||||
}, [onChange, onAdd, isLastItem, payload])
|
||||
|
||||
return (
|
||||
// group class name is for hover row show remove button
|
||||
<div className={cn(className, 'group flex items-start h-min-7 border-t border-gray-200')}>
|
||||
<div className='w-1/2 h-full border-r border-gray-200'>
|
||||
<InputItem
|
||||
instanceId={`http-key-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.key}
|
||||
onChange={handleChange('key')}
|
||||
hasRemove={false}
|
||||
placeholder={t(`${i18nPrefix}.key`)!}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
<div className='w-1/2 h-full'>
|
||||
<InputItem
|
||||
instanceId={`http-value-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.value}
|
||||
onChange={handleChange('value')}
|
||||
hasRemove={!readonly && canRemove}
|
||||
onRemove={onRemove}
|
||||
placeholder={t(`${i18nPrefix}.value`)!}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueItem)
|
Reference in New Issue
Block a user