Commit b08327cb authored by Joel's avatar Joel

feat: edit body

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