From d72debf0db1e4e7902eebbac7b786a94da193146 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Wed, 24 Dec 2025 09:32:41 +0800 Subject: [PATCH] Fix: Add prompts when merging or deleting metadata. (#12138) ### What problem does this PR solve? Fix: Add prompts when merging or deleting metadata. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --------- Co-authored-by: Kevin Hu --- web/src/components/dynamic-form.tsx | 41 ++++- .../list-filter-bar/filter-field.tsx | 70 ++++---- .../list-filter-bar/filter-popover.tsx | 33 ++-- web/src/locales/en.ts | 6 + web/src/locales/zh.ts | 4 + .../pages/dataset/components/metedata/hook.ts | 14 +- .../dataset/components/metedata/interface.ts | 2 + .../components/metedata/manage-modal.tsx | 82 ++++++--- .../metedata/manage-values-modal.tsx | 164 ++++++++++++++++-- .../datasets/dataset-creating-dialog.tsx | 4 +- web/src/services/knowledge-service.ts | 2 +- 11 files changed, 322 insertions(+), 100 deletions(-) diff --git a/web/src/components/dynamic-form.tsx b/web/src/components/dynamic-form.tsx index 163632fc9..5aea77273 100644 --- a/web/src/components/dynamic-form.tsx +++ b/web/src/components/dynamic-form.tsx @@ -35,6 +35,7 @@ import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { Loader } from 'lucide-react'; import { MultiSelect, MultiSelectOptionType } from './ui/multi-select'; +import { Switch } from './ui/switch'; // Field type enumeration export enum FormFieldType { @@ -46,6 +47,7 @@ export enum FormFieldType { Select = 'select', MultiSelect = 'multi-select', Checkbox = 'checkbox', + Switch = 'switch', Tag = 'tag', Custom = 'custom', } @@ -154,6 +156,7 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { } break; case FormFieldType.Checkbox: + case FormFieldType.Switch: fieldSchema = z.boolean(); break; case FormFieldType.Tag: @@ -193,6 +196,8 @@ export const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { if ( field.type !== FormFieldType.Number && field.type !== FormFieldType.Checkbox && + field.type !== FormFieldType.Switch && + field.type !== FormFieldType.Custom && field.type !== FormFieldType.Tag && field.required ) { @@ -289,7 +294,10 @@ const generateDefaultValues = ( const lastKey = keys[keys.length - 1]; if (field.defaultValue !== undefined) { current[lastKey] = field.defaultValue; - } else if (field.type === FormFieldType.Checkbox) { + } else if ( + field.type === FormFieldType.Checkbox || + field.type === FormFieldType.Switch + ) { current[lastKey] = false; } else if (field.type === FormFieldType.Tag) { current[lastKey] = []; @@ -299,7 +307,10 @@ const generateDefaultValues = ( } else { if (field.defaultValue !== undefined) { defaultValues[field.name] = field.defaultValue; - } else if (field.type === FormFieldType.Checkbox) { + } else if ( + field.type === FormFieldType.Checkbox || + field.type === FormFieldType.Switch + ) { defaultValues[field.name] = false; } else if ( field.type === FormFieldType.Tag || @@ -502,6 +513,32 @@ export const RenderField = ({ )} /> ); + case FormFieldType.Switch: + return ( + + {(fieldProps) => { + const finalFieldProps = field.onChange + ? { + ...fieldProps, + onChange: (checked: boolean) => { + fieldProps.onChange(checked); + field.onChange?.(checked); + }, + } + : fieldProps; + return ( + finalFieldProps.onChange(checked)} + disabled={field.disabled} + /> + ); + }} + + ); case FormFieldType.Tag: return ( diff --git a/web/src/components/list-filter-bar/filter-field.tsx b/web/src/components/list-filter-bar/filter-field.tsx index 134e6f3ef..8fd4e3bd5 100644 --- a/web/src/components/list-filter-bar/filter-field.tsx +++ b/web/src/components/list-filter-bar/filter-field.tsx @@ -31,14 +31,16 @@ const handleCheckChange = ({ (value: string) => value !== item.id.toString(), ); - const newValue = { - ...currentValue, - [parentId]: newParentValues, - }; + const newValue = newParentValues?.length + ? { + ...currentValue, + [parentId]: newParentValues, + } + : { ...currentValue }; - if (newValue[parentId].length === 0) { - delete newValue[parentId]; - } + // if (newValue[parentId].length === 0) { + // delete newValue[parentId]; + // } return field.onChange(newValue); } else { @@ -66,20 +68,31 @@ const FilterItem = memo( }) => { return (
0 ? 'ml-4' : ''}`} + className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-1' : ''}`} > - + - - handleCheckChange({ checked, field, item }) - } - /> +
+ + handleCheckChange({ checked, field, item }) + } + // className="hidden group-hover:block" + /> + + handleCheckChange({ + checked: !field.value?.includes(item.id.toString()), + field, + item, + }) + } + > + {item.label} + +
- e.stopPropagation()}> - {item.label} -
{item.count !== undefined && ( {item.count} @@ -107,11 +120,11 @@ export const FilterField = memo( { if (hasNestedList) { return ( -
0 ? 'ml-4' : ''}`}> +
0 ? 'ml-1' : ''}`}>
{ @@ -138,23 +151,6 @@ export const FilterField = memo( }} level={level + 1} /> - // - //
- // - // - // handleCheckChange({ checked, field, item: child }) - // } - // /> - // - // e.stopPropagation()}> - // {child.label} - // - //
))}
); diff --git a/web/src/components/list-filter-bar/filter-popover.tsx b/web/src/components/list-filter-bar/filter-popover.tsx index ba4628193..c93fae09e 100644 --- a/web/src/components/list-filter-bar/filter-popover.tsx +++ b/web/src/components/list-filter-bar/filter-popover.tsx @@ -11,7 +11,7 @@ import { useMemo, useState, } from 'react'; -import { useForm } from 'react-hook-form'; +import { FieldPath, useForm } from 'react-hook-form'; import { ZodArray, ZodString, z } from 'zod'; import { Button } from '@/components/ui/button'; @@ -178,7 +178,9 @@ function CheckboxFormMultiple({ > + } render={() => (
@@ -186,19 +188,20 @@ function CheckboxFormMultiple({ {x.label}
- {x.list.map((item) => { - return ( - - ); - })} + {x.list?.length && + x.list.map((item) => { + return ( + + ); + })}
)} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index bdc4e600c..6ef6d2a72 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -176,6 +176,11 @@ Procedural Memory: Learned skills, habits, and automated procedures.`, }, knowledgeDetails: { metadata: { + valueExists: + 'Value already exists. Confirm to merge duplicates and combine all associated files.', + fieldNameExists: + 'Field name already exists. Confirm to merge duplicates and combine all associated files.', + fieldExists: 'Field already exists.', fieldSetting: 'Field settings', changesAffectNewParses: 'Changes affect new parses only.', editMetadataForDataset: 'View and edit metadata for ', @@ -190,6 +195,7 @@ Procedural Memory: Learned skills, habits, and automated procedures.`, description: 'Description', fieldName: 'Field name', editMetadata: 'Edit metadata', + deleteWarn: 'This {{field}} will be removed from all associated files', }, metadataField: 'Metadata field', systemAttribute: 'System attribute', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index 66b4e57a4..4225a5aa8 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -182,6 +182,10 @@ export default { description: '描述', fieldName: '字段名', editMetadata: '编辑元数据', + valueExists: '值已存在。确认合并重复项并组合所有关联文件。', + fieldNameExists: '字段名已存在。确认合并重复项并组合所有关联文件。', + fieldExists: '字段名已存在。', + deleteWarn: '此 {{field}} 将从所有关联文件中移除', }, localUpload: '本地上传', fileSize: '文件大小', diff --git a/web/src/pages/dataset/components/metedata/hook.ts b/web/src/pages/dataset/components/metedata/hook.ts index 3bef9fed9..38aae43c9 100644 --- a/web/src/pages/dataset/components/metedata/hook.ts +++ b/web/src/pages/dataset/components/metedata/hook.ts @@ -1,11 +1,14 @@ import message from '@/components/ui/message'; import { useSetModalState } from '@/hooks/common-hooks'; -import { useSetDocumentMeta } from '@/hooks/use-document-request'; +import { + DocumentApiAction, + useSetDocumentMeta, +} from '@/hooks/use-document-request'; import kbService, { getMetaDataService, updateMetaData, } from '@/services/knowledge-service'; -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'umi'; @@ -191,7 +194,7 @@ export const useManageMetaDataModal = ( const { data, loading } = useFetchMetaDataManageData(type); const [tableData, setTableData] = useState(metaData); - + const queryClient = useQueryClient(); const { operations, addDeleteRow, addDeleteValue, addUpdateValue } = useMetadataOperations(); @@ -259,11 +262,14 @@ export const useManageMetaDataModal = ( data: operations, }); if (res.code === 0) { + queryClient.invalidateQueries({ + queryKey: [DocumentApiAction.FetchDocumentList], + }); message.success(t('message.operated')); callback(); } }, - [operations, id, t], + [operations, id, t, queryClient], ); const handleSaveUpdateSingle = useCallback( diff --git a/web/src/pages/dataset/components/metedata/interface.ts b/web/src/pages/dataset/components/metedata/interface.ts index 5ba08f548..2cd25b612 100644 --- a/web/src/pages/dataset/components/metedata/interface.ts +++ b/web/src/pages/dataset/components/metedata/interface.ts @@ -39,6 +39,7 @@ export type IManageModalProps = { export interface IManageValuesProps { title: ReactNode; + existsKeys: string[]; visible: boolean; isEditField?: boolean; isAddValue?: boolean; @@ -46,6 +47,7 @@ export interface IManageValuesProps { isShowValueSwitch?: boolean; isVerticalShowValue?: boolean; data: IMetaDataTableData; + type: MetadataType; hideModal: () => void; onSave: (data: IMetaDataTableData) => void; addUpdateValue: (key: string, value: string | string[]) => void; diff --git a/web/src/pages/dataset/components/metedata/manage-modal.tsx b/web/src/pages/dataset/components/metedata/manage-modal.tsx index 8bb15841e..0df879239 100644 --- a/web/src/pages/dataset/components/metedata/manage-modal.tsx +++ b/web/src/pages/dataset/components/metedata/manage-modal.tsx @@ -54,6 +54,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => { values: [], }); + const [currentValueIndex, setCurrentValueIndex] = useState(0); const [deleteDialogContent, setDeleteDialogContent] = useState({ visible: false, title: '', @@ -94,12 +95,12 @@ export const ManageMetadataModal = (props: IManageModalProps) => { description: '', values: [], }); - // setCurrentValueIndex(tableData.length || 0); + setCurrentValueIndex(tableData.length || 0); showManageValuesModal(); }; const handleEditValueRow = useCallback( - (data: IMetaDataTableData) => { - // setCurrentValueIndex(index); + (data: IMetaDataTableData, index: number) => { + setCurrentValueIndex(index); setValueData(data); showManageValuesModal(); }, @@ -153,10 +154,33 @@ export const ManageMetadataModal = (props: IManageModalProps) => { variant={'delete'} className="p-0 bg-transparent" onClick={() => { - handleDeleteSingleValue( - row.getValue('field'), - value, - ); + setDeleteDialogContent({ + visible: true, + title: + t('common.delete') + + ' ' + + t('knowledgeDetails.metadata.metadata'), + name: row.getValue('field') + '/' + value, + warnText: t( + 'knowledgeDetails.metadata.deleteWarn', + { + field: + t('knowledgeDetails.metadata.field') + + '/' + + t('knowledgeDetails.metadata.values'), + }, + ), + onOk: () => { + hideDeleteModal(); + handleDeleteSingleValue( + row.getValue('field'), + value, + ); + }, + onCancel: () => { + hideDeleteModal(); + }, + }); }} > @@ -185,7 +209,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => { variant={'ghost'} className="bg-transparent px-1 py-0" onClick={() => { - handleEditValueRow(row.original); + handleEditValueRow(row.original, row.index); }} > @@ -201,7 +225,9 @@ export const ManageMetadataModal = (props: IManageModalProps) => { ' ' + t('knowledgeDetails.metadata.metadata'), name: row.getValue('field'), - warnText: t('knowledgeDetails.metadata.deleteWarn'), + warnText: t('knowledgeDetails.metadata.deleteWarn', { + field: t('knowledgeDetails.metadata.field'), + }), onOk: () => { hideDeleteModal(); handleDeleteSingleRow(row.getValue('field')); @@ -243,12 +269,26 @@ export const ManageMetadataModal = (props: IManageModalProps) => { const handleSaveValues = (data: IMetaDataTableData) => { setTableData((prev) => { - //If the keys are the same, they need to be merged. - const fieldMap = new Map(); + let newData; + if (currentValueIndex >= prev.length) { + // Add operation + newData = [...prev, data]; + } else { + // Edit operation + newData = prev.map((item, index) => { + if (index === currentValueIndex) { + return data; + } + return item; + }); + } - prev.forEach((item) => { + // Deduplicate by field and merge values + const fieldMap = new Map(); + newData.forEach((item) => { if (fieldMap.has(item.field)) { - const existingItem = fieldMap.get(item.field); + // Merge values if field exists + const existingItem = fieldMap.get(item.field)!; const mergedValues = [ ...new Set([...existingItem.values, ...item.values]), ]; @@ -258,20 +298,14 @@ export const ManageMetadataModal = (props: IManageModalProps) => { } }); - if (fieldMap.has(data.field)) { - const existingItem = fieldMap.get(data.field); - const mergedValues = [ - ...new Set([...existingItem.values, ...data.values]), - ]; - fieldMap.set(data.field, { ...existingItem, values: mergedValues }); - } else { - fieldMap.set(data.field, data); - } - return Array.from(fieldMap.values()); }); }; + const existsKeys = useMemo(() => { + return tableData.map((item) => item.field); + }, [tableData]); + return ( <> { : t('knowledgeDetails.metadata.editMetadata')}
} + type={metadataType} + existsKeys={existsKeys} visible={manageValuesVisible} hideModal={hideManageValuesModal} data={valueData} diff --git a/web/src/pages/dataset/components/metedata/manage-values-modal.tsx b/web/src/pages/dataset/components/metedata/manage-values-modal.tsx index d0f4eed27..510ebbbc9 100644 --- a/web/src/pages/dataset/components/metedata/manage-values-modal.tsx +++ b/web/src/pages/dataset/components/metedata/manage-values-modal.tsx @@ -1,3 +1,7 @@ +import { + ConfirmDeleteDialog, + ConfirmDeleteDialogNode, +} from '@/components/confirm-delete-dialog'; import EditTag from '@/components/edit-tag'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -7,6 +11,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Plus, Trash2 } from 'lucide-react'; import { memo, useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { MetadataType } from './hook'; import { IManageValuesProps, IMetaDataTableData } from './interface'; // Create a separate input component, wrapped with memo to avoid unnecessary re-renders @@ -63,17 +68,62 @@ export const ManageValuesModal = (props: IManageValuesProps) => { onSave, addUpdateValue, addDeleteValue, + existsKeys, + type, } = props; const [metaData, setMetaData] = useState(data); const { t } = useTranslation(); + const [valueError, setValueError] = useState>({ + field: '', + values: '', + }); + const [deleteDialogContent, setDeleteDialogContent] = useState({ + visible: false, + title: '', + name: '', + warnText: '', + onOk: () => {}, + onCancel: () => {}, + }); + const hideDeleteModal = () => { + setDeleteDialogContent({ + visible: false, + title: '', + name: '', + warnText: '', + onOk: () => {}, + onCancel: () => {}, + }); + }; // Use functional update to avoid closure issues - const handleChange = useCallback((field: string, value: any) => { - setMetaData((prev) => ({ - ...prev, - [field]: value, - })); - }, []); + const handleChange = useCallback( + (field: string, value: any) => { + if (field === 'field' && existsKeys.includes(value)) { + setValueError((prev) => { + return { + ...prev, + field: + type === MetadataType.Setting + ? t('knowledgeDetails.metadata.fieldExists') + : t('knowledgeDetails.metadata.fieldNameExists'), + }; + }); + } else if (field === 'field' && !existsKeys.includes(value)) { + setValueError((prev) => { + return { + ...prev, + field: '', + }; + }); + } + setMetaData((prev) => ({ + ...prev, + [field]: value, + })); + }, + [existsKeys, type, t], + ); // Maintain separate state for each input box const [tempValues, setTempValues] = useState([...data.values]); @@ -89,6 +139,9 @@ export const ManageValuesModal = (props: IManageValuesProps) => { }, [hideModal]); const handleSave = useCallback(() => { + if (type === MetadataType.Setting && valueError.field) { + return; + } if (!metaData.restrictDefinedValues && isShowValueSwitch) { const newMetaData = { ...metaData, values: [] }; onSave(newMetaData); @@ -96,17 +149,35 @@ export const ManageValuesModal = (props: IManageValuesProps) => { onSave(metaData); } handleHideModal(); - }, [metaData, onSave, handleHideModal, isShowValueSwitch]); + }, [metaData, onSave, handleHideModal, isShowValueSwitch, type, valueError]); // Handle value changes, only update temporary state - const handleValueChange = useCallback((index: number, value: string) => { - setTempValues((prev) => { - const newValues = [...prev]; - newValues[index] = value; + const handleValueChange = useCallback( + (index: number, value: string) => { + setTempValues((prev) => { + if (prev.includes(value)) { + setValueError((prev) => { + return { + ...prev, + values: t('knowledgeDetails.metadata.valueExists'), + }; + }); + } else { + setValueError((prev) => { + return { + ...prev, + values: '', + }; + }); + } + const newValues = [...prev]; + newValues[index] = value; - return newValues; - }); - }, []); + return newValues; + }); + }, + [t], + ); // Handle blur event, synchronize to main state const handleValueBlur = useCallback(() => { @@ -137,6 +208,27 @@ export const ManageValuesModal = (props: IManageValuesProps) => { [addDeleteValue, metaData], ); + const showDeleteModal = (item: string, callback: () => void) => { + setDeleteDialogContent({ + visible: true, + title: t('common.delete') + ' ' + t('knowledgeDetails.metadata.metadata'), + name: metaData.field + '/' + item, + warnText: t('knowledgeDetails.metadata.deleteWarn', { + field: + t('knowledgeDetails.metadata.field') + + '/' + + t('knowledgeDetails.metadata.values'), + }), + onOk: () => { + hideDeleteModal(); + callback(); + }, + onCancel: () => { + hideDeleteModal(); + }, + }); + }; + // Handle adding new value const handleAddValue = useCallback(() => { setTempValues((prev) => [...new Set([...prev, ''])]); @@ -172,9 +264,13 @@ export const ManageValuesModal = (props: IManageValuesProps) => { { - handleChange('field', e.target?.value || ''); + const value = e.target?.value || ''; + if (/^[a-zA-Z_]*$/.test(value)) { + handleChange('field', value); + } }} /> +
{valueError.field}
)} @@ -230,7 +326,11 @@ export const ManageValuesModal = (props: IManageValuesProps) => { item={item} index={index} onValueChange={handleValueChange} - onDelete={handleDelete} + onDelete={(idx: number) => { + showDeleteModal(item, () => { + handleDelete(idx); + }); + }} onBlur={handleValueBlur} /> ); @@ -240,11 +340,41 @@ export const ManageValuesModal = (props: IManageValuesProps) => { {!isVerticalShowValue && ( handleChange('values', value)} + onChange={(value) => { + // find deleted value + const item = metaData.values.find( + (item) => !value.includes(item), + ); + if (item) { + showDeleteModal(item, () => { + // handleDelete(idx); + handleChange('values', value); + }); + } else { + handleChange('values', value); + } + }} /> )} +
{valueError.values}
)} + {deleteDialogContent.visible && ( + + ), + }} + /> + )} ); diff --git a/web/src/pages/datasets/dataset-creating-dialog.tsx b/web/src/pages/datasets/dataset-creating-dialog.tsx index f8e5ed0a9..75202838d 100644 --- a/web/src/pages/datasets/dataset-creating-dialog.tsx +++ b/web/src/pages/datasets/dataset-creating-dialog.tsx @@ -17,6 +17,7 @@ import { } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { FormLayout } from '@/constants/form'; +import { useFetchTenantInfo } from '@/hooks/use-user-setting-request'; import { IModalProps } from '@/interfaces/common'; import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect } from 'react'; @@ -33,6 +34,7 @@ const FormId = 'dataset-creating-form'; export function InputForm({ onOk }: IModalProps) { const { t } = useTranslation(); + const { data: tenantInfo } = useFetchTenantInfo(); const FormSchema = z .object({ @@ -80,7 +82,7 @@ export function InputForm({ onOk }: IModalProps) { name: '', parseType: 1, parser_id: '', - embd_id: '', + embd_id: tenantInfo?.embd_id, }, }); diff --git a/web/src/services/knowledge-service.ts b/web/src/services/knowledge-service.ts index 265d86983..169da2cd9 100644 --- a/web/src/services/knowledge-service.ts +++ b/web/src/services/knowledge-service.ts @@ -263,7 +263,7 @@ export const documentFilter = (kb_id: string) => export const getMetaDataService = ({ kb_id }: { kb_id: string }) => request.post(api.getMetaData, { data: { kb_id } }); export const updateMetaData = ({ kb_id, data }: { kb_id: string; data: any }) => - request.post(api.updateMetaData, { data: { kb_id, data } }); + request.post(api.updateMetaData, { data: { kb_id, ...data } }); export const listDataPipelineLogDocument = ( params?: IFetchKnowledgeListRequestParams,