Commit b08327cb authored by Joel's avatar Joel

feat: edit body

parent f1b868d5
......@@ -7,6 +7,7 @@ type Props = {
value: string
onChange: (value: string) => void
title: JSX.Element
language?: string
headerRight?: JSX.Element
}
......
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useBoolean } from 'ahooks'
import Base from './base'
type Props = {
value: string
onChange: (value: string) => void
title: JSX.Element
headerRight?: JSX.Element
minHeight?: number
onBlur?: () => void
}
const TextEditor: FC<Props> = ({
value,
onChange,
title,
headerRight,
minHeight,
onBlur,
}) => {
const [isFocus, {
setTrue: setIsFocus,
setFalse: setIsNotFocus,
}] = useBoolean(false)
const handleBlur = useCallback(() => {
setIsNotFocus()
onBlur?.()
}, [setIsNotFocus, onBlur])
return (
<div>
<Base
title={title}
value={value}
headerRight={headerRight}
isFocus={isFocus}
minHeight={minHeight}
>
<textarea
value={value}
onChange={e => onChange(e.target.value)}
onFocus={setIsFocus}
onBlur={handleBlur}
className='w-full h-full p-3 resize-none bg-transparent'
/>
</Base>
</div>
)
}
export default React.memo(TextEditor)
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { MethodEnum } from '../types'
import { Method } from '../types'
import Selector from '../../_base/components/selector'
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
const MethodOptions = [
{ label: 'GET', value: MethodEnum.get },
{ label: 'POST', value: MethodEnum.post },
{ label: 'HEAD', value: MethodEnum.head },
{ label: 'PATCH', value: MethodEnum.patch },
{ label: 'PUT', value: MethodEnum.put },
{ label: 'DELETE', value: MethodEnum.delete },
{ 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 = {
readonly: boolean
method: MethodEnum
onMethodChange: (method: MethodEnum) => void
method: Method
onMethodChange: (method: Method) => void
url: string
onUrlChange: (url: string) => void
}
......
'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 TextEditor from '../../../_base/components/editor/text-editor'
import CodeEditor from '../../../_base/components/editor/code-editor'
type Props = {
readonly: boolean
payload: Body
onChange: (newPayload: 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,
payload,
onChange,
}) => {
const { type } = payload
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,
isKeyValueEdit: isBodyKeyValueEdit,
toggleIsKeyValueEdit: toggleIsBodyKeyValueEdit,
} = useKeyValueList(payload.data)
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
}, [body, 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}
/>
<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}
list={body}
onChange={setBody}
onAdd={addBody}
isKeyValueEdit={isBodyKeyValueEdit}
toggleKeyValueEdit={toggleIsBodyKeyValueEdit}
/>
)}
{type === BodyType.rawText && (
<TextEditor
title={<div className='uppercase'>Raw text</div>}
onChange={handleBodyValueChange}
value={payload.data}
minHeight={150}
/>
)}
{type === BodyType.json && (
<CodeEditor
title={<div className='uppercase'>JSON</div>}
value={payload.data} onChange={handleBodyValueChange}
/>
)}
</div>
</div>
)
}
export default React.memo(EditBody)
......@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import Editor from '@/app/components/workflow/nodes/_base/components/editor/base'
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'
......@@ -21,12 +21,14 @@ const BulkEdit: FC<Props> = ({
const { t } = useTranslation()
const [tempValue, setTempValue] = React.useState(value)
const [isFocus, setIsFocus] = React.useState(false)
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTempValue(e.target.value)
const handleChange = useCallback((value: string) => {
setTempValue(value)
}, [])
const handleBlur = useCallback(() => {
onChange(tempValue)
}, [tempValue, onChange])
const handleSwitchToKeyValueEdit = useCallback(() => {
onChange(tempValue)
onSwitchToKeyValueEdit()
......@@ -34,9 +36,11 @@ const BulkEdit: FC<Props> = ({
return (
<div>
<Editor
<TextEditor
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
value={value}
value={tempValue}
onChange={handleChange}
onBlur={handleBlur}
headerRight={
<div className='flex items-center h-[18px]'>
<div
......@@ -49,17 +53,8 @@ const BulkEdit: FC<Props> = ({
<div className='ml-3 mr-1.5 w-px h-3 bg-gray-200'></div>
</div>
}
isFocus={isFocus}
minHeight={150}
>
<textarea
value={tempValue}
onChange={handleChange}
onFocus={() => setIsFocus(true)}
onBlur={() => setIsFocus(false)}
className='w-full h-full p-3 resize-none bg-transparent'
/>
</Editor>
</div>
)
}
......
......@@ -5,18 +5,21 @@ import type { KeyValue } from '../types'
const strToKeyValueList = (value: string) => {
return value.split('\n').map((item) => {
const [key, value] = item.split(':')
return { key: key.trim(), value: value.trim() }
return { key: key.trim(), value: value?.trim() }
})
}
const useKeyValueList = (value: string) => {
const [list, setList] = useState<KeyValue[]>(value ? strToKeyValueList(value) : [])
const addItem = useCallback(() => {
setList(prev => [...prev, { key: '', value: '' }])
}, [])
const [isKeyValueEdit, {
toggle: toggleIsKeyValueEdit,
}] = useBoolean(false)
}] = useBoolean(true)
return {
list: list.length === 0 ? [{ key: '', value: '' }] : list, // no item can not add new item
setList,
......
import { BlockEnum } from '../../types'
import { MethodEnum } from './types'
import { BodyType, Method } from './types'
import type { HttpNodeType } from './types'
export const mockData: HttpNodeType = {
......@@ -16,12 +16,12 @@ export const mockData: HttpNodeType = {
value_selector: ['bbb', 'b', 'c'],
},
],
method: MethodEnum.get,
method: Method.get,
url: 'https://api.dify.com/xx',
headers: 'Content-Type: application/json\nAccept: */*',
params: '',
body: {
type: 'json',
type: BodyType.none,
data: '',
},
}
......@@ -4,12 +4,12 @@ import useConfig from './use-config'
import { mockData } from './mock'
import ApiInput from './components/api-input'
import KeyValue from './components/key-value'
import EditBody from './components/edit-body'
import VarList from '@/app/components/workflow/nodes/_base/components/variable/var-list'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import AddButton from '@/app/components/base/button/add-button'
import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
const i18nPrefix = 'workflow.nodes.http'
const Panel: FC = () => {
......@@ -27,6 +27,12 @@ const Panel: FC = () => {
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
params,
setParams,
addParam,
isParamKeyValueEdit,
toggleIsParamKeyValueEdit,
setBody,
} = useConfig(mockData)
return (
......@@ -70,12 +76,23 @@ const Panel: FC = () => {
<Field
title={t(`${i18nPrefix}.params`)}
>
params
<KeyValue
list={params}
onChange={setParams}
onAdd={addParam}
readonly={readOnly}
isKeyValueEdit={isParamKeyValueEdit}
toggleKeyValueEdit={toggleIsParamKeyValueEdit}
/>
</Field>
<Field
title={t(`${i18nPrefix}.body`)}
>
body
<EditBody
readonly={readOnly}
payload={inputs.body}
onChange={setBody}
/>
</Field>
</div>
<Split />
......
import type { CommonNodeType, Variable } from '@/app/components/workflow/types'
export enum MethodEnum {
export enum Method {
get = 'get',
post = 'post',
head = 'head',
......@@ -9,19 +9,29 @@ export enum MethodEnum {
delete = 'delete',
}
export enum BodyType {
none = 'none',
formData = 'form-data',
xWwwFormUrlencoded = 'x-www-form-urlencoded',
rawText = 'raw-text',
json = 'json',
}
export type KeyValue = {
key: string
value: string
}
export type Body = {
type: BodyType
data: string
}
export type HttpNodeType = CommonNodeType & {
variables: Variable[]
method: MethodEnum
method: Method
url: string
headers: string
params: string
body: {
type: string
data: string
}
body: Body
}
import { useCallback, useState } from 'react'
import produce from 'immer'
import useVarList from '../_base/hooks/use-var-list'
import type { HttpNodeType, MethodEnum } from './types'
import type { Body, HttpNodeType, Method } from './types'
import useKeyValueList from './hooks/use-key-value-list'
const useConfig = (initInputs: HttpNodeType) => {
const [inputs, setInputs] = useState<HttpNodeType>(initInputs)
......@@ -10,7 +11,7 @@ const useConfig = (initInputs: HttpNodeType) => {
setInputs,
})
const handleMethodChange = useCallback((method: MethodEnum) => {
const handleMethodChange = useCallback((method: Method) => {
setInputs(prev => ({
...prev,
method,
......@@ -32,17 +33,41 @@ const useConfig = (initInputs: HttpNodeType) => {
toggleIsKeyValueEdit: toggleIsHeaderKeyValueEdit,
} = useKeyValueList(inputs.headers)
const {
list: params,
setList: setParams,
addItem: addParam,
isKeyValueEdit: isParamKeyValueEdit,
toggleIsKeyValueEdit: toggleIsParamKeyValueEdit,
} = useKeyValueList(inputs.params)
const setBody = useCallback((data: Body) => {
const newInputs = produce(inputs, (draft: HttpNodeType) => {
draft.body = data
})
setInputs(newInputs)
}, [inputs, setInputs])
return {
inputs,
handleVarListChange,
handleAddVariable,
handleMethodChange,
handleUrlChange,
// headers
headers,
setHeaders,
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
// params
params,
setParams,
addParam,
isParamKeyValueEdit,
toggleIsParamKeyValueEdit,
// body
setBody,
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment