dify
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
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 cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var'
|
||||
|
||||
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, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].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', 'flex h-8 shrink-0 items-center rounded-lg border border-components-button-secondary-border bg-components-button-secondary-bg px-2.5')} >
|
||||
<div className='w-12 pl-0.5 text-xs font-medium uppercase leading-[18px] text-text-primary'>{method}</div>
|
||||
{!readonly && <RiArrowDownSLine className='ml-1 h-3.5 w-3.5 text-text-secondary' />}
|
||||
</div>
|
||||
}
|
||||
popupClassName='top-[34px] w-[108px]'
|
||||
showChecked
|
||||
readonly={readonly}
|
||||
/>
|
||||
|
||||
<Input
|
||||
instanceId='http-api-url'
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
value={url}
|
||||
onChange={onUrlChange}
|
||||
readOnly={readonly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={!readonly ? t('workflow.nodes.http.apiPlaceholder')! : ''}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
export default React.memo(ApiInput)
|
||||
@@ -0,0 +1,183 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type { Authorization as AuthorizationPayloadType } from '../../types'
|
||||
import { APIType, AuthorizationType } from '../../types'
|
||||
import RadioGroup from './radio-group'
|
||||
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
|
||||
import { VarType } from '@/app/components/workflow/types'
|
||||
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'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
payload: AuthorizationPayloadType
|
||||
onChange: (payload: AuthorizationPayloadType) => void
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
const Field = ({ title, isRequired, children }: { title: string; isRequired?: boolean; children: React.JSX.Element }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='text-[13px] font-medium leading-8 text-text-secondary'>
|
||||
{title}
|
||||
{isRequired && <span className='ml-0.5 text-text-destructive'>*</span>}
|
||||
</div>
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Authorization: FC<Props> = ({
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
isShow,
|
||||
onHide,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
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 handleAPIKeyChange = useCallback((str: string) => {
|
||||
const newPayload = produce(tempPayload, (draft: AuthorizationPayloadType) => {
|
||||
if (!draft.config) {
|
||||
draft.config = {
|
||||
type: APIType.basic,
|
||||
api_key: '',
|
||||
}
|
||||
}
|
||||
draft.config.api_key = str
|
||||
})
|
||||
setTempPayload(newPayload)
|
||||
}, [tempPayload, setTempPayload])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
onChange(tempPayload)
|
||||
onHide()
|
||||
}, [tempPayload, onChange, onHide])
|
||||
return (
|
||||
<Modal
|
||||
title={t(`${i18nPrefix}.authorization`)}
|
||||
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>
|
||||
<BaseInput
|
||||
value={tempPayload.config?.header || ''}
|
||||
onChange={handleAPIKeyOrHeaderChange('header')}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
|
||||
<Field title={t(`${i18nPrefix}.api-key-title`)} isRequired>
|
||||
<div className='flex'>
|
||||
<Input
|
||||
instanceId='http-api-key'
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
value={tempPayload.config?.api_key || ''}
|
||||
onChange={handleAPIKeyChange}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={' '}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='mt-6 flex justify-end space-x-2'>
|
||||
<Button onClick={onHide}>{t('common.operation.cancel')}</Button>
|
||||
<Button variant='primary' onClick={handleConfirm}>{t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
export default React.memo(Authorization)
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import cn from '@/utils/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(
|
||||
'system-sm-regular flex h-8 grow cursor-default items-center rounded-md border border-components-option-card-option-border bg-components-option-card-option-bg px-2 text-text-secondary',
|
||||
!isSelected && 'cursor-pointer hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs',
|
||||
isSelected && 'system-sm-medium border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs',
|
||||
)}
|
||||
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,165 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BodyPayloadValueType, BodyType, type HttpNodeType, Method } from '../types'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useNodesInteractions } from '@/app/components/workflow/hooks'
|
||||
|
||||
type Props = {
|
||||
nodeId: string
|
||||
isShow: boolean
|
||||
onHide: () => void
|
||||
handleCurlImport: (node: HttpNodeType) => void
|
||||
}
|
||||
|
||||
const parseCurl = (curlCommand: string): { node: HttpNodeType | null; error: string | null } => {
|
||||
if (!curlCommand.trim().toLowerCase().startsWith('curl'))
|
||||
return { node: null, error: 'Invalid cURL command. Command must start with "curl".' }
|
||||
|
||||
const node: Partial<HttpNodeType> = {
|
||||
title: 'HTTP Request',
|
||||
desc: 'Imported from cURL',
|
||||
method: undefined,
|
||||
url: '',
|
||||
headers: '',
|
||||
params: '',
|
||||
body: { type: BodyType.none, data: '' },
|
||||
}
|
||||
const args = curlCommand.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []
|
||||
let hasData = false
|
||||
|
||||
for (let i = 1; i < args.length; i++) {
|
||||
const arg = args[i].replace(/^['"]|['"]$/g, '')
|
||||
switch (arg) {
|
||||
case '-X':
|
||||
case '--request':
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing HTTP method after -X or --request.' }
|
||||
node.method = (args[++i].replace(/^['"]|['"]$/g, '') as Method) || Method.get
|
||||
hasData = true
|
||||
break
|
||||
case '-H':
|
||||
case '--header':
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing header value after -H or --header.' }
|
||||
node.headers += (node.headers ? '\n' : '') + args[++i].replace(/^['"]|['"]$/g, '')
|
||||
break
|
||||
case '-d':
|
||||
case '--data':
|
||||
case '--data-raw':
|
||||
case '--data-binary': {
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing data value after -d, --data, --data-raw, or --data-binary.' }
|
||||
const bodyPayload = [{
|
||||
type: BodyPayloadValueType.text,
|
||||
value: args[++i].replace(/^['"]|['"]$/g, ''),
|
||||
}]
|
||||
node.body = { type: BodyType.rawText, data: bodyPayload }
|
||||
break
|
||||
}
|
||||
case '-F':
|
||||
case '--form': {
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing form data after -F or --form.' }
|
||||
if (node.body?.type !== BodyType.formData)
|
||||
node.body = { type: BodyType.formData, data: '' }
|
||||
const formData = args[++i].replace(/^['"]|['"]$/g, '')
|
||||
const [key, ...valueParts] = formData.split('=')
|
||||
if (!key)
|
||||
return { node: null, error: 'Invalid form data format.' }
|
||||
let value = valueParts.join('=')
|
||||
|
||||
// To support command like `curl -F "file=@/path/to/file;type=application/zip"`
|
||||
// the `;type=application/zip` should translate to `Content-Type: application/zip`
|
||||
const typeMatch = value.match(/^(.+?);type=(.+)$/)
|
||||
if (typeMatch) {
|
||||
const [, actualValue, mimeType] = typeMatch
|
||||
value = actualValue
|
||||
node.headers += `${node.headers ? '\n' : ''}Content-Type: ${mimeType}`
|
||||
}
|
||||
|
||||
node.body.data += `${node.body.data ? '\n' : ''}${key}:${value}`
|
||||
break
|
||||
}
|
||||
case '--json':
|
||||
if (i + 1 >= args.length)
|
||||
return { node: null, error: 'Missing JSON data after --json.' }
|
||||
node.body = { type: BodyType.json, data: args[++i].replace(/^['"]|['"]$/g, '') }
|
||||
break
|
||||
default:
|
||||
if (arg.startsWith('http') && !node.url)
|
||||
node.url = arg
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Determine final method
|
||||
node.method = node.method || (hasData ? Method.post : Method.get)
|
||||
|
||||
if (!node.url)
|
||||
return { node: null, error: 'Missing URL or url not start with http.' }
|
||||
|
||||
// Extract query params from URL
|
||||
const urlParts = node.url?.split('?') || []
|
||||
if (urlParts.length > 1) {
|
||||
node.url = urlParts[0]
|
||||
node.params = urlParts[1].replace(/&/g, '\n').replace(/=/g, ': ')
|
||||
}
|
||||
|
||||
return { node: node as HttpNodeType, error: null }
|
||||
}
|
||||
|
||||
const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
|
||||
const [inputString, setInputString] = useState('')
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const { node, error } = parseCurl(inputString)
|
||||
if (error) {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: error,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!node)
|
||||
return
|
||||
|
||||
onHide()
|
||||
handleCurlImport(node)
|
||||
// Close the panel then open it again to make the panel re-render
|
||||
handleNodeSelect(nodeId, true)
|
||||
setTimeout(() => {
|
||||
handleNodeSelect(nodeId)
|
||||
}, 0)
|
||||
}, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('workflow.nodes.http.curl.title')}
|
||||
isShow={isShow}
|
||||
onClose={onHide}
|
||||
className='!w-[400px] !max-w-[400px] !p-4'
|
||||
>
|
||||
<div>
|
||||
<Textarea
|
||||
value={inputString}
|
||||
className='my-3 h-40 w-full grow'
|
||||
onChange={e => setInputString(e.target.value)}
|
||||
placeholder={t('workflow.nodes.http.curl.placeholder')!}
|
||||
/>
|
||||
</div>
|
||||
<div className='mt-4 flex justify-end space-x-2'>
|
||||
<Button className='!w-[95px]' onClick={onHide} >{t('common.operation.cancel')}</Button>
|
||||
<Button className='!w-[95px]' variant='primary' onClick={handleSave} > {t('common.operation.save')}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(CurlPanel)
|
||||
@@ -0,0 +1,204 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useMemo } from 'react'
|
||||
import { produce } from 'immer'
|
||||
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 { 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
|
||||
payload: Body
|
||||
onChange: (payload: Body) => void
|
||||
}
|
||||
|
||||
const allTypes = [
|
||||
BodyType.none,
|
||||
BodyType.formData,
|
||||
BodyType.xWwwFormUrlencoded,
|
||||
BodyType.json,
|
||||
BodyType.rawText,
|
||||
BodyType.binary,
|
||||
]
|
||||
const bodyTextMap = {
|
||||
[BodyType.none]: 'none',
|
||||
[BodyType.formData]: 'form-data',
|
||||
[BodyType.xWwwFormUrlencoded]: 'x-www-form-urlencoded',
|
||||
[BodyType.rawText]: 'raw',
|
||||
[BodyType.json]: 'JSON',
|
||||
[BodyType.binary]: 'binary',
|
||||
}
|
||||
|
||||
const EditBody: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
payload,
|
||||
onChange,
|
||||
}) => {
|
||||
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) => {
|
||||
return [VarType.string, VarType.number, VarType.secret, VarType.arrayNumber, VarType.arrayString].includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
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: hasKeyValue
|
||||
? [
|
||||
{
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.text,
|
||||
key: '',
|
||||
value: '',
|
||||
},
|
||||
]
|
||||
: [],
|
||||
})
|
||||
}, [onChange])
|
||||
|
||||
const handleAddBody = useCallback(() => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
(draft.data as BodyPayload).push({
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
type: BodyPayloadValueType.text,
|
||||
key: '',
|
||||
value: '',
|
||||
})
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [onChange, payload])
|
||||
|
||||
const handleBodyPayloadChange = useCallback((newList: KeyValueType[]) => {
|
||||
const newPayload = produce(payload, (draft) => {
|
||||
draft.data = newList as BodyPayload
|
||||
})
|
||||
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) => {
|
||||
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])
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* body type */}
|
||||
<div className='flex flex-wrap'>
|
||||
{allTypes.map(t => (
|
||||
<label key={t} htmlFor={`body-type-${t}`} className='mr-4 flex h-7 items-center space-x-2'>
|
||||
<input
|
||||
type="radio"
|
||||
id={`body-type-${t}`}
|
||||
value={t}
|
||||
checked={type === t}
|
||||
onChange={handleTypeChange}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className='text-[13px] font-normal leading-[18px] text-text-secondary'>{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={bodyPayload as KeyValueType[]}
|
||||
onChange={handleBodyPayloadChange}
|
||||
onAdd={handleAddBody}
|
||||
isSupportFile={type === BodyType.formData}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.rawText && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-raw'}
|
||||
title={<div className='uppercase'>Raw text</div>}
|
||||
onChange={handleBodyValueChange}
|
||||
value={stringValue}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.json && (
|
||||
<InputWithVar
|
||||
instanceId={'http-body-json'}
|
||||
title='JSON'
|
||||
value={stringValue}
|
||||
onChange={handleBodyValueChange}
|
||||
justVar
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodes}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === BodyType.binary && (
|
||||
<VarReferencePicker
|
||||
nodeId={nodeId}
|
||||
readonly={readonly}
|
||||
value={bodyPayload[0]?.file || []}
|
||||
onChange={handleFileChange}
|
||||
filterVar={filterOnlyFileVariable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(EditBody)
|
||||
@@ -0,0 +1,62 @@
|
||||
'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
|
||||
isInNode
|
||||
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
|
||||
value={tempValue}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
headerRight={
|
||||
<div className='flex h-[18px] items-center'>
|
||||
<div
|
||||
className='flex cursor-pointer items-center space-x-1'
|
||||
onClick={handleSwitchToKeyValueEdit}
|
||||
>
|
||||
<LayoutGrid02 className='h-3 w-3 text-gray-500' />
|
||||
<div className='text-xs font-normal leading-[18px] text-gray-500'>{t(`${i18nPrefix}.keyValueEdit`)}</div>
|
||||
</div>
|
||||
<div className='ml-3 mr-1.5 h-3 w-px bg-gray-200'></div>
|
||||
</div>
|
||||
}
|
||||
minHeight={150}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(BulkEdit)
|
||||
@@ -0,0 +1,62 @@
|
||||
'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
|
||||
isSupportFile?: boolean
|
||||
// toggleKeyValueEdit: () => void
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
// 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}
|
||||
isSupportFile={isSupportFile}
|
||||
// onSwitchToBulkEdit={toggleKeyValueEdit}
|
||||
/>
|
||||
// : <BulkEdit
|
||||
// value={bulkList}
|
||||
// onChange={handleBulkValueChange}
|
||||
// onSwitchToKeyValueEdit={toggleKeyValueEdit}
|
||||
// />
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
||||
@@ -0,0 +1,87 @@
|
||||
'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 cn from '@/utils/classnames'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
list: KeyValue[]
|
||||
onChange: (newList: KeyValue[]) => void
|
||||
onAdd: () => void
|
||||
isSupportFile?: boolean
|
||||
// onSwitchToBulkEdit: () => void
|
||||
keyNotSupportVar?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
|
||||
const KeyValueList: FC<Props> = ({
|
||||
readonly,
|
||||
nodeId,
|
||||
list,
|
||||
onChange,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
// onSwitchToBulkEdit,
|
||||
keyNotSupportVar,
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
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])
|
||||
|
||||
if (!Array.isArray(list))
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='overflow-hidden rounded-lg border border-divider-regular'>
|
||||
<div className={cn('system-xs-medium-uppercase flex h-7 items-center leading-7 text-text-tertiary')}>
|
||||
<div className={cn('h-full border-r border-divider-regular pl-3', isSupportFile ? 'w-[140px]' : 'w-1/2')}>{t(`${i18nPrefix}.key`)}</div>
|
||||
{isSupportFile && <div className='h-full w-[70px] shrink-0 border-r border-divider-regular pl-3'>{t(`${i18nPrefix}.type`)}</div>}
|
||||
<div className={cn('h-full items-center justify-between pl-3 pr-1', isSupportFile ? 'grow' : 'w-1/2')}>{t(`${i18nPrefix}.value`)}</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}
|
||||
isSupportFile={isSupportFile}
|
||||
keyNotSupportVar={keyNotSupportVar}
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueList)
|
||||
@@ -0,0 +1,109 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useAvailableVarList from '../../../../_base/hooks/use-available-var-list'
|
||||
import cn from '@/utils/classnames'
|
||||
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
|
||||
isSupportFile?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
|
||||
const InputItem: FC<Props> = ({
|
||||
className,
|
||||
instanceId,
|
||||
nodeId,
|
||||
value,
|
||||
onChange,
|
||||
hasRemove,
|
||||
onRemove,
|
||||
placeholder,
|
||||
readOnly,
|
||||
isSupportFile,
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const hasValue = !!value
|
||||
|
||||
const [isFocus, setIsFocus] = useState(false)
|
||||
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
|
||||
onlyLeafNodeVar: false,
|
||||
filterVar: (varPayload: Var) => {
|
||||
const supportVarTypes = [VarType.string, VarType.number, VarType.secret]
|
||||
if (isSupportFile)
|
||||
supportVarTypes.push(VarType.file, VarType.arrayFile)
|
||||
|
||||
return supportVarTypes.includes(varPayload.type)
|
||||
},
|
||||
})
|
||||
|
||||
const handleRemove = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onRemove?.()
|
||||
}, [onRemove])
|
||||
|
||||
return (
|
||||
<div className={cn(className, 'hover:cursor-text hover:bg-state-base-hover', 'relative flex h-full')}>
|
||||
{(!readOnly)
|
||||
? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'bg-components-input-bg-active' : 'bg-width', 'w-0 grow px-3 py-1')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
promptMinHeightClassName='h-full'
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
)
|
||||
: <div
|
||||
className="h-[18px] w-full pl-0.5 leading-[18px]"
|
||||
>
|
||||
{!hasValue && <div className='text-xs font-normal text-text-quaternary'>{placeholder}</div>}
|
||||
{hasValue && (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
className={cn(isFocus ? 'border-components-input-border-active bg-components-input-bg-active shadow-xs' : 'border-components-input-border-hover bg-components-input-bg-normal', 'w-0 grow rounded-lg border px-3 py-[6px]')}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
readOnly={readOnly}
|
||||
nodesOutputVars={availableVars}
|
||||
availableNodes={availableNodesWithParent}
|
||||
onFocusChange={setIsFocus}
|
||||
placeholder={t('workflow.nodes.http.insertVarPlaceholder')!}
|
||||
placeholderClassName='!leading-[21px]'
|
||||
promptMinHeightClassName='h-full'
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div>}
|
||||
{hasRemove && !isFocus && (
|
||||
<RemoveButton
|
||||
className='absolute right-1 top-0.5 hidden group-hover:block'
|
||||
onClick={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default React.memo(InputItem)
|
||||
@@ -0,0 +1,135 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
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 { 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'
|
||||
|
||||
type Props = {
|
||||
instanceId: string
|
||||
className?: string
|
||||
nodeId: string
|
||||
readonly: boolean
|
||||
canRemove: boolean
|
||||
payload: KeyValue
|
||||
onChange: (newPayload: KeyValue) => void
|
||||
onRemove: () => void
|
||||
isLastItem: boolean
|
||||
onAdd: () => void
|
||||
isSupportFile?: boolean
|
||||
keyNotSupportVar?: boolean
|
||||
insertVarTipToLeft?: boolean
|
||||
}
|
||||
|
||||
const KeyValueItem: FC<Props> = ({
|
||||
instanceId,
|
||||
className,
|
||||
nodeId,
|
||||
readonly,
|
||||
canRemove,
|
||||
payload,
|
||||
onChange,
|
||||
onRemove,
|
||||
isLastItem,
|
||||
onAdd,
|
||||
isSupportFile,
|
||||
keyNotSupportVar,
|
||||
insertVarTipToLeft,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleChange = useCallback((key: string) => {
|
||||
return (value: string | ValueSelector) => {
|
||||
const newPayload = produce(payload, (draft: any) => {
|
||||
draft[key] = value
|
||||
})
|
||||
onChange(newPayload)
|
||||
}
|
||||
}, [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, 'h-min-7 group flex border-t border-divider-regular')}>
|
||||
<div className={cn('shrink-0 border-r border-divider-regular', isSupportFile ? 'w-[140px]' : 'w-1/2')}>
|
||||
{!keyNotSupportVar
|
||||
? (
|
||||
<InputItem
|
||||
instanceId={`http-key-${instanceId}`}
|
||||
nodeId={nodeId}
|
||||
value={payload.key}
|
||||
onChange={handleChange('key')}
|
||||
hasRemove={false}
|
||||
placeholder={t(`${i18nPrefix}.key`)!}
|
||||
readOnly={readonly}
|
||||
insertVarTipToLeft={insertVarTipToLeft}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<input
|
||||
className='system-sm-regular focus:bg-gray-100! appearance-none rounded-none border-none bg-transparent outline-none hover:bg-components-input-bg-hover focus:ring-0'
|
||||
value={payload.key}
|
||||
onChange={e => handleChange('key')(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{isSupportFile && (
|
||||
<div className='w-[70px] shrink-0 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 text-text-primary'
|
||||
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>
|
||||
)
|
||||
}
|
||||
export default React.memo(KeyValueItem)
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Timeout as TimeoutPayloadType } from '../../types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import { FieldCollapse } from '@/app/components/workflow/nodes/_base/components/collapse'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
nodeId: string
|
||||
payload: TimeoutPayloadType
|
||||
onChange: (payload: TimeoutPayloadType) => void
|
||||
}
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
const InputField: FC<{
|
||||
title: string
|
||||
description: string
|
||||
placeholder: string
|
||||
value?: number
|
||||
onChange: (value: number | undefined) => void
|
||||
readOnly?: boolean
|
||||
min: number
|
||||
max: number
|
||||
}> = ({ title, description, placeholder, value, onChange, readOnly, min, max }) => {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex h-[18px] items-center space-x-2">
|
||||
<span className="text-[13px] font-medium text-text-primary">{title}</span>
|
||||
<span className="text-xs font-normal text-text-tertiary">{description}</span>
|
||||
</div>
|
||||
<Input
|
||||
type='number'
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const inputValue = e.target.value
|
||||
if (inputValue === '') {
|
||||
// When user clears the input, set to undefined to let backend use default values
|
||||
onChange(undefined)
|
||||
}
|
||||
else {
|
||||
const parsedValue = Number.parseInt(inputValue, 10)
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
const value = Math.max(min, Math.min(max, parsedValue))
|
||||
onChange(value)
|
||||
}
|
||||
}
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
min={min}
|
||||
max={max}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Timeout: FC<Props> = ({ readonly, payload, onChange }) => {
|
||||
const { t } = useTranslation()
|
||||
const { connect, read, write, max_connect_timeout, max_read_timeout, max_write_timeout } = payload ?? {}
|
||||
|
||||
// Get default config from store for max timeout values
|
||||
const nodesDefaultConfigs = useStore(s => s.nodesDefaultConfigs)
|
||||
const defaultConfig = nodesDefaultConfigs?.[BlockEnum.HttpRequest]
|
||||
const defaultTimeout = defaultConfig?.timeout || {}
|
||||
|
||||
return (
|
||||
<FieldCollapse title={t(`${i18nPrefix}.timeout.title`)}>
|
||||
<div className='mt-2 space-y-1'>
|
||||
<div className="space-y-3">
|
||||
<InputField
|
||||
title={t('workflow.nodes.http.timeout.connectLabel')!}
|
||||
description={t('workflow.nodes.http.timeout.connectPlaceholder')!}
|
||||
placeholder={t('workflow.nodes.http.timeout.connectPlaceholder')!}
|
||||
readOnly={readonly}
|
||||
value={connect}
|
||||
onChange={v => onChange?.({ ...payload, connect: v })}
|
||||
min={1}
|
||||
max={max_connect_timeout || defaultTimeout.max_connect_timeout || 10}
|
||||
/>
|
||||
<InputField
|
||||
title={t('workflow.nodes.http.timeout.readLabel')!}
|
||||
description={t('workflow.nodes.http.timeout.readPlaceholder')!}
|
||||
placeholder={t('workflow.nodes.http.timeout.readPlaceholder')!}
|
||||
readOnly={readonly}
|
||||
value={read}
|
||||
onChange={v => onChange?.({ ...payload, read: v })}
|
||||
min={1}
|
||||
max={max_read_timeout || defaultTimeout.max_read_timeout || 600}
|
||||
/>
|
||||
<InputField
|
||||
title={t('workflow.nodes.http.timeout.writeLabel')!}
|
||||
description={t('workflow.nodes.http.timeout.writePlaceholder')!}
|
||||
placeholder={t('workflow.nodes.http.timeout.writePlaceholder')!}
|
||||
readOnly={readonly}
|
||||
value={write}
|
||||
onChange={v => onChange?.({ ...payload, write: v })}
|
||||
min={1}
|
||||
max={max_write_timeout || defaultTimeout.max_write_timeout || 600}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</FieldCollapse>
|
||||
)
|
||||
}
|
||||
export default React.memo(Timeout)
|
||||
60
dify/web/app/components/workflow/nodes/http/default.ts
Normal file
60
dify/web/app/components/workflow/nodes/http/default.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { NodeDefault } from '../../types'
|
||||
import { AuthorizationType, BodyType, Method } from './types'
|
||||
import type { BodyPayload, HttpNodeType } from './types'
|
||||
import { genNodeMetaData } from '@/app/components/workflow/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { BlockClassificationEnum } from '@/app/components/workflow/block-selector/types'
|
||||
|
||||
const metaData = genNodeMetaData({
|
||||
classification: BlockClassificationEnum.Utilities,
|
||||
sort: 1,
|
||||
type: BlockEnum.HttpRequest,
|
||||
})
|
||||
const nodeDefault: NodeDefault<HttpNodeType> = {
|
||||
metaData,
|
||||
defaultValue: {
|
||||
variables: [],
|
||||
method: Method.get,
|
||||
url: '',
|
||||
authorization: {
|
||||
type: AuthorizationType.none,
|
||||
config: null,
|
||||
},
|
||||
headers: '',
|
||||
params: '',
|
||||
body: {
|
||||
type: BodyType.none,
|
||||
data: [],
|
||||
},
|
||||
ssl_verify: true,
|
||||
timeout: {
|
||||
max_connect_timeout: 0,
|
||||
max_read_timeout: 0,
|
||||
max_write_timeout: 0,
|
||||
},
|
||||
retry_config: {
|
||||
retry_enabled: true,
|
||||
max_retries: 3,
|
||||
retry_interval: 100,
|
||||
},
|
||||
},
|
||||
checkValid(payload: HttpNodeType, t: any) {
|
||||
let errorMessages = ''
|
||||
|
||||
if (!errorMessages && !payload.url)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.api') })
|
||||
|
||||
if (!errorMessages
|
||||
&& payload.body.type === BodyType.binary
|
||||
&& ((!(payload.body.data as BodyPayload)[0]?.file) || (payload.body.data as BodyPayload)[0]?.file?.length === 0)
|
||||
)
|
||||
errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('workflow.nodes.http.binaryFileVariable') })
|
||||
|
||||
return {
|
||||
isValid: !errorMessages,
|
||||
errorMessage: errorMessages,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default nodeDefault
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { uniqueId } from 'lodash-es'
|
||||
import type { KeyValue } from '../types'
|
||||
|
||||
const UNIQUE_ID_PREFIX = 'key-value-'
|
||||
const strToKeyValueList = (value: string) => {
|
||||
return value.split('\n').map((item) => {
|
||||
const [key, ...others] = item.split(':')
|
||||
return {
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
key: key.trim(),
|
||||
value: others.join(':').trim(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const useKeyValueList = (value: string, onChange: (value: string) => void, noFilter?: boolean) => {
|
||||
const [list, doSetList] = useState<KeyValue[]>(() => value ? strToKeyValueList(value) : [])
|
||||
const setList = (l: KeyValue[]) => {
|
||||
doSetList(l.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
id: item.id || uniqueId(UNIQUE_ID_PREFIX),
|
||||
}
|
||||
}))
|
||||
}
|
||||
useEffect(() => {
|
||||
if (noFilter)
|
||||
return
|
||||
const newValue = list.filter(item => item.key && item.value).map(item => `${item.key}:${item.value}`).join('\n')
|
||||
if (newValue !== value)
|
||||
onChange(newValue)
|
||||
}, [list, noFilter])
|
||||
const addItem = useCallback(() => {
|
||||
setList([...list, {
|
||||
id: uniqueId(UNIQUE_ID_PREFIX),
|
||||
key: '',
|
||||
value: '',
|
||||
}])
|
||||
}, [list])
|
||||
|
||||
const [isKeyValueEdit, {
|
||||
toggle: toggleIsKeyValueEdit,
|
||||
}] = useBoolean(true)
|
||||
|
||||
return {
|
||||
list: list.length === 0 ? [{ id: uniqueId(UNIQUE_ID_PREFIX), key: '', value: '' }] : list, // no item can not add new item
|
||||
setList,
|
||||
addItem,
|
||||
isKeyValueEdit,
|
||||
toggleIsKeyValueEdit,
|
||||
}
|
||||
}
|
||||
|
||||
export default useKeyValueList
|
||||
30
dify/web/app/components/workflow/nodes/http/node.tsx
Normal file
30
dify/web/app/components/workflow/nodes/http/node.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import ReadonlyInputWithSelectVar from '../_base/components/readonly-input-with-select-var'
|
||||
import type { HttpNodeType } from './types'
|
||||
import type { NodeProps } from '@/app/components/workflow/types'
|
||||
const Node: FC<NodeProps<HttpNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { method, url } = data
|
||||
if (!url)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='mb-1 px-3 py-1'>
|
||||
<div className='flex items-center justify-start rounded-md bg-workflow-block-parma-bg p-1'>
|
||||
<div className='flex h-4 shrink-0 items-center rounded bg-components-badge-white-to-dark px-1 text-xs font-semibold uppercase text-text-secondary'>{method}</div>
|
||||
<div className='w-0 grow pl-1 pt-1'>
|
||||
<ReadonlyInputWithSelectVar
|
||||
className='text-text-secondary'
|
||||
value={url}
|
||||
nodeId={id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(Node)
|
||||
197
dify/web/app/components/workflow/nodes/http/panel.tsx
Normal file
197
dify/web/app/components/workflow/nodes/http/panel.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useConfig from './use-config'
|
||||
import ApiInput from './components/api-input'
|
||||
import KeyValue from './components/key-value'
|
||||
import EditBody from './components/edit-body'
|
||||
import AuthorizationModal from './components/authorization'
|
||||
import type { HttpNodeType } from './types'
|
||||
import Timeout from './components/timeout'
|
||||
import CurlPanel from './components/curl-panel'
|
||||
import cn from '@/utils/classnames'
|
||||
import Switch from '@/app/components/base/switch'
|
||||
import Field from '@/app/components/workflow/nodes/_base/components/field'
|
||||
import Split from '@/app/components/workflow/nodes/_base/components/split'
|
||||
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
|
||||
import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { FileArrow01 } from '@/app/components/base/icons/src/vender/line/files'
|
||||
import type { NodePanelProps } from '@/app/components/workflow/types'
|
||||
|
||||
const i18nPrefix = 'workflow.nodes.http'
|
||||
|
||||
const Panel: FC<NodePanelProps<HttpNodeType>> = ({
|
||||
id,
|
||||
data,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
readOnly,
|
||||
isDataReady,
|
||||
inputs,
|
||||
handleMethodChange,
|
||||
handleUrlChange,
|
||||
headers,
|
||||
setHeaders,
|
||||
addHeader,
|
||||
params,
|
||||
setParams,
|
||||
addParam,
|
||||
setBody,
|
||||
isShowAuthorization,
|
||||
showAuthorization,
|
||||
hideAuthorization,
|
||||
setAuthorization,
|
||||
setTimeout,
|
||||
isShowCurlPanel,
|
||||
showCurlPanel,
|
||||
hideCurlPanel,
|
||||
handleCurlImport,
|
||||
handleSSLVerifyChange,
|
||||
} = useConfig(id, data)
|
||||
// To prevent prompt editor in body not update data.
|
||||
if (!isDataReady)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='pt-2'>
|
||||
<div className='space-y-4 px-4 pb-4'>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.api`)}
|
||||
required
|
||||
operations={
|
||||
<div className='flex'>
|
||||
<div
|
||||
onClick={showAuthorization}
|
||||
className={cn(!readOnly && 'cursor-pointer hover:bg-state-base-hover', 'flex h-6 items-center space-x-1 rounded-md px-2 ')}
|
||||
>
|
||||
{!readOnly && <Settings01 className='h-3 w-3 text-text-tertiary' />}
|
||||
<div className='text-xs font-medium text-text-tertiary'>
|
||||
{t(`${i18nPrefix}.authorization.authorization`)}
|
||||
<span className='ml-1 text-text-secondary'>{t(`${i18nPrefix}.authorization.${inputs.authorization.type}`)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
onClick={showCurlPanel}
|
||||
className={cn(!readOnly && 'cursor-pointer hover:bg-state-base-hover', 'flex h-6 items-center space-x-1 rounded-md px-2 ')}
|
||||
>
|
||||
{!readOnly && <FileArrow01 className='h-3 w-3 text-text-tertiary' />}
|
||||
<div className='text-xs font-medium text-text-tertiary'>
|
||||
{t(`${i18nPrefix}.curl.title`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ApiInput
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
method={inputs.method}
|
||||
onMethodChange={handleMethodChange}
|
||||
url={inputs.url}
|
||||
onUrlChange={handleUrlChange}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.headers`)}
|
||||
>
|
||||
<KeyValue
|
||||
nodeId={id}
|
||||
list={headers}
|
||||
onChange={setHeaders}
|
||||
onAdd={addHeader}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.params`)}
|
||||
>
|
||||
<KeyValue
|
||||
nodeId={id}
|
||||
list={params}
|
||||
onChange={setParams}
|
||||
onAdd={addParam}
|
||||
readonly={readOnly}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.body`)}
|
||||
required
|
||||
>
|
||||
<EditBody
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
payload={inputs.body}
|
||||
onChange={setBody}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
title={t(`${i18nPrefix}.verifySSL.title`)}
|
||||
tooltip={t(`${i18nPrefix}.verifySSL.warningTooltip`)}
|
||||
operations={
|
||||
<Switch
|
||||
defaultValue={!!inputs.ssl_verify}
|
||||
onChange={handleSSLVerifyChange}
|
||||
size='md'
|
||||
disabled={readOnly}
|
||||
/>
|
||||
}>
|
||||
</Field>
|
||||
</div>
|
||||
<Split />
|
||||
<Timeout
|
||||
nodeId={id}
|
||||
readonly={readOnly}
|
||||
payload={inputs.timeout}
|
||||
onChange={setTimeout}
|
||||
/>
|
||||
{(isShowAuthorization && !readOnly) && (
|
||||
<AuthorizationModal
|
||||
nodeId={id}
|
||||
isShow
|
||||
onHide={hideAuthorization}
|
||||
payload={inputs.authorization}
|
||||
onChange={setAuthorization}
|
||||
/>
|
||||
)}
|
||||
<Split />
|
||||
<div className=''>
|
||||
<OutputVars>
|
||||
<>
|
||||
<VarItem
|
||||
name='body'
|
||||
type='string'
|
||||
description={t(`${i18nPrefix}.outputVars.body`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='status_code'
|
||||
type='number'
|
||||
description={t(`${i18nPrefix}.outputVars.statusCode`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='headers'
|
||||
type='object'
|
||||
description={t(`${i18nPrefix}.outputVars.headers`)}
|
||||
/>
|
||||
<VarItem
|
||||
name='files'
|
||||
type='Array[File]'
|
||||
description={t(`${i18nPrefix}.outputVars.files`)}
|
||||
/>
|
||||
</>
|
||||
</OutputVars>
|
||||
</div>
|
||||
{(isShowCurlPanel && !readOnly) && (
|
||||
<CurlPanel
|
||||
nodeId={id}
|
||||
isShow
|
||||
onHide={hideCurlPanel}
|
||||
handleCurlImport={handleCurlImport}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Panel)
|
||||
85
dify/web/app/components/workflow/nodes/http/types.ts
Normal file
85
dify/web/app/components/workflow/nodes/http/types.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { CommonNodeType, ValueSelector, Variable } from '@/app/components/workflow/types'
|
||||
|
||||
export enum Method {
|
||||
get = 'get',
|
||||
post = 'post',
|
||||
head = 'head',
|
||||
patch = 'patch',
|
||||
put = 'put',
|
||||
delete = 'delete',
|
||||
}
|
||||
|
||||
export enum BodyType {
|
||||
none = 'none',
|
||||
formData = 'form-data',
|
||||
xWwwFormUrlencoded = 'x-www-form-urlencoded',
|
||||
rawText = 'raw-text',
|
||||
json = 'json',
|
||||
binary = 'binary',
|
||||
}
|
||||
|
||||
export type KeyValue = {
|
||||
id?: string
|
||||
key: string
|
||||
value: string
|
||||
type?: string
|
||||
file?: ValueSelector
|
||||
}
|
||||
|
||||
export enum BodyPayloadValueType {
|
||||
text = 'text',
|
||||
file = 'file',
|
||||
}
|
||||
|
||||
export type BodyPayload = {
|
||||
id?: string
|
||||
key?: string
|
||||
type: BodyPayloadValueType
|
||||
file?: ValueSelector // when type is file
|
||||
value?: string // when type is text
|
||||
}[]
|
||||
export type Body = {
|
||||
type: BodyType
|
||||
data: string | BodyPayload // string is deprecated, it would convert to BodyPayload after loaded
|
||||
}
|
||||
|
||||
export enum AuthorizationType {
|
||||
none = 'no-auth',
|
||||
apiKey = 'api-key',
|
||||
}
|
||||
|
||||
export enum APIType {
|
||||
basic = 'basic',
|
||||
bearer = 'bearer',
|
||||
custom = 'custom',
|
||||
}
|
||||
|
||||
export type Authorization = {
|
||||
type: AuthorizationType
|
||||
config?: {
|
||||
type: APIType
|
||||
api_key: string
|
||||
header?: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export type Timeout = {
|
||||
connect?: number
|
||||
read?: number
|
||||
write?: number
|
||||
max_connect_timeout?: number
|
||||
max_read_timeout?: number
|
||||
max_write_timeout?: number
|
||||
}
|
||||
|
||||
export type HttpNodeType = CommonNodeType & {
|
||||
variables: Variable[]
|
||||
method: Method
|
||||
url: string
|
||||
headers: string
|
||||
params: string
|
||||
body: Body
|
||||
authorization: Authorization
|
||||
timeout: Timeout
|
||||
ssl_verify?: boolean
|
||||
}
|
||||
189
dify/web/app/components/workflow/nodes/http/use-config.ts
Normal file
189
dify/web/app/components/workflow/nodes/http/use-config.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import useVarList from '../_base/hooks/use-var-list'
|
||||
import { VarType } from '../../types'
|
||||
import type { Var } from '../../types'
|
||||
import { useStore } from '../../store'
|
||||
import { type Authorization, type Body, BodyType, type HttpNodeType, type Method, type Timeout } from './types'
|
||||
import useKeyValueList from './hooks/use-key-value-list'
|
||||
import { transformToBodyPayload } from './utils'
|
||||
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
|
||||
const useConfig = (id: string, payload: HttpNodeType) => {
|
||||
const { nodesReadOnly: readOnly } = useNodesReadOnly()
|
||||
|
||||
const defaultConfig = useStore(s => s.nodesDefaultConfigs?.[payload.type])
|
||||
|
||||
const { inputs, setInputs } = useNodeCrud<HttpNodeType>(id, payload)
|
||||
|
||||
const { handleVarListChange, handleAddVariable } = useVarList<HttpNodeType>({
|
||||
inputs,
|
||||
setInputs,
|
||||
})
|
||||
|
||||
const [isDataReady, setIsDataReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const isReady = defaultConfig && Object.keys(defaultConfig).length > 0
|
||||
if (isReady) {
|
||||
const newInputs = {
|
||||
...defaultConfig,
|
||||
...inputs,
|
||||
}
|
||||
const bodyData = newInputs.body.data
|
||||
if (typeof bodyData === 'string') {
|
||||
newInputs.body = {
|
||||
...newInputs.body,
|
||||
data: transformToBodyPayload(bodyData, [BodyType.formData, BodyType.xWwwFormUrlencoded].includes(newInputs.body.type)),
|
||||
}
|
||||
}
|
||||
else if (!bodyData) {
|
||||
newInputs.body = {
|
||||
...newInputs.body,
|
||||
data: [],
|
||||
}
|
||||
}
|
||||
|
||||
setInputs(newInputs)
|
||||
setIsDataReady(true)
|
||||
}
|
||||
}, [defaultConfig])
|
||||
|
||||
const handleMethodChange = useCallback((method: Method) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.method = method
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleUrlChange = useCallback((url: string) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.url = url
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleFieldChange = useCallback((field: string) => {
|
||||
return (value: string) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
(draft as any)[field] = value
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const {
|
||||
list: headers,
|
||||
setList: setHeaders,
|
||||
addItem: addHeader,
|
||||
isKeyValueEdit: isHeaderKeyValueEdit,
|
||||
toggleIsKeyValueEdit: toggleIsHeaderKeyValueEdit,
|
||||
} = useKeyValueList(inputs.headers, handleFieldChange('headers'))
|
||||
|
||||
const {
|
||||
list: params,
|
||||
setList: setParams,
|
||||
addItem: addParam,
|
||||
isKeyValueEdit: isParamKeyValueEdit,
|
||||
toggleIsKeyValueEdit: toggleIsParamKeyValueEdit,
|
||||
} = useKeyValueList(inputs.params, handleFieldChange('params'))
|
||||
|
||||
const setBody = useCallback((data: Body) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.body = data
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
// authorization
|
||||
const [isShowAuthorization, {
|
||||
setTrue: showAuthorization,
|
||||
setFalse: hideAuthorization,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const setAuthorization = useCallback((authorization: Authorization) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.authorization = authorization
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const setTimeout = useCallback((timeout: Timeout) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.timeout = timeout
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const filterVar = useCallback((varPayload: Var) => {
|
||||
return [VarType.string, VarType.number, VarType.secret].includes(varPayload.type)
|
||||
}, [])
|
||||
|
||||
// curl import panel
|
||||
const [isShowCurlPanel, {
|
||||
setTrue: showCurlPanel,
|
||||
setFalse: hideCurlPanel,
|
||||
}] = useBoolean(false)
|
||||
|
||||
const handleCurlImport = useCallback((newNode: HttpNodeType) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.method = newNode.method
|
||||
draft.url = newNode.url
|
||||
draft.headers = newNode.headers
|
||||
draft.params = newNode.params
|
||||
draft.body = newNode.body
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
const handleSSLVerifyChange = useCallback((checked: boolean) => {
|
||||
const newInputs = produce(inputs, (draft: HttpNodeType) => {
|
||||
draft.ssl_verify = checked
|
||||
})
|
||||
setInputs(newInputs)
|
||||
}, [inputs, setInputs])
|
||||
|
||||
return {
|
||||
readOnly,
|
||||
isDataReady,
|
||||
inputs,
|
||||
handleVarListChange,
|
||||
handleAddVariable,
|
||||
filterVar,
|
||||
handleMethodChange,
|
||||
handleUrlChange,
|
||||
// headers
|
||||
headers,
|
||||
setHeaders,
|
||||
addHeader,
|
||||
isHeaderKeyValueEdit,
|
||||
toggleIsHeaderKeyValueEdit,
|
||||
// params
|
||||
params,
|
||||
setParams,
|
||||
addParam,
|
||||
isParamKeyValueEdit,
|
||||
toggleIsParamKeyValueEdit,
|
||||
// body
|
||||
setBody,
|
||||
// ssl verify
|
||||
handleSSLVerifyChange,
|
||||
// authorization
|
||||
isShowAuthorization,
|
||||
showAuthorization,
|
||||
hideAuthorization,
|
||||
setAuthorization,
|
||||
setTimeout,
|
||||
// curl import
|
||||
isShowCurlPanel,
|
||||
showCurlPanel,
|
||||
hideCurlPanel,
|
||||
handleCurlImport,
|
||||
}
|
||||
}
|
||||
|
||||
export default useConfig
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { RefObject } from 'react'
|
||||
import type { InputVar, Variable } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import useNodeCrud from '../_base/hooks/use-node-crud'
|
||||
import type { HttpNodeType } from './types'
|
||||
|
||||
type Params = {
|
||||
id: string,
|
||||
payload: HttpNodeType,
|
||||
runInputData: Record<string, any>
|
||||
runInputDataRef: RefObject<Record<string, any>>
|
||||
getInputVars: (textList: string[]) => InputVar[]
|
||||
setRunInputData: (data: Record<string, any>) => void
|
||||
toVarInputs: (variables: Variable[]) => InputVar[]
|
||||
}
|
||||
const useSingleRunFormParams = ({
|
||||
id,
|
||||
payload,
|
||||
runInputData,
|
||||
getInputVars,
|
||||
setRunInputData,
|
||||
}: Params) => {
|
||||
const { inputs } = useNodeCrud<HttpNodeType>(id, payload)
|
||||
|
||||
const fileVarInputs = useMemo(() => {
|
||||
if (!Array.isArray(inputs.body.data))
|
||||
return ''
|
||||
|
||||
const res = inputs.body.data
|
||||
.filter(item => item.file?.length)
|
||||
.map(item => item.file ? `{{#${item.file.join('.')}#}}` : '')
|
||||
.join(' ')
|
||||
return res
|
||||
}, [inputs.body.data])
|
||||
const varInputs = getInputVars([
|
||||
inputs.url,
|
||||
inputs.headers,
|
||||
inputs.params,
|
||||
typeof inputs.body.data === 'string' ? inputs.body.data : inputs.body.data?.map(item => item.value).join(''),
|
||||
fileVarInputs,
|
||||
])
|
||||
const setInputVarValues = useCallback((newPayload: Record<string, any>) => {
|
||||
setRunInputData(newPayload)
|
||||
}, [setRunInputData])
|
||||
const inputVarValues = (() => {
|
||||
const vars: Record<string, any> = {}
|
||||
Object.keys(runInputData)
|
||||
.forEach((key) => {
|
||||
vars[key] = runInputData[key]
|
||||
})
|
||||
return vars
|
||||
})()
|
||||
|
||||
const forms = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
inputs: varInputs,
|
||||
values: inputVarValues,
|
||||
onChange: setInputVarValues,
|
||||
},
|
||||
]
|
||||
}, [inputVarValues, setInputVarValues, varInputs])
|
||||
|
||||
const getDependentVars = () => {
|
||||
return varInputs.map((item) => {
|
||||
// Guard against null/undefined variable to prevent app crash
|
||||
if (!item.variable || typeof item.variable !== 'string')
|
||||
return []
|
||||
|
||||
return item.variable.slice(1, -1).split('.')
|
||||
}).filter(arr => arr.length > 0)
|
||||
}
|
||||
|
||||
return {
|
||||
forms,
|
||||
getDependentVars,
|
||||
}
|
||||
}
|
||||
|
||||
export default useSingleRunFormParams
|
||||
21
dify/web/app/components/workflow/nodes/http/utils.ts
Normal file
21
dify/web/app/components/workflow/nodes/http/utils.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { type BodyPayload, BodyPayloadValueType } from './types'
|
||||
|
||||
export const transformToBodyPayload = (old: string, hasKey: boolean): BodyPayload => {
|
||||
if (!hasKey) {
|
||||
return [
|
||||
{
|
||||
type: BodyPayloadValueType.text,
|
||||
value: old,
|
||||
},
|
||||
]
|
||||
}
|
||||
const bodyPayload = old.split('\n').map((item) => {
|
||||
const [key, value] = item.split(':')
|
||||
return {
|
||||
key: key || '',
|
||||
type: BodyPayloadValueType.text,
|
||||
value: value || '',
|
||||
}
|
||||
})
|
||||
return bodyPayload
|
||||
}
|
||||
Reference in New Issue
Block a user