Feat: Enhanced metadata functionality (#12560)

### What problem does this PR solve?

Feat: Enhanced metadata functionality
- Metadata filtering supports searching.
- Values ​​can be directly modified.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2026-01-12 19:05:33 +08:00
committed by GitHub
parent 653001b14f
commit fd0a1fde6b
9 changed files with 322 additions and 123 deletions

View File

@ -5,6 +5,7 @@ import {
import { EmptyType } from '@/components/empty/constant';
import Empty from '@/components/empty/empty';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import {
Table,
@ -25,7 +26,13 @@ import {
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { Plus, Settings, Trash2 } from 'lucide-react';
import {
ListChevronsDownUp,
ListChevronsUpDown,
Plus,
Settings,
Trash2,
} from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from '../../sidebar/hooks';
@ -61,6 +68,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
values: [],
});
const [expanded, setExpanded] = useState(true);
const [currentValueIndex, setCurrentValueIndex] = useState<number>(0);
const [deleteDialogContent, setDeleteDialogContent] = useState({
visible: false,
@ -70,6 +78,11 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
onOk: () => {},
onCancel: () => {},
});
const [editingValue, setEditingValue] = useState<{
field: string;
value: string;
newValue: string;
} | null>(null);
const {
tableData,
@ -81,6 +94,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
addDeleteValue,
} = useManageMetaDataModal(originalTableData, metadataType, otherData);
const { handleMenuClick } = useHandleMenuClick();
const [shouldSave, setShouldSave] = useState(false);
const {
visible: manageValuesVisible,
showModal: showManageValuesModal,
@ -96,6 +110,32 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
onCancel: () => {},
});
};
const handleEditValue = (field: string, value: string) => {
setEditingValue({ field, value, newValue: value });
};
const saveEditedValue = useCallback(() => {
if (editingValue) {
setTableData((prev) => {
return prev.map((row) => {
if (row.field === editingValue.field) {
const updatedValues = row.values.map((v) =>
v === editingValue.value ? editingValue.newValue : v,
);
return { ...row, values: updatedValues };
}
return row;
});
});
setEditingValue(null);
setShouldSave(true);
}
}, [editingValue, setTableData]);
const cancelEditValue = () => {
setEditingValue(null);
};
const handAddValueRow = () => {
setValueData({
field: '',
@ -136,66 +176,119 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
},
{
accessorKey: 'values',
header: () => <span>{t('knowledgeDetails.metadata.values')}</span>,
header: () => (
<div className="flex items-center">
<span>{t('knowledgeDetails.metadata.values')}</span>
<div
className="ml-2 p-1 cursor-pointer"
onClick={() => {
setExpanded(!expanded);
}}
>
{expanded ? (
<ListChevronsDownUp size={14} />
) : (
<ListChevronsUpDown size={14} />
)}
{expanded}
</div>
</div>
),
cell: ({ row }) => {
const values = row.getValue('values') as Array<string>;
if (!Array.isArray(values) || values.length === 0) {
return <div></div>;
}
const displayedValues = expanded ? values : values.slice(0, 2);
const hasMore = Array.isArray(values) && values.length > 2;
return (
<div className="flex items-center gap-1">
{Array.isArray(values) &&
values.length > 0 &&
values
.filter((value: string, index: number) => index < 2)
?.map((value: string) => {
return (
<Button
key={value}
variant={'ghost'}
className="border border-border-button"
aria-label="Edit"
>
<div className="flex gap-1 items-center">
<div className="text-sm truncate max-w-24">
{value}
</div>
{isDeleteSingleValue && (
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={() => {
setDeleteDialogContent({
visible: true,
title:
t('common.delete') +
' ' +
t('knowledgeDetails.metadata.value'),
name: value,
warnText:
MetadataDeleteMap(t)[
metadataType as MetadataType
].warnValueText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleValue(
row.getValue('field'),
value,
);
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
)}
</div>
</Button>
);
})}
{Array.isArray(values) && values.length > 2 && (
<div className="text-text-secondary self-end">...</div>
)}
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">
{displayedValues?.map((value: string) => {
const isEditing =
editingValue &&
editingValue.field === row.getValue('field') &&
editingValue.value === value;
return isEditing ? (
<div key={value}>
<Input
type="text"
value={editingValue.newValue}
onChange={(e) =>
setEditingValue({
...editingValue,
newValue: e.target.value,
})
}
onBlur={saveEditedValue}
onKeyDown={(e) => {
if (e.key === 'Enter') {
saveEditedValue();
} else if (e.key === 'Escape') {
cancelEditValue();
}
}}
autoFocus
// className="text-sm min-w-20 max-w-32 outline-none bg-transparent px-1 py-0.5"
/>
</div>
) : (
<Button
key={value}
variant={'ghost'}
className="border border-border-button"
onClick={() =>
handleEditValue(row.getValue('field'), value)
}
aria-label="Edit"
>
<div className="flex gap-1 items-center">
<div className="text-sm truncate max-w-24">{value}</div>
{isDeleteSingleValue && (
<Button
variant={'delete'}
className="p-0 bg-transparent"
onClick={(e) => {
e.stopPropagation();
setDeleteDialogContent({
visible: true,
title:
t('common.delete') +
' ' +
t('knowledgeDetails.metadata.value'),
name: value,
warnText:
MetadataDeleteMap(t)[
metadataType as MetadataType
].warnValueText,
onOk: () => {
hideDeleteModal();
handleDeleteSingleValue(
row.getValue('field'),
value,
);
},
onCancel: () => {
hideDeleteModal();
},
});
}}
>
<Trash2 />
</Button>
)}
</div>
</Button>
);
})}
{hasMore && !expanded && (
<div className="text-text-secondary self-end">...</div>
)}
</div>
</div>
);
},
@ -260,6 +353,9 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
isDeleteSingleValue,
handleEditValueRow,
metadataType,
expanded,
editingValue,
saveEditedValue,
]);
const table = useReactTable({
@ -271,7 +367,7 @@ export const ManageMetadataModal = (props: IManageModalProps) => {
getFilteredRowModel: getFilteredRowModel(),
manualPagination: true,
});
const [shouldSave, setShouldSave] = useState(false);
const handleSaveValues = (data: IMetaDataTableData) => {
setTableData((prev) => {
let newData;

View File

@ -127,6 +127,7 @@ export default function Dataset() {
type: MetadataType.Manage,
isCanAdd: false,
isEditField: true,
isDeleteSingleValue: true,
title: (
<div className="flex flex-col gap-2">
<div className="text-base font-normal">

View File

@ -21,7 +21,6 @@ import { ParsingCard } from './parsing-card';
import { ReparseDialog } from './reparse-dialog';
import { UseChangeDocumentParserShowType } from './use-change-document-parser';
import { useHandleRunDocumentByIds } from './use-run-document';
import { UseSaveMetaShowType } from './use-save-meta';
import { isParserRunning } from './utils';
const IconMap = {
[RunningStatus.UNSTART]: (
@ -44,13 +43,12 @@ const IconMap = {
export function ParsingStatusCell({
record,
showChangeParserModal,
showSetMetaModal,
// showSetMetaModal,
showLog,
}: {
record: IDocumentInfo;
showLog: (record: IDocumentInfo) => void;
} & UseChangeDocumentParserShowType &
UseSaveMetaShowType) {
} & UseChangeDocumentParserShowType) {
const { t } = useTranslation();
const {
run,
@ -83,10 +81,6 @@ export function ParsingStatusCell({
showChangeParserModal(record);
}, [record, showChangeParserModal]);
const handleShowSetMetaModal = useCallback(() => {
showSetMetaModal(record);
}, [record, showSetMetaModal]);
const showParse = useMemo(() => {
return record.type !== DocumentType.Virtual;
}, [record]);
@ -124,9 +118,6 @@ export function ParsingStatusCell({
<DropdownMenuItem onClick={handleShowChangeParserModal}>
{t('knowledgeDetails.dataPipeline')}
</DropdownMenuItem>
<DropdownMenuItem onClick={handleShowSetMetaModal}>
{t('knowledgeDetails.setMetaData')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>

View File

@ -172,17 +172,18 @@ export function useDatasetTableColumns({
),
},
{
accessorKey: 'run',
header: t('Parse'),
// meta: { cellClassName: 'min-w-[20vw]' },
accessorKey: 'meta_fields',
header: t('metadata.metadata'),
cell: ({ row }) => {
const length = Object.keys(row.getValue('meta_fields') || {}).length;
return (
<ParsingStatusCell
record={row.original}
showChangeParserModal={showChangeParserModal}
showSetMetaModal={(row) =>
<div
className="capitalize cursor-pointer"
onClick={() => {
showManageMetadataModal({
metadata: util.JSONToMetaDataTableData(row.meta_fields || {}),
metadata: util.JSONToMetaDataTableData(
row.original.meta_fields || {},
),
isCanAdd: true,
type: MetadataType.UpdateSingle,
record: row,
@ -193,13 +194,28 @@ export function useDatasetTableColumns({
</div>
<div className="text-sm text-text-secondary w-full truncate">
{t('metadata.editMetadataForDataset')}
{row.name}
{row.original.name}
</div>
</div>
),
isDeleteSingleValue: true,
})
}
});
}}
>
{length + ' fields'}
</div>
);
},
},
{
accessorKey: 'run',
header: t('Parse'),
// meta: { cellClassName: 'min-w-[20vw]' },
cell: ({ row }) => {
return (
<ParsingStatusCell
record={row.original}
showChangeParserModal={showChangeParserModal}
showLog={showLog}
></ParsingStatusCell>
);

View File

@ -72,7 +72,12 @@ export function useSelectDatasetFilters() {
return [
{ field: 'type', label: 'File Type', list: fileTypes },
{ field: 'run', label: 'Status', list: fileStatus },
{ field: 'metadata', label: 'Metadata field', list: metaDataList },
{
field: 'metadata',
label: 'Metadata field',
canSearch: true,
list: metaDataList,
},
] as FilterCollection[];
}, [fileStatus, fileTypes, metaDataList]);