Browse Source

Fix/variable input validation issue (#23300)

tags/1.7.2
Matri Qi 3 months ago
parent
commit
aac849d4f4
No account linked to committer's email address

+ 19
- 9
web/app/components/base/toast/index.tsx View File

close: () => void close: () => void
} }


export type ToastHandle = {
clear?: VoidFunction
}

export const ToastContext = createContext<IToastContext>({} as IToastContext) export const ToastContext = createContext<IToastContext>({} as IToastContext)
export const useToastContext = () => useContext(ToastContext) export const useToastContext = () => useContext(ToastContext)
const Toast = ({ const Toast = ({


return <div className={classNames( return <div className={classNames(
className, className,
'fixed w-[360px] rounded-xl my-4 mx-8 flex-grow z-[9999] overflow-hidden',
'fixed z-[9999] mx-8 my-4 w-[360px] grow overflow-hidden rounded-xl',
size === 'md' ? 'p-3' : 'p-2', size === 'md' ? 'p-3' : 'p-2',
'border border-components-panel-border-subtle bg-components-panel-bg-blur shadow-sm', 'border border-components-panel-border-subtle bg-components-panel-bg-blur shadow-sm',
'top-0', 'top-0',
className, className,
customComponent, customComponent,
onClose, onClose,
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className' | 'customComponent' | 'onClose'>) => {
}: Pick<IToastProps, 'type' | 'size' | 'message' | 'duration' | 'className' | 'customComponent' | 'onClose'>): ToastHandle => {
const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000 const defaultDuring = (type === 'success' || type === 'info') ? 3000 : 6000
const toastHandler: ToastHandle = {}

if (typeof window === 'object') { if (typeof window === 'object') {
const holder = document.createElement('div') const holder = document.createElement('div')
const root = createRoot(holder) const root = createRoot(holder)


toastHandler.clear = () => {
if (holder) {
root.unmount()
holder.remove()
}
onClose?.()
}

root.render( root.render(
<ToastContext.Provider value={{ <ToastContext.Provider value={{
notify: noop, notify: noop,
</ToastContext.Provider>, </ToastContext.Provider>,
) )
document.body.appendChild(holder) document.body.appendChild(holder)
setTimeout(() => {
if (holder) {
root.unmount()
holder.remove()
}
onClose?.()
}, duration || defaultDuring)
setTimeout(toastHandler.clear, duration || defaultDuring)
} }

return toastHandler
} }


export default Toast export default Toast

+ 28
- 20
web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx View File

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback } from 'react'
import React, { useCallback, useState } from 'react'
import produce from 'immer' import produce from 'immer'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import type { OutputVar } from '../../../code/types' import type { OutputVar } from '../../../code/types'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import type { VarType } from '@/app/components/workflow/types' import type { VarType } from '@/app/components/workflow/types'
import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var'
import type { ToastHandle } from '@/app/components/base/toast'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { useDebounceFn } from 'ahooks'


type Props = { type Props = {
readonly: boolean readonly: boolean
onRemove, onRemove,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [toastHandler, setToastHandler] = useState<ToastHandle>()


const list = outputKeyOrders.map((key) => { const list = outputKeyOrders.map((key) => {
return { return {
variable_type: outputs[key]?.type, variable_type: outputs[key]?.type,
} }
}) })

const { run: validateVarInput } = useDebounceFn((existingVariables: typeof list, newKey: string) => {
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
if (!isValid) {
setToastHandler(Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
}))
return
}
if (existingVariables.some(key => key.variable?.trim() === newKey.trim())) {
setToastHandler(Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
}))
}
else {
toastHandler?.clear?.()
}
}, { wait: 500 })

const handleVarNameChange = useCallback((index: number) => { const handleVarNameChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => { return (e: React.ChangeEvent<HTMLInputElement>) => {
const oldKey = list[index].variable const oldKey = list[index].variable
replaceSpaceWithUnderscreInVarNameInput(e.target) replaceSpaceWithUnderscreInVarNameInput(e.target)
const newKey = e.target.value const newKey = e.target.value


const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}

if (list.map(item => item.variable?.trim()).includes(newKey.trim())) {
Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
})
return
}
toastHandler?.clear?.()
validateVarInput(list.toSpliced(index, 1), newKey)


const newOutputs = produce(outputs, (draft) => { const newOutputs = produce(outputs, (draft) => {
draft[newKey] = draft[oldKey] draft[newKey] = draft[oldKey]
}) })
onChange(newOutputs, index, newKey) onChange(newOutputs, index, newKey)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list, onChange, outputs, outputKeyOrders])
}, [list, onChange, outputs, outputKeyOrders, validateVarInput])


const handleVarTypeChange = useCallback((index: number) => { const handleVarTypeChange = useCallback((index: number) => {
return (value: string) => { return (value: string) => {
}) })
onChange(newOutputs) onChange(newOutputs)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list, onChange, outputs, outputKeyOrders]) }, [list, onChange, outputs, outputKeyOrders])


const handleVarRemove = useCallback((index: number) => { const handleVarRemove = useCallback((index: number) => {

+ 28
- 17
web/app/components/workflow/nodes/_base/components/variable/var-list.tsx View File

'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import React, { useCallback, useMemo } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import produce from 'immer' import produce from 'immer'
import RemoveButton from '../remove-button' import RemoveButton from '../remove-button'
import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var' import { checkKeys, replaceSpaceWithUnderscreInVarNameInput } from '@/utils/var'
import type { ToastHandle } from '@/app/components/base/toast'
import Toast from '@/app/components/base/toast' import Toast from '@/app/components/base/toast'
import { ReactSortable } from 'react-sortablejs' import { ReactSortable } from 'react-sortablejs'
import { v4 as uuid4 } from 'uuid' import { v4 as uuid4 } from 'uuid'
import { RiDraggable } from '@remixicon/react' import { RiDraggable } from '@remixicon/react'
import cn from '@/utils/classnames' import cn from '@/utils/classnames'
import { useDebounceFn } from 'ahooks'


type Props = { type Props = {
nodeId: string nodeId: string
isSupportFileVar = true, isSupportFileVar = true,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [toastHandle, setToastHandle] = useState<ToastHandle>()


const listWithIds = useMemo(() => list.map((item) => { const listWithIds = useMemo(() => list.map((item) => {
const id = uuid4() const id = uuid4()
} }
}), [list]) }), [list])


const { run: validateVarInput } = useDebounceFn((list: Variable[], newKey: string) => {
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
if (!isValid) {
setToastHandle(Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
}))
return
}
if (list.some(item => item.variable?.trim() === newKey.trim())) {
console.log('new key', newKey.trim())
setToastHandle(Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
}))
}
else {
toastHandle?.clear?.()
}
}, { wait: 500 })

const handleVarNameChange = useCallback((index: number) => { const handleVarNameChange = useCallback((index: number) => {
return (e: React.ChangeEvent<HTMLInputElement>) => { return (e: React.ChangeEvent<HTMLInputElement>) => {
replaceSpaceWithUnderscreInVarNameInput(e.target) replaceSpaceWithUnderscreInVarNameInput(e.target)


const newKey = e.target.value const newKey = e.target.value
const { isValid, errorKey, errorMessageKey } = checkKeys([newKey], true)
if (!isValid) {
Toast.notify({
type: 'error',
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
})
return
}


if (list.map(item => item.variable?.trim()).includes(newKey.trim())) {
Toast.notify({
type: 'error',
message: t('appDebug.varKeyError.keyAlreadyExists', { key: newKey }),
})
return
}
toastHandle?.clear?.()
validateVarInput(list.toSpliced(index, 1), newKey)


onVarNameChange?.(list[index].variable, newKey) onVarNameChange?.(list[index].variable, newKey)
const newList = produce(list, (draft) => { const newList = produce(list, (draft) => {
}) })
onChange(newList) onChange(newList)
} }
}, [list, onVarNameChange, onChange])
}, [list, onVarNameChange, onChange, validateVarInput])


const handleVarReferenceChange = useCallback((index: number) => { const handleVarReferenceChange = useCallback((index: number) => {
return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => { return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => {

Loading…
Cancel
Save