Commit 76ff004e authored by Joel's avatar Joel

feat: bulk edit

parent df173764
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9H21M3 15H21M12 3V21M7.8 3H16.2C17.8802 3 18.7202 3 19.362 3.32698C19.9265 3.6146 20.3854 4.07354 20.673 4.63803C21 5.27976 21 6.11984 21 7.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.00195 4C3.00195 3.44772 3.44967 3 4.00195 3H20.002C20.5542 3 21.002 3.44772 21.002 4C21.002 4.55228 20.5542 5 20.002 5H4.00195C3.44967 5 3.00195 4.55228 3.00195 4Z" fill="black"/>
<path d="M3.00195 8C3.00195 7.44772 3.44967 7 4.00195 7H10.502C11.0542 7 11.502 7.44772 11.502 8C11.502 8.55228 11.0542 9 10.502 9H4.00195C3.44967 9 3.00195 8.55228 3.00195 8Z" fill="black"/>
<path d="M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H7.0022C7.55448 13 8.0022 12.5523 8.0022 12C8.0022 11.4477 7.55448 11 7.0022 11H4Z" fill="black"/>
<path d="M19.2584 8.70705C18.0868 7.53548 16.1873 7.53547 15.0158 8.70705L7.29485 16.428C7.10731 16.6155 7.00195 16.8699 7.00195 17.1351V20.9999C7.00195 21.5522 7.44967 21.9999 8.00195 21.9999H11.8668C12.132 21.9999 12.3864 21.8946 12.5739 21.7071L20.2948 13.9861C21.4664 12.8146 21.4664 10.9151 20.2948 9.74349L19.2584 8.70705Z" fill="black"/>
</svg>
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3 9H21M3 15H21M12 3V21M7.8 3H16.2C17.8802 3 18.7202 3 19.362 3.32698C19.9265 3.6146 20.3854 4.07354 20.673 4.63803C21 5.27976 21 6.11984 21 7.8V16.2C21 17.8802 21 18.7202 20.673 19.362C20.3854 19.9265 19.9265 20.3854 19.362 20.673C18.7202 21 17.8802 21 16.2 21H7.8C6.11984 21 5.27976 21 4.63803 20.673C4.07354 20.3854 3.6146 19.9265 3.32698 19.362C3 18.7202 3 17.8802 3 16.2V7.8C3 6.11984 3 5.27976 3.32698 4.63803C3.6146 4.07354 4.07354 3.6146 4.63803 3.32698C5.27976 3 6.11984 3 7.8 3Z",
"stroke": "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round"
},
"children": []
}
]
},
"name": "LayoutGrid02"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './LayoutGrid02.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'LayoutGrid02'
export default Icon
export { default as AlignLeft01 } from './AlignLeft01'
export { default as AlignRight01 } from './AlignRight01'
export { default as Grid01 } from './Grid01'
export { default as LayoutGrid02 } from './LayoutGrid02'
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "24",
"height": "24",
"viewBox": "0 0 24 24",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.00195 4C3.00195 3.44772 3.44967 3 4.00195 3H20.002C20.5542 3 21.002 3.44772 21.002 4C21.002 4.55228 20.5542 5 20.002 5H4.00195C3.44967 5 3.00195 4.55228 3.00195 4Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.00195 8C3.00195 7.44772 3.44967 7 4.00195 7H10.502C11.0542 7 11.502 7.44772 11.502 8C11.502 8.55228 11.0542 9 10.502 9H4.00195C3.44967 9 3.00195 8.55228 3.00195 8Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M4 11C3.44772 11 3 11.4477 3 12C3 12.5523 3.44772 13 4 13H7.0022C7.55448 13 8.0022 12.5523 8.0022 12C8.0022 11.4477 7.55448 11 7.0022 11H4Z",
"fill": "currentColor"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M19.2584 8.70705C18.0868 7.53548 16.1873 7.53547 15.0158 8.70705L7.29485 16.428C7.10731 16.6155 7.00195 16.8699 7.00195 17.1351V20.9999C7.00195 21.5522 7.44967 21.9999 8.00195 21.9999H11.8668C12.132 21.9999 12.3864 21.8946 12.5739 21.7071L20.2948 13.9861C21.4664 12.8146 21.4664 10.9151 20.2948 9.74349L19.2584 8.70705Z",
"fill": "currentColor"
},
"children": []
}
]
},
"name": "EditList"
}
\ No newline at end of file
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import * as React from 'react'
import data from './EditList.json'
import IconBase from '@/app/components/base/icons/IconBase'
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
props,
ref,
) => <IconBase {...props} ref={ref} data={data as IconData} />)
Icon.displayName = 'EditList'
export default Icon
export { default as AiText } from './AiText'
export { default as CuteRobote } from './CuteRobote'
export { default as EditList } from './EditList'
export { default as MessageDotsCircle } from './MessageDotsCircle'
export { default as MessageFast } from './MessageFast'
export { default as MessageHeartCircle } from './MessageHeartCircle'
......
'use client'
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 { 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 [isFocus, setIsFocus] = React.useState(false)
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTempValue(e.target.value)
}, [])
const handleSwitchToKeyValueEdit = useCallback(() => {
onChange(tempValue)
onSwitchToKeyValueEdit()
}, [tempValue, onChange, onSwitchToKeyValueEdit])
return (
<div>
<Editor
title={<div className='uppercase'>{t(`${i18nPrefix}.bulkEdit`)}</div>}
value={value}
headerRight={
<div className='flex items-center h-[18px]'>
<div
className='flex items-center space-x-1 cursor-pointer'
onClick={handleSwitchToKeyValueEdit}
>
<LayoutGrid02 className='w-3 h-3 text-gray-500' />
<div className='leading-[18px] text-xs font-normal text-gray-500'>{t(`${i18nPrefix}.keyValueEdit`)}</div>
</div>
<div className='ml-3 mr-1.5 w-px h-3 bg-gray-200'></div>
</div>
}
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>
)
}
export default React.memo(BulkEdit)
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { KeyValue } from '../../types'
import KeyValueEdit from './key-value-edit'
import BulkEdit from './bulk-edit'
type Props = {
readonly: boolean
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
isKeyValueEdit: boolean
toggleKeyValueEdit: () => void
}
const KeyValueList: FC<Props> = ({
readonly,
list,
onChange,
onAdd,
isKeyValueEdit,
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 (
<>
{isKeyValueEdit
? <KeyValueEdit
readonly={readonly}
list={list}
onChange={onChange}
onAdd={onAdd}
onSwitchToBulkEdit={toggleKeyValueEdit}
/>
: <BulkEdit
value={bulkList}
onChange={handleBulkValueChange}
onSwitchToKeyValueEdit={toggleKeyValueEdit}
/>
}
</>
)
}
export default React.memo(KeyValueList)
......@@ -2,13 +2,20 @@
import type { FC } from 'react'
import React, { useCallback } from 'react'
import produce from 'immer'
import type { KeyValue } from '../../types'
import { useTranslation } from 'react-i18next'
import type { KeyValue } from '../../../types'
import KeyValueItem from './item'
import TooltipPlus from '@/app/components/base/tooltip-plus'
import { EditList } from '@/app/components/base/icons/src/vender/solid/communication'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
readonly: boolean
list: KeyValue[]
onChange: (newList: KeyValue[]) => void
onAdd: () => void
onSwitchToBulkEdit: () => void
}
const KeyValueList: FC<Props> = ({
......@@ -16,7 +23,10 @@ const KeyValueList: FC<Props> = ({
list,
onChange,
onAdd,
onSwitchToBulkEdit,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((index: number) => {
return (newItem: KeyValue) => {
const newList = produce(list, (draft: any) => {
......@@ -38,8 +48,20 @@ const KeyValueList: FC<Props> = ({
return (
<div className='border border-gray-200 rounded-lg overflow-hidden'>
<div className='flex items-center h-7 leading-7 text-xs font-medium text-gray-500 uppercase'>
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>key</div>
<div className='w-1/2 h-full pl-3'>value</div>
<div className='w-1/2 h-full pl-3 border-r border-gray-200'>{t(`${i18nPrefix}.key`)}</div>
<div className='flex w-1/2 h-full pl-3 pr-1 items-center justify-between'>
<div>{t(`${i18nPrefix}.value`)}</div>
<TooltipPlus
popupContent={t(`${i18nPrefix}.bulkEdit`)}
>
<div
className='p-1 cursor-pointer rounded-md hover:bg-black/5 text-gray-500 hover:text-gray-800'
onClick={onSwitchToBulkEdit}
>
<EditList className='w-3 h-3' />
</div>
</TooltipPlus>
</div>
</div>
{
list.map((item, index) => (
......
......@@ -10,6 +10,7 @@ type Props = {
onChange: (newValue: string) => void
hasRemove: boolean
onRemove?: () => void
placeholder?: string
}
const InputItem: FC<Props> = ({
......@@ -18,7 +19,9 @@ const InputItem: FC<Props> = ({
onChange,
hasRemove,
onRemove,
placeholder,
}) => {
const hasValue = !!value
const [isEdit, {
setTrue: setIsEditTrue,
setFalse: setIsEditFalse,
......@@ -39,18 +42,19 @@ const InputItem: FC<Props> = ({
? (
<input
type='text'
className='w-full h-[18px] leading-[18px] pl-0.5 text-gray-900 text-xs font-normal placeholder:text-gray-400 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
className='w-full h-[18px] leading-[18px] pl-0.5 text-gray-900 text-xs font-normal placeholder:text-gray-300 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
value={value}
onChange={handleChange}
onBlur={setIsEditFalse}
autoFocus
placeholder={placeholder}
/>
)
: <div
className="pl-0.5 w-full h-[18px] leading-[18px]"
onClick={setIsEditTrue}
>
<div className='text-gray-900 text-xs font-normal'>{value}</div>
<div className={cn(hasValue ? 'text-gray-900' : 'text-gray-300', 'text-xs font-normal')}>{hasValue ? value : placeholder}</div>
{hasRemove && !isEdit && (
<RemoveButton
className='group-hover:block hidden absolute right-1 top-0.5'
......
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import cn from 'classnames'
import produce from 'immer'
import type { KeyValue } from '../../types'
import type { KeyValue } from '../../../types'
import InputItem from './input-item'
const i18nPrefix = 'workflow.nodes.http'
type Props = {
className?: string
readonly: boolean
......@@ -27,6 +30,8 @@ const KeyValueItem: FC<Props> = ({
isLastItem,
onAdd,
}) => {
const { t } = useTranslation()
const handleChange = useCallback((key: string) => {
return (value: string) => {
const newPayload = produce(payload, (draft: any) => {
......@@ -46,7 +51,9 @@ const KeyValueItem: FC<Props> = ({
className='pr-2.5'
value={payload.key}
onChange={handleChange('key')}
hasRemove={false} />
hasRemove={false}
placeholder={t(`${i18nPrefix}.key`)!}
/>
</div>
<div className='w-1/2 h-full'>
<InputItem
......@@ -55,6 +62,7 @@ const KeyValueItem: FC<Props> = ({
onChange={handleChange('value')}
hasRemove={!readonly && canRemove}
onRemove={onRemove}
placeholder={t(`${i18nPrefix}.value`)!}
/>
</div>
</div>
......
......@@ -16,7 +16,7 @@ const useKeyValueList = (value: string) => {
}, [])
const [isKeyValueEdit, {
toggle: toggleIsKeyValueEdit,
}] = useBoolean(true)
}] = useBoolean(false)
return {
list: list.length === 0 ? [{ key: '', value: '' }] : list, // no item can not add new item
setList,
......
......@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import useConfig from './use-config'
import { mockData } from './mock'
import ApiInput from './components/api-input'
import KeyValueList from './components/key-value/list'
import KeyValue from './components/key-value'
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'
......@@ -25,6 +25,8 @@ const Panel: FC = () => {
headers,
setHeaders,
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
} = useConfig(mockData)
return (
......@@ -56,11 +58,13 @@ const Panel: FC = () => {
<Field
title={t(`${i18nPrefix}.headers`)}
>
<KeyValueList
<KeyValue
list={headers}
onChange={setHeaders}
onAdd={addHeader}
readonly={readOnly}
isKeyValueEdit={isHeaderKeyValueEdit}
toggleKeyValueEdit={toggleIsHeaderKeyValueEdit}
/>
</Field>
<Field
......
......@@ -24,7 +24,13 @@ const useConfig = (initInputs: HttpNodeType) => {
}))
}, [])
const { list: headers, setList: setHeaders, addItem: addHeader } = useKeyValueList(inputs.headers)
const {
list: headers,
setList: setHeaders,
addItem: addHeader,
isKeyValueEdit: isHeaderKeyValueEdit,
toggleIsKeyValueEdit: toggleIsHeaderKeyValueEdit,
} = useKeyValueList(inputs.headers)
return {
inputs,
......@@ -35,6 +41,8 @@ const useConfig = (initInputs: HttpNodeType) => {
headers,
setHeaders,
addHeader,
isHeaderKeyValueEdit,
toggleIsHeaderKeyValueEdit,
}
}
......
......@@ -49,6 +49,10 @@ const translation = {
http: {
inputVars: 'Input Variables',
api: 'API',
key: 'Key',
value: 'Value',
bulkEdit: 'Bulk Edit',
keyValueEdit: 'Key-Value Edit',
headers: 'Headers',
params: 'Params',
body: 'Body',
......
......@@ -48,6 +48,10 @@ const translation = {
http: {
inputVars: '输入变量',
api: 'API',
key: '键',
value: '值',
bulkEdit: '批量编辑',
keyValueEdit: '键值编辑',
headers: '响应头',
params: '参数',
body: '响应内容',
......
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