npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@qfei-design/make-app-filter

v1.1.2

Published

Headless Make advanced filter model, React panel, and optional host component adapters.

Readme

@qfei-design/make-app-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-app-filter@^1.0.0
npm install @qfei-design/make-app-filter@^1.0.0
yarn add @qfei-design/make-app-filter@^1.0.0

1.0.0 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. unsupportedExpression, AdvancedFilterPanel.disabled, AdvancedFilterPanel.layout, AdvancedFilterPanelLayout, minConditionCount, maxGroupDepth, autoCreateDefaultCondition, canRemoveNodeFromGroup, and RemoveNodeFromGroupOptions are newer public APIs. v1.0.0 and v1.0.1 do not contain unsupportedExpression, AdvancedFilterPanel.disabled, AdvancedFilterPanel.layout, AdvancedFilterPanelLayout, minConditionCount, maxGroupDepth, autoCreateDefaultCondition, canRemoveNodeFromGroup, or RemoveNodeFromGroupOptions. Released v1.0.0 through v1.0.3 do not contain AdvancedFilterPanelLayout "mobile" or its compact size="middle" control contract. Upgrade to the release whose React declaration includes the "mobile" literal before using that layout. Before using any of these APIs, ensure the installed root declaration exports CompileListFilterOptions, canRemoveNodeFromGroup, and RemoveNodeFromGroupOptions, and the React declaration includes disabled, layout, minConditionCount, maxGroupDepth, and autoCreateDefaultCondition and exports AdvancedFilterPanelLayout; a lockfile pinned to either older version must be upgraded through the normal release channel.

Public Entrypoints

  • @qfei-design/make-app-filter: headless core helpers, operators, validation, summary, CEL compile/parse, and compileListFilter.
  • @qfei-design/make-app-filter/react: AdvancedFilterPanel, useAdvancedFilterController, candidate-source types, and component contracts.
  • @qfei-design/make-app-filter/adapters/antd: createAntdFilterComponents for Ant Design hosts.
  • @qfei-design/make-app-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 { useLayoutEffect, useRef, useState } from "react";
import { cloneFilterGroup } from "@qfei-design/make-app-filter";
import {
  AdvancedFilterPanel,
  useAdvancedFilterController,
} from "@qfei-design/make-app-filter/react";
import { createAntdFilterComponents } from "@qfei-design/make-app-filter/adapters/antd";
import "@qfei-design/make-app-filter/styles.css";

const filterComponents = createAntdFilterComponents();

export function ContextualAdvancedFilterPopover({ resetKey, ...props }) {
  return <AdvancedFilterPopover key={resetKey} {...props} />;
}

function AdvancedFilterPopover({
  appliedGroup,
  candidateSources,
  fields,
  onApplied,
  onPersist,
}) {
  const [open, setOpen] = useState(false);
  const [saving, setSaving] = useState(false);
  const [saveError, setSaveError] = useState("");
  const activeRef = useRef(true);
  const saveRequestRef = useRef<symbol | null>(null);

  const controller = useAdvancedFilterController({
    fields,
    value: appliedGroup,
  });

  useLayoutEffect(() => {
    activeRef.current = true;
    return () => {
      activeRef.current = false;
      saveRequestRef.current = null;
    };
  }, []);

  function handleOpenChange(nextOpen) {
    if (saveRequestRef.current) return;
    if (nextOpen) {
      controller.beginDraft();
      setSaveError("");
      setOpen(true);
      return;
    }
    controller.resetDraft();
    setOpen(false);
  }

  function isCurrentSave(requestId) {
    return activeRef.current && saveRequestRef.current === requestId;
  }

  function finishSave(requestId) {
    if (!isCurrentSave(requestId)) return;
    saveRequestRef.current = null;
    setSaving(false);
  }

  async function handleConfirm() {
    if (saveRequestRef.current) return;
    const validation = controller.validate();
    if (!validation.valid) return;

    const nextValue = cloneFilterGroup(controller.draftValue);
    const requestId = Symbol("filter-preset-save");
    saveRequestRef.current = requestId;
    setSaving(true);
    setSaveError("");

    try {
      await onPersist(nextValue);
    } catch {
      if (isCurrentSave(requestId)) {
        setSaveError("筛选保存失败,请重试");
      }
      finishSave(requestId);
      return;
    }

    if (!isCurrentSave(requestId)) return;
    onApplied(nextValue);
    controller.resetDraft();
    setOpen(false);
    finishSave(requestId);
  }

  return (
    <Popover
      open={open}
      trigger="click"
      placement="bottomLeft"
      content={
        <div className="advanced-filter-popover">
          <AdvancedFilterPanel
            candidateSources={candidateSources}
            components={filterComponents}
            disabled={saving}
            fields={fields}
            value={controller.draftValue}
            validationErrors={controller.validationErrors}
            onChange={controller.setDraftValue}
            onClear={controller.clearDraft}
            onConfirm={() => void handleConfirm()}
          />
          {saveError ? <div role="alert">{saveError}</div> : null}
        </div>
      }
      onOpenChange={handleOpenChange}
    >
      <Button disabled={saving}>筛选</Button>
    </Popover>
  );
}

The host sizes the outer container and keeps only the package body scrollable:

.advanced-filter-popover {
  width: 724px;
  max-height: min(560px, calc(100vh - 160px));
  overflow: hidden;
}

.advanced-filter-popover .advanced-filter__body {
  overflow-y: auto;
}

onPersist only saves the preset. onApplied updates the applied filter after that save succeeds; the record query should react to applied state in its own effect or query key. resetDraft() ends the successful package draft only after the persisted snapshot has become the applied value. Do not reload records inside the persistence callback. Pass disabled={saving} to freeze every draft mutation while persistence is pending; otherwise a later user edit could be discarded when the saved snapshot is applied.

resetKey is the host's committed, monotonic object-context generation. The keyed wrapper remounts the controller only after that context transition commits, and the layout-effect cleanup invalidates the old async handler on context change or unmount. Do not mutate request-generation, saving, or context refs during render. Comparing only the current key cannot reject an old A -> B -> A result.

条件组保存与刷新回显(宿主必读)

本次修复使用合法 CEL 的双层括号保留显式子条件组:

amount > 100 && ((title.contains("项目")))
amount > 100 || ((title.contains("项目") && amount > 200))

双层括号是编辑器分组边界;“为空、包含任一项”等操作符内部的普通括号仍属于一个条件。不要手写括号标记,统一调用包内编译与解析 API。

宿主按以下链路对接,不需要在后端增加条件树字段:

  1. 确认草稿后使用 compileAdvancedFilterToCel(draft, fields) 生成仅含高级筛选的表达式。不要把顶部搜索词合入需要保存的 Preset。
  2. 表达式非空时保存 { filter: { expression } },清空时保存 { filter: null }。服务端必须原样保存并返回表达式,允许清理首尾空白,但不能重排、简化或删除括号。
  3. 保存成功后更新已应用状态;失败时保留原状态和草稿。异步结果继续校验对象和请求代次,防止旧的 A → B → A 响应覆盖新状态。
  4. 刷新页面或再次进入时,调用 parseCelToAdvancedFilter(savedExpression, fields),将成功返回的 filter 直接传给 controller / AdvancedFilterPanel,不要在宿主再做同关系组展开。
  5. 查询记录时再调用 compileListFilter({ advancedFilter, fields, searchText }) 合并当前搜索词;它的输出用于查询,不替代第 1 步的 Preset 表达式。
const validation = validateAdvancedFilter(draft, fields);
if (!validation.valid) {
  // 显示 validation.structureError 和 conditionErrors,保留草稿,不发起保存。
  return;
}
// 包括搜索合并在内的编译要放在宿主现有错误处理内;成功后才能继续保存/查询。
const expression = compileAdvancedFilterToCel(draft, fields);
const presetUpdate = { filter: expression ? { expression } : null };
// 先用宿主已有接口保存 presetUpdate,成功后应用 draft。

const restored = parseCelToAdvancedFilter(savedExpression ?? "", fields);
if (restored.status === "supported") {
  // restored.filter 是回显值;刷新后也不需要靠内存快照恢复分组。
  const panelValue = restored.filter;
}

包的公共函数签名、React props 和样式入口不变。宿主需要安装包含本次修复的发布版本、同步锁文件并重新构建部署;已发布的 1.1.1 不包含此修复。已有内存快照可保留,但不能代替上述刷新回显链路。

本次不迁移或猜测历史表达式的分组意图。空组不保存;只有一个子节点时关系没有运算区别,恢复为默认 and;组/条件 ID 会重新生成,宿主不能依赖它们跨刷新保持一致。查询结果仍受服务端 CEL 子集约束,本功能不会自动将任意嵌套 OR 展开成 DNF。

验收至少覆盖:确认后重开、浏览器刷新、离开后返回、单条件组、同关系多组、多层组、复合操作符、清空、保存失败和旧请求晚返回。

Make 逻辑组合限制与搜索合并

后端支持外层“或”、组内“且”:A || (B && C)、(A && B) || (C && D)。不支持 A && (B || C);条件组容器本身可以保留,同关系组、空组和单子节点包装不算新增逻辑运算。

  • 编辑阶段就限制能力:关系下拉框只显示切换后受支持的值;根组及子组的添加操作若会产生不支持组合就禁用;表头 openWithField 也不能绕过限制。只有切换为受支持关系后,相关添加操作才恢复。maxGroupDepth 等参数不能放开后端逻辑限制。
  • validateAdvancedFilter 对不支持的组合返回 valid: false 和 structureError。controller 的 confirm() 不触发 onChange;所有面板布局立即显示提示并禁用确认,允许用户继续修改草稿。
  • compileAdvancedFilterToCel 和 compileListFilter({ advancedFilter }) 遇到此类树会抛出错误,不能将它当成空筛选或静默丢弃条件。无面板或自定义确认按钮的宿主也必须先校验,并在现有错误处理内编译。
  • 仅查询阶段,compileListFilter 将搜索与高级筛选等价分配为 DNF。例如高级筛选 A || B、搜索 S1 || S2,输出 (A && S1) || (A && S2) || (B && S1) || (B && S2)。字段操作符内部的空值检查等复合表达式作为完整条件保留,不改变其语义。
  • 自动合并最多生成 256 个分支,超限抛错;宿主显示错误并保留当前已应用查询,不能捕获后改成无筛选请求。搜索输入更新时也要处理该错误;不要为失败的编译发送记录请求。在保存草稿前,可用当前搜索词调用 compileListFilter 预检,预检通过再保存 Preset。
  • unsupportedExpression 仍是服务端已接受的未知表达式透传通道。合并搜索只拆外层 OR,不解释未知谓词、不修复原文内部不受支持的嵌套逻辑;没有搜索时继续原样透传。宿主不能用此通道绕过新草稿校验。

否定操作符按后端标准写法编译:not_in 为 !(field in [...]),has_none 为 !([...].exists(x, x in field)),不额外拼接“或为空”。NULL 是否命中遵循后端语义;如业务还需要包含空值,显式使用外层“或”添加“为空”条件。多选、多用户、多部门不支持 neq,校验不再接受该操作符,不恢复旧的集合不等式扩展。

新建草稿仅提供当前后端支持的组合,不新增历史条件树迁移或兼容策略。以上限制与合并统一由包负责。宿主升级包含修复的新版本后,沿用 controller 和面板即可获得拦截;自定义保存/查询链路按上述约定接入错误处理,无需自行实现 DNF 转换。新查询表达式只用于请求,不覆盖用于条件组回显的 Preset 原文。

Quick Start: Compile Service Filter

Use compileListFilter to merge toolbar search with either an editable filter group or a preserved unsupported expression.

import {
  compileListFilter,
  parseCelToAdvancedFilter,
} from "@qfei-design/make-app-filter";

const parsed = parseCelToAdvancedFilter(savedExpression, fields);
const filter =
  parsed.status === "unsupported"
    ? compileListFilter({
        fields,
        searchText,
        unsupportedExpression: parsed.expression,
      })
    : compileListFilter({
        advancedFilter: parsed.filter,
        fields,
        searchText,
      });

const requestPayload = filter ? { filter } : {};

When compileListFilter returns undefined, omit filter. Do not send filter: [], filter: {}, or { expression: "" }.

Keep the parse result as a discriminated applied state. When it is unsupported, pass its raw expression back through compileListFilter.unsupportedExpression; the package preserves it and owns the safe AND merge with search. Do not concatenate CEL in the host. Keep the toolbar visibly active and show a compatibility warning; only replace or clear the expression after a new confirmation persists successfully.

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.
  • AdvancedFilterPanel for React hosts.
  • AdvancedFilterPanel optional layout="stacked" presentation for narrow side panels. It keeps the same filter IR, interactions, validation, and CEL output while placing the value editor beneath the field and operator controls.
  • AdvancedFilterPanel optional layout="mobile" presentation for a host-owned mobile sheet. The host may use a bottom or full-screen presentation. It keeps the same filter IR, interactions, validation, and CEL output while rendering touch-friendly condition cards and defaulting the panel title and confirm action to 设置筛选条件 / 完成.
  • Complete default condition rows with the first filterable field and first supported operator selected. Field dropdowns are not auto-opened by the package.
  • Optional whole-tree minimum condition retention through AdvancedFilterPanel minConditionCount={1}. It disables destructive actions that would leave no conditions and removes nested groups emptied by an allowed condition deletion.
  • Optional maxGroupDepth for explicitly limiting condition-group levels while keeping the legacy depth behavior when it is omitted.
  • Optional autoCreateDefaultCondition={false} for hosts that need an empty filter until the user adds the first condition or group.
  • useAdvancedFilterController for draft, reset, clear, confirm, validation, and openWithField for host-owned field entry points.
  • candidateSources props for remote user and department values.
  • createAntdFilterComponents for Ant Design controls.
  • ExpensePoc-derived internal panel styling through styles.css.

Narrow panel layout

The default layout="default" preserves the existing compact inline layout. When a host renders the panel in a constrained side area, pass layout="stacked" to keep the field and operator controls on one row and move the value control to the next row. This is a presentation-only option; it does not alter the filter group structure, nested AND/OR behavior, validation, or generated CEL.

<AdvancedFilterPanel
  components={filterComponents}
  fields={fields}
  layout="stacked"
  value={controller.draftValue}
  onChange={controller.setDraftValue}
/>

Mobile sheet layout

For a mobile record list, keep search and the filter trigger in the host page and mount the panel inside a host-owned mobile sheet. Use a bottom or full-screen presentation according to the available height and filter complexity. Pass layout="mobile" to render condition cards, grouped labels, a panel footer, and a 完成 confirmation action. This is presentation-only: the host still controls the sheet, mask, close action, scroll container, safe-area handling, persistence, and record reload. When the host scrolls the entire Sheet, the footer follows the host sheet's scroll behavior. To keep actions visible while conditions scroll, the host must constrain the Sheet and make only .advanced-filter__body scrollable, as in the Popover example above.

<MobileSheet presentation="bottom" open={open} onClose={closeAndDiscardDraft}>
  <AdvancedFilterPanel
    components={filterComponents}
    fields={fields}
    layout="mobile"
    value={controller.draftValue}
    onChange={controller.setDraftValue}
    onConfirm={handlePersistThenApply}
  />
</MobileSheet>

Every layout sends size="middle" to injected controls by default. The built-in styles use the standard 32px middle height for fields, operators, values, relation controls, secondary actions, and confirmation. The mobile layout also adds its scoped mobile control class. A custom component adapter must honor the same 32px visual control contract. The built-in Ant Design adapter keeps mobile multi-select values to one ellipsized tag plus a remaining-count hint. Custom adapters must forward the provided className and render field, operator, and value text on one line with ellipsis instead of wrapping or overflowing its control; the package does not import or override a host design system. Released v1.0.0 through v1.0.3 do not contain AdvancedFilterPanelLayout "mobile" or its compact size="middle" control contract.

Group Depth and Empty Drafts

maxGroupDepth counts condition-group levels below the root group. Pass 0 to hide “添加条件组”, 1 to allow only one group level, or a larger integer to allow further nested levels. Omit it to preserve the existing maxDepth and allowNestedGroupAdd behavior.

autoCreateDefaultCondition defaults to true. Set it to false on both the panel and controller to render an empty filter with only available add actions. When minConditionCount is greater than 0, the minimum takes precedence and the package creates enough draft conditions to satisfy it.

const controller = useAdvancedFilterController({
  autoCreateDefaultCondition: false,
  fields,
  value,
});

<AdvancedFilterPanel
  autoCreateDefaultCondition={false}
  components={filterComponents}
  fields={fields}
  maxGroupDepth={3}
  value={controller.draftValue}
  onChange={controller.setDraftValue}
/>

Minimum Retained Conditions

Set minConditionCount={1} when the host requires the filter to keep at least one condition across its whole group tree. The panel disables a condition or group delete control when that action would violate the limit, disables the clear action, and removes a nested group after an allowed deletion leaves it empty. The package also materializes the minimum in the controlled panel value. When using useAdvancedFilterController, pass the same option to the controller so direct clearDraft, setDraftValue, and confirm calls preserve the rule. This does not require every group to contain a condition. If the host currently cannot provide enough filterable fields to satisfy the minimum, the panel disables confirmation and the controller returns an invalid confirmation result without emitting an empty filter; once filterable fields are available, the existing minimum is materialized normally.

const controller = useAdvancedFilterController({
  fields,
  minConditionCount: 1,
  value,
});

<AdvancedFilterPanel
  components={filterComponents}
  fields={fields}
  minConditionCount={1}
  value={controller.draftValue}
  onChange={controller.setDraftValue}
/>

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.
  • Async persistence request generation and reset behavior for object changes.
  • Visible applied state for unsupported saved CEL while backend filtering remains active.
  • CanvasTable header filter UI, menu behavior, and openWithField linkage.
  • 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:

  1. Read relationKey and targetFieldKey from the source field or its properties.
  2. Find the relation and select the opposite Entity from from / to.
  3. Find targetFieldKey in that target Entity.
  4. 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-app-filter";

const parsed = parseCelToAdvancedFilter(savedExpression, fields);
const filter =
  parsed.status === "unsupported"
    ? compileListFilter({
        fields,
        searchText,
        unsupportedExpression: parsed.expression,
      })
    : compileListFilter({
        advancedFilter: parsed.filter,
        fields,
        searchText,
      });
const requestPayload = filter ? { filter } : {};

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.

For Make.Field.SingleUser, the package prepends a built-in 当前登录人 option before remote user candidates. Selecting it stores a structured system-variable value and compiles to _currentUser, for example approver == _currentUser. Host apps should not add this option to candidateSources.users.options; those options should remain real user identities only. Make.Field.MultiUser does not show this built-in option.

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

值校验与编译失败约定(宿主必读)

validateAdvancedFilter、isConditionComplete 和编译器复用同一套字段值校验。自定义编辑器也必须遵守:

  • 数值必须为有限的 number,不接受数字字符串、NaN 或 Infinity;文本必须为非空字符串。
  • 日期使用有效的 YYYY-MM-DD;日期时间使用 YYYY-MM-DD HH:mm:ss,包括日期时间区间的两个端点,不接受仅日期或 ISO 时区格式。区间必须恰有两个有效端点,开始不晚于结束。
  • 多值操作符使用非空数组,每个元素符合字段类型;不接受稀疏数组、空元素、嵌套数组或任意对象。单选值支持字符串、有限数字和布尔值;用户/部门 ID 支持非空字符串和有限数字;结构化当前用户变量仅适用于已支持的单用户操作符。
  • 附件数量必须为非负安全整数,附件名称包含/不包含使用非空字符串。“为空/不为空”不读取遗留 value。

真正的空条件树仍可省略;只要含有未知字段、缺失字段元数据、不支持的操作符、无效值或不支持的结构,compileAdvancedFilterToCel / compileListFilter({ advancedFilter }) 就整体抛错,不再静默删除该条件后生成剩余查询。宿主先校验,再在现有异常处理内编译;失败时保留已应用状态并停止请求,禁止将编译失败当成空筛选发送全量查询。标准面板与控制器已复用此校验,自定义接入遵循同一合同,无需复制包内规则。

公开字面量工具 renderCelLiteral 对非有限数字和不支持的对象抛错,不再替换成 0 或 null;显式 null/undefined 仍输出 null。此轮没有增加公共导出、依赖或宿主私有逻辑,版本号未变;已发布的 1.1.1 不包含这些尚未发布的修改。

CEL 值的无损回显

数组中包含 null、嵌套数组、对象或无法表示的元素时,解析器整体返回 unsupported 并保留原始表达式,绝不删除部分元素后返回可编辑条件。宿主继续遵守已有 unsupportedExpression 合同,无需实现自己的清洗或数值转换。

CEL 整数字面量只有在能精确转换为 JavaScript number 时才进入编辑器;无法精确表示的大整数返回 unsupported,不转换为字符串或四舍五入。包内 number 超出安全整数范围时编译为指数浮点格式(例如 1e+16),使重新解析后仍保持相同数值与 number 类型;负数字面量同样支持往返。该规则不表示后端各数值字段允许任意大小,业务仍遵守字段范围。公共类型、导出及依赖未变。

独立条件保持原结构

同字段的 >= 与 <= 保持两条独立比较条件,不自动合并为区间操作符;原生日期区间仍通过 .isWithin(...) 等专用表达式回显。“为空,或不属于列表”(以及显式 NULL 与集合否定的 OR)保留两条条件,不再套用旧的空值包装折叠规则。根层和显式条件组采用同一合同,宿主直接使用包解析结果,无需自行拆分条件;不新增历史数据迁移。

复合条件括号协议

解析逻辑链时,仅将带一层完整外括号的谓词识别为编译器生成的复合条件;双层外括号继续标识显式条件组。裸逻辑链保留各独立条件,包括日期区间虚拟开始/结束字段的比较、为空/不为空条件,不因 AST 恰好匹配区间模式而合并。日期区间本身的等于操作仍由编译器输出单层括号,专用方法操作不变。

宿主必须保留编译器输出的表达式原文和括号,读取后直接调用包解析器;不要剥离括号或自行推断/合并字段条件。本轮未引入历史表达式迁移,也未修改公共导出、依赖或编译输出。