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

@ -80,7 +80,7 @@ const FilterItem = memo(
}
// className="hidden group-hover:block"
/>
<FormLabel
<div
onClick={() =>
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}
</FormLabel>
</div>
</div>
</FormControl>
</FormItem>
@ -101,7 +102,7 @@ const FilterItem = memo(
);
},
);
FilterItem.displayName = 'FilterItem';
export const FilterField = memo(
({
item,

View File

@ -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<string, string[]>;
};
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<FilterCollection[]>(filters);
// const [resolvedFilters, setResolvedFilters] =
// useState<FilterCollection[]>(filters);
const [searchTerms, setSearchTerms] = useState<Record<string, string>>({});
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<Record<string, any>>((pre, cur) => {
return filters.reduce<Record<string, any>>((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<ZodString, 'many'> | z.ZodObject<any> | z.ZodOptional<any>
@ -90,13 +132,10 @@ function CheckboxFormMultiple({
return pre;
}, {}),
);
}, [resolvedFilters]);
// const FormSchema = useMemo(() => {
// return z.object({});
// }, []);
}, [filters]);
const form = useForm<z.infer<typeof FormSchema>>({
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 (
<Form {...form}>
<form
@ -142,9 +201,11 @@ function CheckboxFormMultiple({
{filterGroup &&
Object.keys(filterGroup).map((key) => {
const filterKeys = filterGroup[key];
const thisFilters = filters.filter((x) =>
const originalFilters = filters.filter((x) =>
filterKeys.includes(x.field),
);
const thisFilters = getFilteredFilters(originalFilters);
return (
<div
key={key}
@ -153,15 +214,29 @@ function CheckboxFormMultiple({
<div className="text-text-primary text-sm">{key}</div>
<div className="flex flex-col space-y-4">
{thisFilters.map((x) => (
<FilterField
key={x.field}
item={{ ...x, id: x.field }}
parent={{
...x,
id: x.field,
field: ``,
}}
/>
<div key={x.field}>
{x.canSearch && (
<div className="mb-2">
<Input
placeholder={t('common.search') + '...'}
value={searchTerms[x.field] || ''}
onChange={(e) =>
handleSearchChange(x.field, e.target.value)
}
className="h-8"
/>
</div>
)}
<FilterField
key={x.field}
item={{ ...x, id: x.field }}
parent={{
...x,
id: x.field,
field: ``,
}}
/>
</div>
))}
</div>
</div>
@ -169,15 +244,29 @@ function CheckboxFormMultiple({
})}
{notInfilterGroup &&
notInfilterGroup.map((x) => {
const filteredItem = getFilteredFilters([x])[0];
return (
<FormItem className="space-y-4" key={x.field}>
<div>
<FormLabel className="text-text-primary text-sm">
{x.label}
</FormLabel>
<div className="flex justify-between items-center mb-2">
<FormLabel className="text-text-primary text-sm">
{x.label}
</FormLabel>
{x.canSearch && (
<Input
placeholder={t('common.search') + '...'}
value={searchTerms[x.field] || ''}
onChange={(e) =>
handleSearchChange(x.field, e.target.value)
}
className="h-8 w-32 ml-2"
/>
)}
</div>
</div>
{x.list?.length &&
x.list.map((item) => {
{!!filteredItem.list?.length &&
filteredItem.list.map((item) => {
return (
<FilterField
key={item.id}

View File

@ -44,6 +44,7 @@ export const FilterButton = React.forwardRef<
);
});
FilterButton.displayName = 'FilterButton';
export default function ListFilterBar({
title,
children,

View File

@ -5,17 +5,16 @@ export type FilterType = {
list?: FilterType[];
value?: string | string[];
count?: number;
canSearch?: boolean;
};
export type FilterCollection = {
field: string;
label: string;
list: FilterType[];
canSearch?: boolean;
};
export type FilterValue = Record<
string,
Array<string> | Record<string, Array<string>>
>;
export type FilterChange = (value: FilterValue) => void;