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)
This commit is contained in:
chanx
2025-12-22 17:35:12 +08:00
committed by GitHub
parent 993bf7c2c8
commit 51b12841d6
9 changed files with 310 additions and 68 deletions

View File

@ -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<string, string[]>)[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 (
<div
className={`flex items-center justify-between text-text-primary text-xs ${level > 0 ? 'ml-4' : ''}`}
>
<FormItem className="flex flex-row space-x-3 space-y-0 items-center">
<FormControl>
<Checkbox
checked={field.value?.includes(item.id.toString())}
onCheckedChange={(checked: boolean) =>
handleCheckChange({ checked, field, item })
}
/>
</FormControl>
<FormLabel onClick={(e) => e.stopPropagation()}>
{item.label}
</FormLabel>
</FormItem>
{item.count !== undefined && (
<span className="text-sm">{item.count}</span>
)}
</div>
);
},
);
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 (
<FormField
key={item.id}
control={form.control}
name={parent.field as string}
render={({ field }) => {
if (hasNestedList) {
return (
<div className={`flex flex-col gap-2 ${level > 0 ? 'ml-4' : ''}`}>
<div
className="flex items-center justify-between cursor-pointer"
onClick={() => {
setShowAll(!showAll);
}}
>
<FormLabel className="text-text-primary">
{item.label}
</FormLabel>
{showAll ? (
<ChevronUp size={12} />
) : (
<ChevronDown size={12} />
)}
</div>
{showAll &&
item.list?.map((child) => (
<FilterField
key={child.id}
item={child}
parent={{
...item,
field: `${parent.field}.${item.field}`,
}}
level={level + 1}
/>
// <FilterItem key={child.id} item={child} field={child.field} level={level+1} />
// <div
// className="flex flex-row space-x-3 space-y-0 items-center"
// key={child.id}
// >
// <FormControl>
// <Checkbox
// checked={field.value?.includes(child.id.toString())}
// onCheckedChange={(checked) =>
// handleCheckChange({ checked, field, item: child })
// }
// />
// </FormControl>
// <FormLabel onClick={(e) => e.stopPropagation()}>
// {child.label}
// </FormLabel>
// </div>
))}
</div>
);
}
return <FilterItem item={item} field={field} level={level} />;
}}
/>
);
},
);
FilterField.displayName = 'FilterField';

View File

@ -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<Record<string, Array<any>>>((pre, cur) => {
pre[cur.field] = [];
return pre;
}, {});
const [resolvedFilters, setResolvedFilters] =
useState<FilterCollection[]>(filters);
const FormSchema = z.object(
filters.reduce<Record<string, ZodArray<ZodString, 'many'>>>((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<Record<string, any>>((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<ZodString, 'many'> | z.ZodObject<any> | z.ZodOptional<any>
>
>((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<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
resolver: resolvedFilters.length > 0 ? zodResolver(FormSchema) : undefined,
defaultValues: fieldsDict,
});
function onSubmit(data: z.infer<typeof FormSchema>) {
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 (
<Form {...form}>
@ -85,44 +135,21 @@ function CheckboxFormMultiple({
render={() => (
<FormItem className="space-y-4">
<div>
<FormLabel className="text-base text-text-sub-title-invert">
{x.label}
</FormLabel>
<FormLabel className="text-text-primary">{x.label}</FormLabel>
</div>
{x.list.map((item) => (
<FormField
key={item.id}
control={form.control}
name={x.field}
render={({ field }) => {
return (
<div className="flex items-center justify-between text-text-primary text-xs">
<FormItem
key={item.id}
className="flex flex-row space-x-3 space-y-0 items-center "
>
<FormControl>
<Checkbox
checked={field.value?.includes(item.id)}
onCheckedChange={(checked) => {
return checked
? field.onChange([...field.value, item.id])
: field.onChange(
field.value?.filter(
(value) => value !== item.id,
),
);
}}
/>
</FormControl>
<FormLabel>{item.label}</FormLabel>
</FormItem>
<span className=" text-sm">{item.count}</span>
</div>
);
}}
/>
))}
{x.list.map((item) => {
return (
<FilterField
key={item.id}
item={{ ...item }}
parent={{
...x,
id: x.field,
// field: `${x.field}${item.field ? '.' + item.field : ''}`,
}}
/>
);
})}
<FormMessage />
</FormItem>
)}
@ -137,7 +164,13 @@ function CheckboxFormMultiple({
>
{t('common.clear')}
</Button>
<Button type="submit" size={'sm'}>
<Button
type="submit"
onClick={() => {
console.log(form.formState.errors, form.getValues());
}}
size={'sm'}
>
{t('common.submit')}
</Button>
</div>

View File

@ -17,6 +17,7 @@ interface IProps {
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
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}
</div>
<div className="flex gap-5 items-center">
{preChildren}
{showFilter && (
<FilterPopover
value={value}

View File

@ -1,6 +1,9 @@
export type FilterType = {
id: string;
field?: string;
label: string | JSX.Element;
list?: FilterType[];
value?: string | string[];
count?: number;
};
@ -10,6 +13,9 @@ export type FilterCollection = {
list: FilterType[];
};
export type FilterValue = Record<string, Array<string>>;
export type FilterValue = Record<
string,
Array<string> | Record<string, Array<string>>
>;
export type FilterChange = (value: FilterValue) => void;

View File

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

View File

@ -60,4 +60,5 @@ interface GraphRag {
export type IDocumentInfoFilter = {
run_status: Record<number, number>;
suffix: Record<string, number>;
metadata: Record<string, Record<string, number>>;
};

View File

@ -204,7 +204,8 @@ export const ManageValuesModal = (props: IManageValuesProps) => {
</div>
</div>
)}
{metaData.restrictDefinedValues && (
{((metaData.restrictDefinedValues && isShowValueSwitch) ||
!isShowValueSwitch) && (
<div className="flex flex-col gap-2">
<div className="flex justify-between items-center">
<div>{t('knowledgeDetails.metadata.values')}</div>

View File

@ -216,12 +216,6 @@ const Generate: React.FC<GenerateProps> = (props) => {
? graphRunData
: raptorRunData
) as ITraceInfo;
console.log(
name,
'data',
data,
!data || (!data.progress && data.progress !== 0),
);
return (
<div key={name}>
<MenuItem

View File

@ -25,12 +25,33 @@ export function useSelectDatasetFilters() {
}));
}
}, [filter.run_status, t]);
const metaDataList = useMemo(() => {
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 };
}