Skip to content

fix(plugin-detail): 相关列表 Add 选择器兑现 add.picker.filter,作者限定的候选范围真的生效 (#3831) - #3908

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3831-picker-filter-basefilter
Aug 9, 2026
Merged

fix(plugin-detail): 相关列表 Add 选择器兑现 add.picker.filter,作者限定的候选范围真的生效 (#3831)#3908
yinlianghui merged 1 commit into
mainfrom
claude/issue-3831-picker-filter-basefilter

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3831

record:related_list.add.picker.filter 被 spec 声明为「Restrict which records the picker offers」,渲染器从未读过它。RelatedListRecordPickerDialog 时传 objectName / title / displayField / columns / cellRenderer / fieldsMeta / multiple / onSelect / onSelectRecords,没有任何 filter。作者写下「只允许指派 active 的岗位」「只允许挂未过期的许可」,拿到的是 picker.object全部记录;选中后 handleAddRecords 直接建链接行或改父,os validate / os build 全绿,运行时零诊断 —— 作者只能靠肉眼发现对话框里多出了不该出现的记录。

现在它按原样传给 baseFilter,不是 lookupFilters:后者会把条件渲染成用户可编辑的筛选栏行,等于把作者的硬性限制降级成建议。

更正 issue 正文的一处误述

正文写「picker.filter 在 pin 版是宽类型」。实读 @objectstack/spec@17.0.0-rc.5(src/ui/component.zod.ts:308):

filter: z.array(ViewFilterRuleSchema).optional().describe('Restrict which records the picker offers.'),

严格的 ViewFilterRule[] —— {field, operator, value?},operator 为 19 个 canonical 值的 enum,与 list 级 filterListViewSchema.filterViewTab.filter 同族。spec 侧无需任何收敛,但形状差距因此比正文预期的大,见下。

为什么改动落到了 packages/fields

派发时的围栏是「只在 RelatedList 侧接线」。复核后发现在该围栏内无法无损接线,已带证据上报并由维护者裁决放宽(#3831 评论)。原因:

baseFilter 声明为 Record< string, any >、以对象展开合并。这个形状服务依赖型 lookup 链(#2215)恰好正确,却装不下 spec 的规则数组 —— TS 接受数组塞进该槽位(数组满足 any 的字符串索引,tsc --strict 实测 exit 0),展开再把它压成 {"0": rule, "1": rule},查询于是去过滤名为 0 / 1 的列:类型全绿、查询错误、无任何诊断。

绕开它只剩一条路:在 objectui 里再写一份 spec-operator 词汇表。而这份词汇表已存在两份(spec 的 AST_OPERATOR_MAP、data-objectstack 的 FILTER_OPERATOR_ALIASES),filter-converter.ts 的文档块以「One lowering, one place」明确反对第三份,并引 #3948 说明两份的代价。实测记录方言确实装不下全部 operator:

{ amount: { $gt: 10, $lte: 100 } }        => ["and",["amount",">",10],["amount","<=",100]]   OK
{ expires_at: { $after: '2026-01-01' } }  => THROWS FilterOperatorError: Unknown filter operator '$after'
{ deleted_at: { $isEmpty: true } }        => THROWS FilterOperatorError: Unknown filter operator '$isEmpty'

落地形状

baseFilter 按结构判别(Array.isArray)接受两种形状。判别子是精确而非启发式的 —— AST 节点必是数组、规则必是普通对象,与 toFilterNode 同一谓词:

  • 记录形式(依赖型 lookup 链)保持键覆盖语义逐字节不变:级联父值必须替换同字段上过期的 lookupFilters 条目,而不是与之求交。这一点承重 —— 朴素改成合取会问 account = 'stale' AND account = 'a1',返回零行。LookupField.dependsOn.test.tsx 一行未动且保持绿。
  • 规则数组mergeFilterNodes(仓内唯一 filter 下沉口,与 plugin-list 的 buildEffectiveFilter、plugin-view 的 ObjectView 共用)下沉,19 个 operator 全部无损,包括记录形式没有 $op 可用的 before / after / is_empty / is_not_empty。两者同时在场时,记录侧仍先合成再作为一个节点下沉,键覆盖优先级在合取中存活。

槽位类型同时从 Record< string, any > 收紧为 unknown;useRecordQuery 的 filter 类型与空判随之数组感知(Object.keys 对数组返回下标,旧的记录专用判断对 AST 节点只是碰巧成立)。RelatedListProps.add.picker.filterany 收紧为 spec 的 ViewFilterRule[] —— 宽类型正是错误形状的藏身处。

KNOWN GAP 句与它的钉子

record:related_list.add 的 input description 删掉 #3808#3165 先例写入的 KNOWN GAP 句 —— gap 兑现之后,不是之前。recordRelatedListInputs.spec-parity.test.ts 原本钉着那句话(「fails the moment someone deletes the warning without doing it」),现已翻转方向:现在它在「gap 警告被放回」或「接线被回滚却仍宣称限制生效」时报红。

反向验证(方向先判后跑)

两向都跑了,预判先写:

摘掉 RelatedList 的直传 —— 预判数组路钉红、无 filter 用例绿、fields 侧钉全绿。实测吻合,3 枚红全部点名 expected undefined to be defined。一处预判偏保守需如实说明:我原判「不降级成筛选栏行」那枚会以「什么都没产出」的空原因保持绿(#5046 式假绿),实测它也红了 —— 它的首行 waitFor 是对接线的真实前置条件,筛选栏断言只在限制真的生效后才执行。比预判更严,不是更松。

把合并回退成旧的对象展开(保留直传)—— 预判两个文件的数组路钉红,且报损坏而非缺失;记录路钉与 #2215 全绿。实测吻合,5 枚红全部报:

AssertionError: expected { '0': { field: 'is_active', …(2) } } to deeply equal [ [ 'is_active', 'equals', true ] ]

LookupField.dependsOn.test.tsx 六枚钉全绿 —— 这是判别式设计的承重预判。另有一枚「空规则数组不发 $filter」在回退下仍绿(展开 []{},空判成立),即旧代码下它是因错误的原因而绿;如实记录,不当作覆盖。

验证

未做

QueryParams.$filter 仍声明为 Record< string, any >,而 plugin-list / plugin-view 的下沉口早已往里送 AST 数组 —— 类型漂移先于本单存在,收敛它会波及 packages/types 的公共面,故在此仅于单个赋值点就地转换并注明,另立观察单。#3898(baseFilterColumns 命名)按分诊未顺手改。


Generated by Claude Code

…3831)

`record:related_list.add.picker.filter` 被 spec 声明为「Restrict which records
the picker offers」,渲染器从未读过它:`RelatedList` 挂 `RecordPickerDialog` 时传
`objectName` / `title` / `displayField` / `columns` / `cellRenderer` /
`fieldsMeta` / `multiple` / `onSelect` / `onSelectRecords`,没有任何 filter。作者
写下「只允许指派 active 的岗位」「只允许挂未过期的许可」,拿到的是 `picker.object`
的全部记录;选中后直接建链接行或改父,`os validate` / `os build` 全绿,运行时零诊断。

现在它按原样传给 `baseFilter` —— 不是 `lookupFilters`,后者会把条件渲染成用户可编辑
的筛选栏行,等于把作者的硬性限制降级成建议。

## 为什么改到了 packages/fields

`baseFilter` 声明为 `Record<string, any>`、以对象展开合并,这个形状服务依赖型 lookup
链(#2215)恰好正确,却根本装不下 spec 的 `ViewFilterRule[]`:TS 接受数组塞进该槽位
(数组满足 `any` 的字符串索引),展开把它压成 `{"0": rule, "1": rule}`,查询于是去过滤
名为 `0` / `1` 的列 —— 类型全绿、查询错误、无任何诊断。绕开它只剩「在 objectui 里再写
一份 spec-operator 词汇表」一条路,而这份词汇表已有两份(spec 的 `AST_OPERATOR_MAP`、
data-objectstack 的 `FILTER_OPERATOR_ALIASES`),#3948 就是两份的代价。

所以槽位按结构判别(`Array.isArray`)接受两种形状,判别子是精确的而非启发式的 ——
AST 节点必是数组、规则必是普通对象,与 `toFilterNode` 同一谓词:

- 记录形式保持键覆盖语义**逐字节不变**:级联父值必须**替换**同字段上过期的
  `lookupFilters` 条目,而不是与之求交(`account = 'stale' AND account = 'a1'` 会返回
  零行)。`LookupField.dependsOn.test.tsx` 一行未动且保持绿。
- 规则数组经 `mergeFilterNodes`(仓内唯一 filter 下沉口,与 plugin-list 的
  `buildEffectiveFilter`、plugin-view 的 ObjectView 共用)下沉,19 个 operator 全部
  无损,包括记录形式没有 `$op` 可用的 `before` / `after` / `is_empty` /
  `is_not_empty`。不新增第二份词汇表。

槽位类型同时收紧为 `unknown`,`useRecordQuery` 的 filter 类型与空判随之数组感知
(`Object.keys` 对数组返回下标,旧的记录专用判断对 AST 节点只是碰巧成立)。

## 顺带

`RelatedListProps.add.picker.filter` 从 `any` 收紧为 spec 的 `ViewFilterRule[]`;
`record:related_list.add` 的 input description 删掉 #3808#3165 先例写入的
KNOWN GAP 句(gap 兑现之后才删),`recordRelatedListInputs.spec-parity.test.ts` 的
那枚钉子随之翻转 —— 现在它反过来在「gap 警告被放回」或「接线被回滚却仍宣称限制生效」
时报红。

反向验证跑了两向,方向先判后跑:摘掉 RelatedList 的直传 → 数组路钉红且点名
`$filter` undefined,无 filter 用例保持绿;把合并回退成旧的对象展开 → 两个文件共
5 枚钉红,且报的是 `{ '0': { field: 'is_active', … } }` 这一**损坏**形态而非缺失,
记录路钉与 #2215 六枚钉全绿。

Co-authored-by: Claude <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 9, 2026 1:31am

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-Burf1mlK.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 482.39KB 106.34KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 230.97KB 56.74KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 117.21KB 30.27KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 236.63KB 59.02KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.63KB 49.66KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Copy link
Copy Markdown
Collaborator Author

✅ 验收(PM,session session_01GTRjn8xBqp75dk7kFupVRt)—— 裁决 A 六条件逐一核毕

实物核验:头 021b5391e,8 文件 +499/−35;RecordPickerDialog 合并分流段 diff 全读 —— baseFilter 类型 Record<string,any>unknown(prop 文档把「TS 接受数组塞进 Record 槽位 → 展开成 {"0":rule} → 查名为 0 的列」的陷阱写成永久注释)、Array.isArray 精确判别(引 toFilterNode 同谓词)、记录路逐字节不变、数组路走 mergeFilterNodes 唯一下沉口、双形状同场时记录侧先合成再作为单节点下沉保住 #2215 键覆盖优先级 —— 这个次序细节超出裁决要求,正确。trailer 0。
CI 终态(独立复核):20 检查全部 completed,18 success + 2 skipped,零失败。

六条件核对:① RelatedList 一行直传 + picker.filter 类型收紧为 spec ViewFilterRule[] ✓;② fields 侧最小改 + useRecordQuery 数组感知 hasFilter、零词汇表副本 ✓;③ #2215 六枚钉一行未动全绿 ✓;④ 两路各配钉(10 例,含 before/after/is_empty 无损)+ 两向反向验证 —— 一处预判如实更正(预判会空绿的那枚实际红了,因其 waitFor 是更严的真前置)与「空数组钉在旧代码下因错误原因而绿」的声明,均不当覆盖凑数 ✓;⑤ KNOWN GAP 句接线后才删、parity 钉翻转为双向守卫 ✓;⑥ 「宽类型」误述更正入 PR 正文 ✓。
另:dev 又抓到一例自建轮询假绿(session 直连 GitHub API 被禁 → check_runs 空读成零 pending),识别后改用 MCC —— 方法库已立案陷阱的再次正确处置。

转 ready 并挂 auto-merge。越界 #3909(QueryParams.$filter 类型声明窄于现实,波及 types 公共面)与 #3898 归分诊席。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 01:39
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit c2ecbae Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3831-picker-filter-basefilter branch August 9, 2026 01:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

record:related_list.add.picker.filter 全仓零读点:作者限定了 Add 选择器的候选范围,对话框照样提供该对象的全部记录

2 participants