mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-24 07:26:47 +08:00
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:
170
web/src/components/list-filter-bar/filter-field.tsx
Normal file
170
web/src/components/list-filter-bar/filter-field.tsx
Normal 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';
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -60,4 +60,5 @@ interface GraphRag {
|
||||
export type IDocumentInfoFilter = {
|
||||
run_status: Record<number, number>;
|
||||
suffix: Record<string, number>;
|
||||
metadata: Record<string, Record<string, number>>;
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user