From 51b12841d62a1db6e60f88a382ca32d361d75380 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Mon, 22 Dec 2025 17:35:12 +0800 Subject: [PATCH] Feature/1217 (#12087) ### What problem does this PR solve? feature: Complete metadata functionality ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- .../list-filter-bar/filter-field.tsx | 170 ++++++++++++++++++ .../list-filter-bar/filter-popover.tsx | 149 +++++++++------ web/src/components/list-filter-bar/index.tsx | 16 +- .../components/list-filter-bar/interface.ts | 8 +- web/src/hooks/use-document-request.ts | 2 + web/src/interfaces/database/document.ts | 1 + .../metedata/manage-values-modal.tsx | 3 +- .../dataset/generate-button/generate.tsx | 6 - .../dataset/dataset/use-select-filters.ts | 23 ++- 9 files changed, 310 insertions(+), 68 deletions(-) create mode 100644 web/src/components/list-filter-bar/filter-field.tsx diff --git a/web/src/components/list-filter-bar/filter-field.tsx b/web/src/components/list-filter-bar/filter-field.tsx new file mode 100644 index 000000000..087d0c064 --- /dev/null +++ b/web/src/components/list-filter-bar/filter-field.tsx @@ -0,0 +1,170 @@ +import { Checkbox } from '@/components/ui/checkbox'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { memo, useState } from 'react'; +import { ControllerRenderProps, useFormContext } from 'react-hook-form'; +import { FormControl, FormField, FormItem, FormLabel } from '../ui/form'; +import { FilterType } from './interface'; +const handleCheckChange = ({ + checked, + field, + item, + isNestedField = false, + parentId = '', +}: { + checked: boolean; + field: ControllerRenderProps< + { [x: string]: { [x: string]: any } | string[] }, + string + >; + item: FilterType; + isNestedField?: boolean; + parentId?: string; +}) => { + if (isNestedField && parentId) { + const currentValue = field.value || {}; + const currentParentValues = + (currentValue as Record)[parentId] || []; + + const newParentValues = checked + ? [...currentParentValues, item.id.toString()] + : currentParentValues.filter( + (value: string) => value !== item.id.toString(), + ); + + const newValue = { + ...currentValue, + [parentId]: newParentValues, + }; + + if (newValue[parentId].length === 0) { + delete newValue[parentId]; + } + + return field.onChange(newValue); + } else { + const list = checked + ? [...(Array.isArray(field.value) ? field.value : []), item.id.toString()] + : (Array.isArray(field.value) ? field.value : []).filter( + (value) => value !== item.id.toString(), + ); + return field.onChange(list); + } +}; + +const FilterItem = memo( + ({ + item, + field, + level = 0, + }: { + item: FilterType; + field: ControllerRenderProps< + { [x: string]: { [x: string]: any } | string[] }, + string + >; + level: number; + }) => { + return ( +
0 ? 'ml-4' : ''}`} + > + + + + handleCheckChange({ checked, field, item }) + } + /> + + e.stopPropagation()}> + {item.label} + + + {item.count !== undefined && ( + {item.count} + )} +
+ ); + }, +); + +export const FilterField = memo( + ({ + item, + parent, + level = 0, + }: { + item: FilterType; + parent: FilterType; + level?: number; + }) => { + const form = useFormContext(); + const [showAll, setShowAll] = useState(false); + const hasNestedList = item.list && item.list.length > 0; + + return ( + { + if (hasNestedList) { + return ( +
0 ? 'ml-4' : ''}`}> +
{ + setShowAll(!showAll); + }} + > + + {item.label} + + {showAll ? ( + + ) : ( + + )} +
+ {showAll && + item.list?.map((child) => ( + + // + //
+ // + // + // handleCheckChange({ checked, field, item: child }) + // } + // /> + // + // e.stopPropagation()}> + // {child.label} + // + //
+ ))} +
+ ); + } + + return ; + }} + /> + ); + }, +); + +FilterField.displayName = 'FilterField'; diff --git a/web/src/components/list-filter-bar/filter-popover.tsx b/web/src/components/list-filter-bar/filter-popover.tsx index 2d8450d86..f4c05bfd2 100644 --- a/web/src/components/list-filter-bar/filter-popover.tsx +++ b/web/src/components/list-filter-bar/filter-popover.tsx @@ -4,21 +4,27 @@ import { PopoverTrigger, } from '@/components/ui/popover'; import { zodResolver } from '@hookform/resolvers/zod'; -import { PropsWithChildren, useCallback, useEffect, useState } from 'react'; +import { + PropsWithChildren, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { useForm } from 'react-hook-form'; import { ZodArray, ZodString, z } from 'zod'; import { Button } from '@/components/ui/button'; -import { Checkbox } from '@/components/ui/checkbox'; + import { Form, - FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { t } from 'i18next'; +import { FilterField } from './filter-field'; import { FilterChange, FilterCollection, FilterValue } from './interface'; export type CheckboxFormMultipleProps = { @@ -35,29 +41,71 @@ function CheckboxFormMultiple({ onChange, setOpen, }: CheckboxFormMultipleProps) { - const fieldsDict = filters?.reduce>>((pre, cur) => { - pre[cur.field] = []; - return pre; - }, {}); + const [resolvedFilters, setResolvedFilters] = + useState(filters); - const FormSchema = z.object( - filters.reduce>>((pre, cur) => { - pre[cur.field] = z.array(z.string()); + useEffect(() => { + if (filters && filters.length > 0) { + setResolvedFilters(filters); + } + }, [filters]); - // .refine((value) => value.some((item) => item), { - // message: 'You have to select at least one item.', - // }); + const fieldsDict = useMemo(() => { + if (resolvedFilters.length === 0) { + return {}; + } + + return resolvedFilters.reduce>((pre, cur) => { + const hasNested = cur.list?.some( + (item) => item.list && item.list.length > 0, + ); + + if (hasNested) { + pre[cur.field] = {}; + } else { + pre[cur.field] = []; + } return pre; - }, {}), - ); + }, {}); + }, [resolvedFilters]); + + const FormSchema = useMemo(() => { + if (resolvedFilters.length === 0) { + return z.object({}); + } + + return z.object( + resolvedFilters.reduce< + Record< + string, + ZodArray | z.ZodObject | z.ZodOptional + > + >((pre, cur) => { + const hasNested = cur.list?.some( + (item) => item.list && item.list.length > 0, + ); + + if (hasNested) { + pre[cur.field] = z + .record(z.string(), z.array(z.string().optional()).optional()) + .optional(); + } else { + pre[cur.field] = z.array(z.string().optional()).optional(); + } + + return pre; + }, {}), + ); + }, [resolvedFilters]); const form = useForm>({ - resolver: zodResolver(FormSchema), + resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined, defaultValues: fieldsDict, }); - function onSubmit(data: z.infer) { - onChange?.(data); + function onSubmit() { + const formValues = form.getValues(); + onChange?.({ ...formValues }); setOpen(false); } @@ -67,8 +115,10 @@ function CheckboxFormMultiple({ }, [fieldsDict, onChange, setOpen]); useEffect(() => { - form.reset(value); - }, [form, value]); + if (resolvedFilters.length > 0) { + form.reset(value || fieldsDict); + } + }, [form, value, resolvedFilters, fieldsDict]); return (
@@ -85,44 +135,21 @@ function CheckboxFormMultiple({ render={() => (
- - {x.label} - + {x.label}
- {x.list.map((item) => ( - { - return ( -
- - - { - return checked - ? field.onChange([...field.value, item.id]) - : field.onChange( - field.value?.filter( - (value) => value !== item.id, - ), - ); - }} - /> - - {item.label} - - {item.count} -
- ); - }} - /> - ))} + {x.list.map((item) => { + return ( + + ); + })}
)} @@ -137,7 +164,13 @@ function CheckboxFormMultiple({ > {t('common.clear')} - diff --git a/web/src/components/list-filter-bar/index.tsx b/web/src/components/list-filter-bar/index.tsx index 856a9997c..2f47d9c95 100644 --- a/web/src/components/list-filter-bar/index.tsx +++ b/web/src/components/list-filter-bar/index.tsx @@ -17,6 +17,7 @@ interface IProps { onSearchChange?: ChangeEventHandler; showFilter?: boolean; leftPanel?: ReactNode; + preChildren?: ReactNode; } export const FilterButton = React.forwardRef< @@ -46,6 +47,7 @@ export const FilterButton = React.forwardRef< export default function ListFilterBar({ title, children, + preChildren, searchString, onSearchChange, showFilter = true, @@ -63,7 +65,18 @@ export default function ListFilterBar({ const filterCount = useMemo(() => { return typeof value === 'object' && value !== null ? Object.values(value).reduce((pre, cur) => { - return pre + cur.length; + if (Array.isArray(cur)) { + return pre + cur.length; + } + if (typeof cur === 'object') { + return ( + pre + + Object.values(cur).reduce((pre, cur) => { + return pre + cur.length; + }, 0) + ); + } + return pre; }, 0) : 0; }, [value]); @@ -80,6 +93,7 @@ export default function ListFilterBar({ {leftPanel || title}
+ {preChildren} {showFilter && ( >; +export type FilterValue = Record< + string, + Array | Record> +>; export type FilterChange = (value: FilterValue) => void; diff --git a/web/src/hooks/use-document-request.ts b/web/src/hooks/use-document-request.ts index 805956eda..ba2dc5dfb 100644 --- a/web/src/hooks/use-document-request.ts +++ b/web/src/hooks/use-document-request.ts @@ -124,6 +124,7 @@ export const useFetchDocumentList = () => { { suffix: filterValue.type, run_status: filterValue.run, + metadata: filterValue.metadata, }, ); if (ret.data.code === 0) { @@ -196,6 +197,7 @@ export const useGetDocumentFilter = (): { filter: data?.filter || { run_status: {}, suffix: {}, + metadata: {}, }, onOpenChange: handleOnpenChange, }; diff --git a/web/src/interfaces/database/document.ts b/web/src/interfaces/database/document.ts index 91450e65a..7eab0672e 100644 --- a/web/src/interfaces/database/document.ts +++ b/web/src/interfaces/database/document.ts @@ -60,4 +60,5 @@ interface GraphRag { export type IDocumentInfoFilter = { run_status: Record; suffix: Record; + metadata: Record>; }; 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 d10a22c0f..1cee907f6 100644 --- a/web/src/pages/dataset/components/metedata/manage-values-modal.tsx +++ b/web/src/pages/dataset/components/metedata/manage-values-modal.tsx @@ -204,7 +204,8 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
)} - {metaData.restrictDefinedValues && ( + {((metaData.restrictDefinedValues && isShowValueSwitch) || + !isShowValueSwitch) && (
{t('knowledgeDetails.metadata.values')}
diff --git a/web/src/pages/dataset/dataset/generate-button/generate.tsx b/web/src/pages/dataset/dataset/generate-button/generate.tsx index 489f81700..c4935a901 100644 --- a/web/src/pages/dataset/dataset/generate-button/generate.tsx +++ b/web/src/pages/dataset/dataset/generate-button/generate.tsx @@ -216,12 +216,6 @@ const Generate: React.FC = (props) => { ? graphRunData : raptorRunData ) as ITraceInfo; - console.log( - name, - 'data', - data, - !data || (!data.progress && data.progress !== 0), - ); return (
{ + if (filter.metadata) { + return Object.keys(filter.metadata).map((x) => ({ + id: x.toString(), + field: x.toString(), + label: x.toString(), + list: Object.keys(filter.metadata[x]).map((y) => ({ + id: y.toString(), + field: y.toString(), + label: y.toString(), + value: [y], + count: filter.metadata[x][y], + })), + count: Object.keys(filter.metadata[x]).reduce( + (acc, cur) => acc + filter.metadata[x][cur], + 0, + ), + })); + } + }, [filter.metadata]); const filters: FilterCollection[] = useMemo(() => { return [ { field: 'type', label: 'File Type', list: fileTypes }, { field: 'run', label: 'Status', list: fileStatus }, + { field: 'metadata', label: 'metadata', list: metaDataList }, ] as FilterCollection[]; - }, [fileStatus, fileTypes]); + }, [fileStatus, fileTypes, metaDataList]); return { filters, onOpenChange }; }