Filter Panel
Staged left-rail filters bound to a data-list handle, with operators, sections, and related EXISTS criteria.
Plus Registry
Filter Panel ships on the authenticated @f-ui-plus registry. Configure FUI_PLUS_REGISTRY_TOKEN as described in Installation — Plus Registry.
Filter Panel is the operator 筛选 rail for a list: fields with operators, staged Apply / Clear, optional sections, and related child criteria that still return parent rows. It is bound to a DataListHandle — not a free form.
Compose it on Data List Chrome DataListShell filters. Pair it with Table (or List View). High-frequency parent inquire without operators stays on Query Filter.
When To Use
- Advanced list filters — operators (contains, between, not-in), staged Apply, a left rail or mobile sheet.
- Filter by a related collection — “orders that contain SKU A-19” — via
schema.options.relatedFilters. - Custom list shells —
DataListProvider+DataListShell filters={<FilterPanel />}when Query List is too assembled. - Use Query Filter instead for high-frequency parent fields with no operators (Ant Pro inquire / 查询).
- Do not mount Filter Panel on Related List. That card is Object Page preview; operators belong on View All → Query List.
Compared to Query Filter
Ant Design splits 查询 (inquire) from 筛选 (filter). Query Filter is inquire; Filter Panel is 筛选.
| Query Filter | Filter Panel | |
|---|---|---|
| Job | High-frequency parent search | Advanced filters with operators |
| Grain | Parent fields only | Parent fields and related EXISTS criteria |
| Operators | None — one control per field | Contains / between / not-in / … per field |
| Submit | Staged Search (Formily submit) | Staged Apply / Clear on the list handle |
| Binding | Standalone — you wire onFinish | Handle required — dataList or DataListProvider |
| Related | Does not compile relatedFilters | Authors related criteria in the panel |
Do not put the same paramKey on both surfaces. Query Filter never auto-promotes related fields; a top-bar “Contains product” is a host-owned parent field with a different key.
Related Criteria
Declare child criteria on the schema — not as hidden table columns:
defineDataListSchema<Order>(
{
orderNumber: f.text({ label: "Order #" }),
customer: f.text({ label: "Customer" }),
},
{
relatedFilters: [
{
collection: "lines",
label: "Line items",
fields: {
sku: f.text({ label: "SKU" }),
qty: f.number({ label: "Qty" }),
},
},
],
},
);- Result grain is parent rows. “Any line has SKU A-19” returns matching orders. Mass actions still run on the parent.
any/none. Collection default isany(EXISTS). Setquantifier: "none"on the collection, orrelatedQuantifieron a field, for “no line matches”.- Independent EXISTS. Two related fields AND at the parent: “has a line with SKU A and has a (possibly other) line with qty 0”. Same-line AND is not in this API.
- Related fields are not table columns and not Query Filter fields. Column-header filters skip them; chips still show applied related criteria.
Features
| Area | Behavior |
|---|---|
| Handle | Resolves dataList or the nearest DataListProvider. Not usable as a standalone form. |
| Staging | Draft edits until Apply; Clear resets the panel draft and applied filters. |
| Sections | Optional sections. With related fields and no sections, parent fields sit unlabeled first; each collection gets its label. |
| Field search | showFieldSearch filters the panel by field label. |
| Layout | Desktop sidebar; below DataListShell filtersBreakpoint it becomes a sheet. |
| Related | relatedFilters compile into panel fields; JSON Logic some / none over the child collection. |
Installing
Filter Panel is a Plus component — install through the authenticated @f-ui-plus registry with FUI_PLUS_REGISTRY_TOKEN configured.
pnpm dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/filter-panel.jsonnpx shadcn@latest add https://ui.isaacfei.com/api/plus/r/filter-panel.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/api/plus/r/filter-panel.jsonbunx shadcn@latest add https://ui.isaacfei.com/api/plus/r/filter-panel.jsonWith the @f-ui-plus namespace registered in components.json, shadcn add @f-ui-plus/filter-panel also works.
registryDependencies: @f-ui-plus/data-list-chrome, @f-ui-plus/data-list-internals, button, input, sheet. Runtime: lucide-react. Wrap the app in QueryClientProvider.
Usage
import { FilterPanel } from "@/components/f-ui/filter-panel/filter-panel";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListShell } from "@/components/f-ui/data-list-chrome/layout/data-list-shell";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { Table } from "@/components/f-ui/table/table";
const handle = useDataList({
schema,
listCode: "orders",
data: rows,
getRowId: (row) => row.id,
features: { filters: true },
});
<DataListProvider dataList={handle}>
<DataListShell
filters={<FilterPanel />}
toolbar={<DataListToolbar title="Orders" />}
>
<Table dataList={handle} fillHeight />
</DataListShell>
</DataListProvider>Pass dataList={handle} on FilterPanel when it is not under DataListProvider. Enable features.filters on useDataList.
Examples
Sidebar On a Table
Two-column Data List Shell: staged left Filter Panel (Apply / Clear) and a Table body. Change a field, Apply, and watch chips plus rows update.
| SO-1001 | Ada | Pending |
| SO-1002 | Lin | Shipped |
| SO-1003 | Mei | Pending |
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { defineDataListSchema } from "@/components/f-ui/data-list-internals/schema/define-data-list-schema";
import { useDataList } from "@/components/f-ui/data-list-internals/use-data-list";
import { DataListActiveFilters } from "@/components/f-ui/data-list-chrome/data-list-active-filters";
import { FilterPanel } from "@/components/f-ui/filter-panel/filter-panel";
import { DataListPagination } from "@/components/f-ui/data-list-chrome/data-list-pagination";
import { DataListSearchInput } from "@/components/f-ui/data-list-chrome/data-list-search-input";
import { DataListToolbar } from "@/components/f-ui/data-list-chrome/data-list-toolbar";
import { DataListProvider } from "@/components/f-ui/data-list-chrome/layout/data-list-provider";
import { DataListShell } from "@/components/f-ui/data-list-chrome/layout/data-list-shell";
import { Table } from "@/components/f-ui/table/table";
import { f } from "@/components/f-ui/field-types/catalog";
interface DemoOrder {
id: string;
orderNumber: string;
customer: string;
status: "pending" | "shipped" | "cancelled";
}
const DEMO_ORDERS: DemoOrder[] = [
{ id: "1", orderNumber: "SO-1001", customer: "Ada", status: "pending" },
{ id: "2", orderNumber: "SO-1002", customer: "Lin", status: "shipped" },
{ id: "3", orderNumber: "SO-1003", customer: "Mei", status: "pending" },
{ id: "4", orderNumber: "SO-1004", customer: "Ravi", status: "cancelled" },
];
const demoOrderSchema = defineDataListSchema<DemoOrder>({
orderNumber: f.text({ label: "Order #" }),
customer: f.text({ label: "Customer" }),
status: f.enum({ render: "status", label: "Status", variants: {
pending: { label: "Pending" },
shipped: { label: "Shipped" },
cancelled: { label: "Cancelled" },
} }),
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function TableFilterPanelDemoInner() {
const handle = useDataList({
schema: demoOrderSchema,
listCode: "demo-orders-filter-panel",
data: DEMO_ORDERS,
getRowId: (row) => row.id,
defaultPageSize: 3,
features: { filters: true, pagination: { mode: "offset" } },
});
return (
<DataListProvider dataList={handle}>
<DataListShell
className="h-[min(640px,80vh)] min-h-[420px] rounded-xl border bg-card p-3 shadow-sm"
filters={<FilterPanel title="Filters" />}
toolbar={<DataListToolbar title="Orders" toolsStart={<DataListSearchInput className="w-56" />} />}
footer={<DataListPagination />}
>
<DataListActiveFilters className="pb-2" />
<Table dataList={handle} fillHeight />
</DataListShell>
</DataListProvider>
);
}
/** Two-column shell with a staged left filter panel (Apply / Clear) and a table body. */
export function TableFilterPanelDemo() {
return (
<QueryClientProvider client={queryClient}>
<TableFilterPanelDemoInner />
</QueryClientProvider>
);
}Composition
DataListProvider (handle)
└── DataListShell
├── filters={<FilterPanel />} ← sidebar / sheet
├── toolbar
├── children
│ ├── DataListActiveFilters ← chips (optional)
│ └── Table / List View
└── footerDataListShell.filters is a ReactNode slot. Chrome does not import Filter Panel; you pass the node.
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
dataList | DataListHandle<TData> | context | List handle. Required unless a parent DataListProvider already supplies it. |
title | string | built-in | Panel heading. |
sections | FilterPanelSection[] | derived | Group fields. Omit to auto-group when related fields exist (unlabeled parent, then labeled collections). |
showFieldSearch | boolean | true | Search box that filters visible field rows by label. |
defaultOpen | boolean | true | Desktop sidebar open on first paint. Ignored when open is controlled. |
open | boolean | — | Controlled open state (also owned by DataListShell layout when composed there). |
onOpenChange | (open: boolean) => void | — | Controlled open callback. |
width | number | 280 | Desktop sidebar width in pixels. |
collapsibleSections | boolean | true when sections given | Accordion sections with per-section active counts. |
className | string | — | Root wrapper class. |
FilterPanelSection
| Prop | Type | Description |
|---|---|---|
id | string | Stable section key. |
label | string | Optional heading. Omit for an unlabeled parent group. |
fieldKeys | string[] | paramKey values in display order. |