From fd0a1fde6b88108e6b4f96e0e5f096c85fe4ac1e Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Mon, 12 Jan 2026 19:05:33 +0800 Subject: [PATCH] Feat: Enhanced metadata functionality (#12560) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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) --- .../list-filter-bar/filter-field.tsx | 7 +- .../list-filter-bar/filter-popover.tsx | 159 ++++++++++--- web/src/components/list-filter-bar/index.tsx | 1 + .../components/list-filter-bar/interface.ts | 5 +- .../components/metedata/manage-modal.tsx | 214 +++++++++++++----- web/src/pages/dataset/dataset/index.tsx | 1 + .../dataset/dataset/parsing-status-cell.tsx | 13 +- .../dataset/use-dataset-table-columns.tsx | 38 +++- .../dataset/dataset/use-select-filters.ts | 7 +- 9 files changed, 322 insertions(+), 123 deletions(-) diff --git a/web/src/components/list-filter-bar/filter-field.tsx b/web/src/components/list-filter-bar/filter-field.tsx index 8fd4e3bd5..8a66e33d9 100644 --- a/web/src/components/list-filter-bar/filter-field.tsx +++ b/web/src/components/list-filter-bar/filter-field.tsx @@ -80,7 +80,7 @@ const FilterItem = memo( } // className="hidden group-hover:block" /> - handleCheckChange({ checked: !field.value?.includes(item.id.toString()), @@ -88,9 +88,10 @@ const FilterItem = memo( item, }) } + className="truncate w-[200px] text-sm font-normal leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-text-secondary" > {item.label} - + @@ -101,7 +102,7 @@ const FilterItem = memo( ); }, ); - +FilterItem.displayName = 'FilterItem'; export const FilterField = memo( ({ item, diff --git a/web/src/components/list-filter-bar/filter-popover.tsx b/web/src/components/list-filter-bar/filter-popover.tsx index 6b787d171..45657a81d 100644 --- a/web/src/components/list-filter-bar/filter-popover.tsx +++ b/web/src/components/list-filter-bar/filter-popover.tsx @@ -15,11 +15,17 @@ import { useForm } from 'react-hook-form'; import { z, ZodArray, ZodString } from 'zod'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; import { Form, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; import { t } from 'i18next'; import { FilterField } from './filter-field'; -import { FilterChange, FilterCollection, FilterValue } from './interface'; +import { + FilterChange, + FilterCollection, + FilterType, + FilterValue, +} from './interface'; export type CheckboxFormMultipleProps = { filters?: FilterCollection[]; @@ -30,6 +36,41 @@ export type CheckboxFormMultipleProps = { filterGroup?: Record; }; +const filterNestedList = ( + list: FilterType[], + searchTerm: string, +): FilterType[] => { + if (!searchTerm) return list; + + const term = searchTerm.toLowerCase(); + + return list + .filter((item) => { + if ( + item.label.toString().toLowerCase().includes(term) || + item.id.toLowerCase().includes(term) + ) { + return true; + } + + if (item.list && item.list.length > 0) { + const filteredSubList = filterNestedList(item.list, searchTerm); + return filteredSubList.length > 0; + } + + return false; + }) + .map((item) => { + if (item.list && item.list.length > 0) { + return { + ...item, + list: filterNestedList(item.list, searchTerm), + }; + } + return item; + }); +}; + function CheckboxFormMultiple({ filters = [], value, @@ -37,21 +78,22 @@ function CheckboxFormMultiple({ setOpen, filterGroup, }: CheckboxFormMultipleProps) { - const [resolvedFilters, setResolvedFilters] = - useState(filters); + // const [resolvedFilters, setResolvedFilters] = + // useState(filters); + const [searchTerms, setSearchTerms] = useState>({}); - useEffect(() => { - if (filters && filters.length > 0) { - setResolvedFilters(filters); - } - }, [filters]); + // useEffect(() => { + // if (filters && filters.length > 0) { + // setResolvedFilters(filters); + // } + // }, [filters]); const fieldsDict = useMemo(() => { - if (resolvedFilters.length === 0) { + if (filters.length === 0) { return {}; } - return resolvedFilters.reduce>((pre, cur) => { + return filters.reduce>((pre, cur) => { const hasNested = cur.list?.some( (item) => item.list && item.list.length > 0, ); @@ -63,14 +105,14 @@ function CheckboxFormMultiple({ } return pre; }, {}); - }, [resolvedFilters]); + }, [filters]); const FormSchema = useMemo(() => { - if (resolvedFilters.length === 0) { + if (filters.length === 0) { return z.object({}); } return z.object( - resolvedFilters.reduce< + filters.reduce< Record< string, ZodArray | z.ZodObject | z.ZodOptional @@ -90,13 +132,10 @@ function CheckboxFormMultiple({ return pre; }, {}), ); - }, [resolvedFilters]); - // const FormSchema = useMemo(() => { - // return z.object({}); - // }, []); + }, [filters]); const form = useForm>({ - resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined, + resolver: filters.length > 0 ? zodResolver(FormSchema) : undefined, defaultValues: fieldsDict, }); @@ -112,10 +151,10 @@ function CheckboxFormMultiple({ }, [fieldsDict, onChange, setOpen]); useEffect(() => { - if (resolvedFilters.length > 0) { + if (filters.length > 0) { form.reset(value || fieldsDict); } - }, [form, value, resolvedFilters, fieldsDict]); + }, [form, value, filters, fieldsDict]); const filterList = useMemo(() => { const filterSet = filterGroup @@ -131,6 +170,26 @@ function CheckboxFormMultiple({ return filters.filter((x) => !filterList.includes(x.field)); }, [filterList, filters]); + const handleSearchChange = (field: string, value: string) => { + setSearchTerms((prev) => ({ + ...prev, + [field]: value, + })); + }; + + const getFilteredFilters = (originalFilters: FilterCollection[]) => { + return originalFilters.map((filter) => { + if (filter.canSearch && searchTerms[filter.field]) { + const filteredList = filterNestedList( + filter.list, + searchTerms[filter.field], + ); + return { ...filter, list: filteredList }; + } + return filter; + }); + }; + return (
{ const filterKeys = filterGroup[key]; - const thisFilters = filters.filter((x) => + const originalFilters = filters.filter((x) => filterKeys.includes(x.field), ); + const thisFilters = getFilteredFilters(originalFilters); + return (
{key}
{thisFilters.map((x) => ( - +
+ {x.canSearch && ( +
+ + handleSearchChange(x.field, e.target.value) + } + className="h-8" + /> +
+ )} + +
))}
@@ -169,15 +244,29 @@ function CheckboxFormMultiple({ })} {notInfilterGroup && notInfilterGroup.map((x) => { + const filteredItem = getFilteredFilters([x])[0]; + return (
- - {x.label} - +
+ + {x.label} + + {x.canSearch && ( + + handleSearchChange(x.field, e.target.value) + } + className="h-8 w-32 ml-2" + /> + )} +
- {x.list?.length && - x.list.map((item) => { + {!!filteredItem.list?.length && + filteredItem.list.map((item) => { return ( | Record> >; - export type FilterChange = (value: FilterValue) => void; diff --git a/web/src/pages/dataset/components/metedata/manage-modal.tsx b/web/src/pages/dataset/components/metedata/manage-modal.tsx index ddecdd685..790b2f1ea 100644 --- a/web/src/pages/dataset/components/metedata/manage-modal.tsx +++ b/web/src/pages/dataset/components/metedata/manage-modal.tsx @@ -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(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: () => {t('knowledgeDetails.metadata.values')}, + header: () => ( +
+ {t('knowledgeDetails.metadata.values')} +
{ + setExpanded(!expanded); + }} + > + {expanded ? ( + + ) : ( + + )} + {expanded} +
+
+ ), cell: ({ row }) => { const values = row.getValue('values') as Array; + + if (!Array.isArray(values) || values.length === 0) { + return
; + } + + const displayedValues = expanded ? values : values.slice(0, 2); + const hasMore = Array.isArray(values) && values.length > 2; + return ( -
- {Array.isArray(values) && - values.length > 0 && - values - .filter((value: string, index: number) => index < 2) - ?.map((value: string) => { - return ( - - )} -
- - ); - })} - {Array.isArray(values) && values.length > 2 && ( -
...
- )} +
+
+ {displayedValues?.map((value: string) => { + const isEditing = + editingValue && + editingValue.field === row.getValue('field') && + editingValue.value === value; + + return isEditing ? ( +
+ + 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" + /> +
+ ) : ( + + )} +
+ + ); + })} + {hasMore && !expanded && ( +
...
+ )} +
); }, @@ -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; diff --git a/web/src/pages/dataset/dataset/index.tsx b/web/src/pages/dataset/dataset/index.tsx index e023d1696..6853c6c72 100644 --- a/web/src/pages/dataset/dataset/index.tsx +++ b/web/src/pages/dataset/dataset/index.tsx @@ -127,6 +127,7 @@ export default function Dataset() { type: MetadataType.Manage, isCanAdd: false, isEditField: true, + isDeleteSingleValue: true, title: (
diff --git a/web/src/pages/dataset/dataset/parsing-status-cell.tsx b/web/src/pages/dataset/dataset/parsing-status-cell.tsx index d6a9deb8f..9ac7fc82d 100644 --- a/web/src/pages/dataset/dataset/parsing-status-cell.tsx +++ b/web/src/pages/dataset/dataset/parsing-status-cell.tsx @@ -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({ {t('knowledgeDetails.dataPipeline')} - - {t('knowledgeDetails.setMetaData')} -
diff --git a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx index ce877ed1f..c3e1e4aa8 100644 --- a/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx +++ b/web/src/pages/dataset/dataset/use-dataset-table-columns.tsx @@ -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 ( - +
{ 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({
{t('metadata.editMetadataForDataset')} - {row.name} + {row.original.name}
), isDeleteSingleValue: true, - }) - } + }); + }} + > + {length + ' fields'} + + ); + }, + }, + { + accessorKey: 'run', + header: t('Parse'), + // meta: { cellClassName: 'min-w-[20vw]' }, + cell: ({ row }) => { + return ( + ); diff --git a/web/src/pages/dataset/dataset/use-select-filters.ts b/web/src/pages/dataset/dataset/use-select-filters.ts index e5497182c..2c7596769 100644 --- a/web/src/pages/dataset/dataset/use-select-filters.ts +++ b/web/src/pages/dataset/dataset/use-select-filters.ts @@ -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]);