From 9c023b6d8c7d12862bc45ad34d6d3f5a694b22b1 Mon Sep 17 00:00:00 2001 From: balibabu Date: Thu, 11 Jul 2024 18:22:53 +0800 Subject: [PATCH] feat: validate the name field of the categorize operator for duplicate names and nulls #918 (#1471) ### What problem does this PR solve? feat: validate the name field of the categorize operator for duplicate names and nulls #918 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- web/src/locales/en.ts | 2 + web/src/locales/zh-traditional.ts | 2 + web/src/locales/zh.ts | 2 + .../flow/canvas/node/categorize-node.tsx | 1 - .../categorize-form/dynamic-categorize.tsx | 110 ++++++++++++++++-- web/src/pages/flow/categorize-form/hooks.ts | 2 - web/src/pages/flow/form-hooks.ts | 9 +- 7 files changed, 111 insertions(+), 17 deletions(-) diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index bb646fe69..28527faf0 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -596,6 +596,8 @@ The above is the content you need to summarize.`, blank: 'Blank', createFromNothing: 'Create from nothing', addItem: 'Add Item', + nameRequiredMsg: 'Name is required', + nameRepeatedMsg: 'The name cannot be repeated', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index 29e96250c..e8b56ba7a 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -557,6 +557,8 @@ export default { blank: '空', createFromNothing: '從無到有', addItem: '新增', + nameRequiredMsg: '名稱不能為空', + nameRepeatedMsg: '名稱不能重複', }, footer: { profile: '“保留所有權利 @ react”', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index c61bbcd8e..287c0c0c1 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -575,6 +575,8 @@ export default { blank: '空', createFromNothing: '从无到有', addItem: '新增', + nameRequiredMsg: '名称不能为空', + nameRepeatedMsg: '名称不能重复', }, footer: { profile: 'All rights reserved @ React', diff --git a/web/src/pages/flow/canvas/node/categorize-node.tsx b/web/src/pages/flow/canvas/node/categorize-node.tsx index 324d215e8..228aa97b9 100644 --- a/web/src/pages/flow/canvas/node/categorize-node.tsx +++ b/web/src/pages/flow/canvas/node/categorize-node.tsx @@ -46,7 +46,6 @@ export function CategorizeNode({ id, data, selected }: NodeProps) { ), indexesInUse, ); - console.info('newPositionMap:', newPositionMap); const nextPostionMap = { ...pick(state, intersectionKeys), diff --git a/web/src/pages/flow/categorize-form/dynamic-categorize.tsx b/web/src/pages/flow/categorize-form/dynamic-categorize.tsx index 6b7a41fab..2995f191b 100644 --- a/web/src/pages/flow/categorize-form/dynamic-categorize.tsx +++ b/web/src/pages/flow/categorize-form/dynamic-categorize.tsx @@ -1,16 +1,91 @@ import { useTranslate } from '@/hooks/commonHooks'; import { CloseOutlined } from '@ant-design/icons'; -import { Button, Card, Form, Input, Select } from 'antd'; +import { Button, Card, Form, FormListFieldData, Input, Select } from 'antd'; +import { FormInstance } from 'antd/lib'; import { humanId } from 'human-id'; +import trim from 'lodash/trim'; +import { + ChangeEventHandler, + FocusEventHandler, + useCallback, + useEffect, + useState, +} from 'react'; import { useUpdateNodeInternals } from 'reactflow'; import { Operator } from '../constant'; import { useBuildFormSelectOptions } from '../form-hooks'; -import { ICategorizeItem } from '../interface'; interface IProps { nodeId?: string; } +interface INameInputProps { + value?: string; + onChange?: (value: string) => void; + otherNames?: string[]; + validate(errors: string[]): void; +} + +const getOtherFieldValues = ( + form: FormInstance, + field: FormListFieldData, + latestField: string, +) => + (form.getFieldValue(['items']) ?? []) + .map((x: any) => x[latestField]) + .filter( + (x: string) => + x !== form.getFieldValue(['items', field.name, latestField]), + ); + +const NameInput = ({ + value, + onChange, + otherNames, + validate, +}: INameInputProps) => { + const [name, setName] = useState(); + const { t } = useTranslate('flow'); + + const handleNameChange: ChangeEventHandler = useCallback( + (e) => { + const val = e.target.value; + // trigger validation + if (otherNames?.some((x) => x === val)) { + validate([t('nameRepeatedMsg')]); + } else if (trim(val) === '') { + validate([t('nameRequiredMsg')]); + } else { + validate([]); + } + setName(val); + }, + [otherNames, validate, t], + ); + + const handleNameBlur: FocusEventHandler = useCallback( + (e) => { + const val = e.target.value; + if (otherNames?.every((x) => x !== val) && trim(val) !== '') { + onChange?.(val); + } + }, + [onChange, otherNames], + ); + + useEffect(() => { + setName(value); + }, [value]); + + return ( + + ); +}; + const DynamicCategorize = ({ nodeId }: IProps) => { const updateNodeInternals = useUpdateNodeInternals(); const form = Form.useFormInstance(); @@ -45,11 +120,28 @@ const DynamicCategorize = ({ nodeId }: IProps) => { } > - + + form.setFields([ + { + name: ['items', field.name, 'name'], + errors, + }, + ]) + } + > {