@qfei-design/make-filter
v0.2.7
Published
Headless Make advanced filter model, React panel, and optional host component adapters.
Keywords
Readme
@qfei-design/make-filter
Make advanced filter package with a headless core, a host-controlled React
panel, optional component-library adapters, and Make Data API compatible
filter.expression output.
The v1 interaction and internal panel styling follow the current ExpensePoc advanced-filter baseline. The package does not render Popover, Modal, Drawer, or any scroll container; host apps decide where to mount the panel and how wide, tall, or scrollable that container is. It also does not implement table-header filter UI. Header filter UI is a host integration that may call the package controller.
For Make App record-list pages, filtering is normally delivered as one integrated
feature. Make App filtering should wire the toolbar advanced filter and host-owned CanvasTable header linkage together: this package provides the advanced filter model, panel, controller, and openWithField; the host provides the CanvasTable header UI/menu and calls the same controller.
Install
pnpm add @qfei-design/make-filter@^0.2.5npm install @qfei-design/make-filter@^0.2.5yarn add @qfei-design/make-filter@^0.2.50.2.5 is the minimum runtime baseline for Lookup filtering with source-field CEL expressions. The package publishes this README,
PUBLIC_API.md, and the root-level machine-readable integration metadata.
Public Entrypoints
@qfei-design/make-filter: headless core helpers, operators, validation, summary, CEL compile/parse, andcompileListFilter.@qfei-design/make-filter/react:AdvancedFilterPanel,useAdvancedFilterController, candidate-source types, and component contracts.@qfei-design/make-filter/adapters/antd:createAntdFilterComponentsfor Ant Design hosts.@qfei-design/make-filter/styles.css: internal panel styles. Import once in the host UI entry.
Do not import from src, dist, or package-internal files.
Quick Start: AntD Popover Host
The package renders only the advanced filter panel. The host owns the toolbar button, Popover, width, max height, scroll behavior, applied state, and record reload.
import { Button, Popover } from "antd";
import { useState } from "react";
import {
AdvancedFilterPanel,
useAdvancedFilterController,
} from "@qfei-design/make-filter/react";
import { createAntdFilterComponents } from "@qfei-design/make-filter/adapters/antd";
import "@qfei-design/make-filter/styles.css";
const filterComponents = createAntdFilterComponents();
export function AdvancedFilterPopover({
appliedGroup,
candidateSources,
fields,
onConfirm,
}) {
const [open, setOpen] = useState(false);
const controller = useAdvancedFilterController({
fields,
value: appliedGroup,
onChange: onConfirm,
});
function handleOpenChange(nextOpen) {
if (nextOpen) {
controller.beginDraft();
setOpen(true);
return;
}
controller.resetDraft();
setOpen(false);
}
function handleConfirm() {
const validation = controller.confirm();
if (validation.valid) setOpen(false);
}
return (
<Popover
open={open}
trigger="click"
placement="bottomLeft"
content={
<div style={{ width: 724, maxHeight: 560, overflow: "auto" }}>
<AdvancedFilterPanel
candidateSources={candidateSources}
components={filterComponents}
fields={fields}
value={controller.draftValue}
validationErrors={controller.validationErrors}
onChange={controller.setDraftValue}
onClear={controller.clearDraft}
onConfirm={handleConfirm}
/>
</div>
}
onOpenChange={handleOpenChange}
>
<Button>筛选</Button>
</Popover>
);
}Quick Start: Compile Service Filter
Use compileListFilter to merge toolbar search and applied advanced filters.
import { compileListFilter } from "@qfei-design/make-filter";
const filter = compileListFilter({
advancedFilter: appliedGroup,
fields,
searchText,
});
const requestPayload = filter ? { filter } : {};When compileListFilter returns undefined, omit filter. Do not send
filter: [], filter: {}, or { expression: "" }.
DateRange Filtering
Pass DateRange fields as normal Make field metadata:
{ key: "period", name: "报销周期", type: "Make.Field.DateRange" }The package keeps the original field as a whole-range condition and also adds two generated field options in the panel:
报销周期.开始时间->period['begin']报销周期.结束时间->period['end']
Whole DateRange empty checks compile to period == null and period != null.
Whole DateRange equality keeps the backend range-boundary contract:
period['begin'] == "2026-07-01" && period['end'] == "2026-07-31".
The generated start/end options use the single-date operator matrix and value
editor. Hosts should not create these options manually or use
period['begin'] / period['end'] as raw fieldKey values; the package owns
the generated collision-safe keys and CEL rendering.
Package provides
- Filter IR helpers.
- Make field support and operator matrix.
- Default values, validation, and active-condition summary.
- CEL expression compile and parse.
AdvancedFilterPanelfor React hosts.- Complete default condition rows with the first filterable field and first supported operator selected. Field dropdowns are not auto-opened by the package.
useAdvancedFilterControllerfor draft, reset, clear, confirm, validation, andopenWithFieldfor host-owned field entry points.candidateSourcesprops for remote user and department values.createAntdFilterComponentsfor Ant Design controls.- ExpensePoc-derived internal panel styling through
styles.css.
Host app provides
- Normalized Make field metadata.
- Resolved Lookup relation and target field metadata when Lookup fields should be filterable.
- Applied advanced-filter state.
- Toolbar trigger placement.
- Popover, Modal, Drawer, or another mounting container.
- Container width, max height, and scrolling.
- User and department candidate APIs.
- Service request adapter and record reload timing.
- CanvasTable header filter UI, menu behavior, and
openWithFieldlinkage. - Optional URL/deep-link encoding and parsing policy.
Lookup Host Schema Resolution
Lookup filtering requires the host to resolve the complete runtime schema before passing fields to the package. Do not read copied DSL files or infer the target type from record values. Start from the current Entity, its Lookup field, all runtime relations, and all runtime entities.
For each Make.Field.Lookup source field:
- Read
relationKeyandtargetFieldKeyfrom the source field or itsproperties. - Find the relation and select the opposite Entity from
from/to. - Find
targetFieldKeyin that target Entity. - Pass the source Lookup key plus resolved target field metadata to this package.
This reference resolver accepts normalized runtime schema objects using
entityKey, fieldKey, fieldType, relationKey, from, and to:
const text = (value) =>
typeof value === "string" ? value.trim() : "";
const normalizeFilterField = (field) => ({
key: text(field.fieldKey ?? field.key),
name: text(field.name) || text(field.fieldKey ?? field.key),
type: text(field.fieldType ?? field.type),
properties: field.properties ?? {},
meta: field.meta ?? {},
disabled: Boolean(field.disabled),
});
function resolveLookupFilterField(
sourceField,
sourceEntityKey,
schema,
) {
const normalized = normalizeFilterField(sourceField);
if (normalized.type !== "Make.Field.Lookup") return normalized;
const relationKey = text(
sourceField.relationKey ?? sourceField.properties?.relationKey,
);
const targetFieldKey = text(
sourceField.targetFieldKey ?? sourceField.properties?.targetFieldKey,
);
const relation = (schema.relations ?? []).find(
(item) => text(item.relationKey ?? item.key) === relationKey,
);
const fromEntityKey = text(relation?.from?.entityKey);
const toEntityKey = text(relation?.to?.entityKey);
const targetEntityKey =
fromEntityKey === sourceEntityKey
? toEntityKey
: toEntityKey === sourceEntityKey
? fromEntityKey
: "";
const entities = schema.objects ?? schema.entities ?? [];
const targetEntity = entities.find(
(item) => text(item.entityKey ?? item.key) === targetEntityKey,
);
const targetField = targetEntity?.fields?.find(
(item) => text(item.fieldKey ?? item.key) === targetFieldKey,
);
const targetFieldType = text(targetField?.fieldType ?? targetField?.type);
if (
!relationKey ||
!targetFieldKey ||
!targetField ||
!targetFieldType ||
targetFieldType === "Make.Field.Lookup"
) {
return normalized;
}
return {
...normalized,
lookup: {
relationKey,
targetField: {
fieldKey: targetFieldKey,
fieldType: targetFieldType,
name: targetField.name,
properties: targetField.properties,
meta: targetField.meta,
disabled: Boolean(targetField.disabled),
},
},
};
}
const fields = currentEntity.fields.map((field) =>
resolveLookupFilterField(field, currentEntity.entityKey, schema),
);If the relation, target Entity, target field, or target type cannot be resolved,
leave the Lookup field without lookup metadata. The package treats it as
unsupported and hides it from field selectors and header filter actions.
Pass the same resolved fields array to the panel, compiler, validation, search,
and parser:
import {
compileListFilter,
parseCelToAdvancedFilter,
} from "@qfei-design/make-filter";
const filter = compileListFilter({
advancedFilter: appliedGroup,
fields,
searchText,
});
const requestPayload = filter ? { filter } : {};
const parsed = parseCelToAdvancedFilter(savedExpression, fields);
controller.openWithField("mediaNameLookup");The panel and Filter IR store the source Lookup key. CEL also uses that source
key, for example mediaNameLookup.contains("腾讯"); never emit
mediaName.contains("腾讯") or a cross-object path. The resolved target field
type only controls operators, value normalization, validation, and value editor
options.
Host-Owned CanvasTable Header Linkage
Header filter UI is a host integration, not a package feature. For CanvasTable
header menus, keep the table integration in the host and call the package
controller when the user chooses 按该字段筛选:
controller.openWithField(fieldKey);The package does not implement CanvasTable header filter UI.
The package does not implement CanvasTable suffixRender.
The package also does not implement header menu placement or scroll cleanup. The
header action should only open the same toolbar advanced filter draft; it must
not submit immediately or reload records before 确认.
Candidate Sources
User and department filter values should use identities, not display labels.
Pass remote candidates through candidateSources:
const candidateSources = {
users: { options: userOptions, loading: usersLoading, onSearch: searchUsers },
departments: {
options: departmentOptions,
loading: departmentsLoading,
onSearch: searchDepartments,
},
};Do not source production user or department options from field schema options, current table rows, local demo arrays, or display labels.
Out Of Scope
This package does not render Popover, Modal, Drawer, or scroll containers. It
does not implement Service routes, authentication, deployment, saved views,
CanvasTable header filter UI, or CanvasTable suffixRender. The package does not
filter loaded table rows locally; Make record lists must send filter.expression
to the Service/backend.
Published Documentation
- Human integration guide:
README.md - Public API contract:
PUBLIC_API.md - Package metadata and read order for agents:
package.ai.json - Integration recipes:
recipes.json - Capability catalog:
capabilities.json
Development
pnpm test
pnpm typecheck
pnpm build