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 <kevinhu.sh@gmail.com>
This commit is contained in:
chanx
2025-12-24 09:32:41 +08:00
committed by Jin Hai
parent 8197f9a873
commit 9a5c5c46f2
11 changed files with 322 additions and 100 deletions

View File

@ -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<IMetaDataTableData[]>(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(

View File

@ -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;

View File

@ -54,6 +54,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
values: [],
});
const [currentValueIndex, setCurrentValueIndex] = useState<number>(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();
},
});
}}
>
<Trash2 />
@ -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);
}}
>
<Settings />
@ -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<string, any>();
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<string, IMetaDataTableData>();
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 (
<>
<Modal
@ -357,6 +391,8 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
: t('knowledgeDetails.metadata.editMetadata')}
</div>
}
type={metadataType}
existsKeys={existsKeys}
visible={manageValuesVisible}
hideModal={hideManageValuesModal}
data={valueData}

View File

@ -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<Record<string, string>>({
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<string[]>([...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) => {
<Input
value={metaData.field}
onChange={(e) => {
handleChange('field', e.target?.value || '');
const value = e.target?.value || '';
if (/^[a-zA-Z_]*$/.test(value)) {
handleChange('field', value);
}
}}
/>
<div className="text-state-error text-sm">{valueError.field}</div>
</div>
</div>
)}
@ -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 && (
<EditTag
value={metaData.values}
onChange={(value) => 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);
}
}}
/>
)}
<div className="text-state-error text-sm">{valueError.values}</div>
</div>
)}
{deleteDialogContent.visible && (
<ConfirmDeleteDialog
open={deleteDialogContent.visible}
onCancel={deleteDialogContent.onCancel}
onOk={deleteDialogContent.onOk}
title={deleteDialogContent.title}
content={{
node: (
<ConfirmDeleteDialogNode
name={deleteDialogContent.name}
warnText={deleteDialogContent.warnText}
/>
),
}}
/>
)}
</div>
</Modal>
);