Skip to content

fix(app-shell): a related list's View all carries the list's own filter, or is not offered - #26060

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-0pu6d9
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-0pu6d9

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #25888

Clause-②: yes (an optional member on the exported ResolveRelatedRecordActionsInput of @object-ui/react widens its public type surface, additive; the seat owes a TIER_DEFAULT review record before the queue; re-declared if the PR changes no exported type)
Metadata-protocol: none

What was wrong

A record:related_list "View all" link opened the child object's list scoped by the list's relationship alone. The list's own filter never reached the host that builds the link: the renderer's resolve call handed the bridge objectName, relationshipField and parentId only, and the bridge built recordListPath(appName, objectName, { [relationshipField]: parentId }). On the card's repro (Approvals on a record page, filter object_name, relationship record_id) the destination read filter[record_id]=ID only, and the approvals request read gate, which admits a record reader only on a read pinning both fields, answered it with the empty participant slice: 2 rows on the tab, 0 on "View all".

What changed

  • @object-ui/react — ResolveRelatedRecordActionsInput gains an optional filter (ViewFilterRule[] | FilterNode): the list's own scope filter, already resolved. The onViewAll doc now says the destination is the relationship AND that filter, and names the cases where the handler is withheld.
  • @object-ui/plugin-detail (record-related-list.tsx) — resolves schema.filter with useResolvedFilter(schema.filter, useFilterScope()), the same hook and scope RelatedList uses for its own query at the same place in the tree, and hands that value to resolve. Resolving here and not in the bridge is deliberate: the bridge is mounted above the page's RecordContextProvider, so its filter scope has no recordId and a {record_id} token would stay unresolved there while the tab resolved it.
  • @object-ui/app-shell — new exactRecordListFilter(sources, canRead) beside recordListPath in useOpenRecordList.ts: the AND of the sources as a filter the list route writes EXACTLY, or null. The bridge passes [{ [relationshipField]: parentId }, filter] and draws "View all" only on a non-null answer.

The open question, settled: a filter the URL cannot express

Ruling: never let the full list show rows the tab did not. Writing only the expressible subset of an AND is always a wider (or, behind a pinning read gate, a different) list, so there is no safe subset: either every condition is written or the link is withheld. exactRecordListFilter writes exactly the route's grammar (equality to a non-empty string or finite number, the four range bounds, $null / $exists / $empty with a boolean, conjunction) and answers null for anything else: $or and other combinators, $in, $ne, text operators, a boolean / null / Date comparand, an empty string (the read side skips an empty param), two different comparands in one param (including a filter that contradicts the relationship), a filter the lowering refuses, and any condition on a field the viewer cannot read (ObjectDataPage drops such a condition). Each accepted condition is proved by its own round trip: written alone it must read back through parseUrlFilterTriples as exactly one triple. Placeholders ({current_user_id}, {record_id}, the date macros) are resolved before this point, by the renderer, against the list's own scope.

Tests

  • RelatedRecordActionsBridge.viewAllFilter-25888.test.tsx (real bridge + SchemaRenderer + RelatedList, oracle parseUrlFilterTriples): the repro carries both record_id and object_name; CONTROL with no filter carries the relationship alone; a {current_user_id} filter is written resolved; range + is-null travel; an or filter draws no link; a filter field the viewer cannot read draws no link; CONTROL denying an unrelated field still draws and carries both.
  • exactRecordListFilter-25888.test.ts: every spelling round-trips, agreeing duplicates collapse, nine inexpressible shapes refuse, canRead refuses for the relationship and the filter field (control: an unrelated field), a lowering refusal is null, never "no filter".
  • RelatedRecordActionsBridge.viewAll-12741.test.tsx (existing) stays green.

Verification (head a281cecb, after merging origin/main)

  • New + existing View all suites: 3 files, 27/27 passed. Ablation: with the bridge's filter argument removed (ablation-replace, anchor 1 -> 0, restored to the HEAD blob, git diff HEAD empty) the 25888 bridge suite went 6 failed / 1 passed, the one survivor being the no-filter CONTROL.
  • Related suites across the three packages (every app-shell test file named for related records / related lists / useOpenRecordList / drill / View all, every plugin-detail related-list file, every react file reading RelatedRecordActions or useResolvedFilter): 73 files, 455/455 passed.
  • type-check for @object-ui/react, @object-ui/plugin-detail, @object-ui/app-shell: exit 0, after building the app-shell dependency closure on the merged tree.
  • Derived gate families (dispatch-gates --commands, no paths): 46 run, every one exit 0, plus check-changeset-fixed exit 0; --ran reconciles 46/46 with 0 NOT-MEASURED.
  • Declared narrowing: the three packages' FULL suites were not run locally (an attempt exceeded the 2-hour background ceiling and was stopped with no reading); CI's Build & E2E UI and Test Core run them.

Acceptance notes

  • scripts/sdui-manifest.record.json is re-recorded (registry source hash only; check-sdui-manifest reds on any ui/packages/** source edit). It was outside the claim's declared file surface; it is a regenerated artifact, node scripts/gen-sdui-manifest-node.mjs, 106 components unchanged.
  • A related list whose filter holds a boolean equality (done = true) now draws no "View all". The URL carries text, and whether filter[done]=true reads back as the list's boolean comparison is unmeasured, so the fail-closed answer is taken. Widening the route's grammar (booleans, $in, $or) is a separate change to the /data URL contract, not this fix.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01HYEgis8rPZK6Dr3G5Jas4q

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 26, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: a281cecb2aaacf3b9fb15b5047ffee1341ea1b44
Local-runs: none

① Derived judgments

  • @object-ui/react adds the optional member ResolveRelatedRecordActionsInput.filter?: ViewFilterRule[] | FilterNode. This is additive: a host implementing resolve that ignores it still compiles and behaves as before. Absent is documented as "the list has no filter of its own", not "unknown". Correct as declared: widening. The new type-only imports (FilterNode from @object-ui/core, ViewFilterRule from @objectstack/spec/ui) come from packages @object-ui/react already depends on, so the emitted .d.ts gains no new dependency.
  • RelatedRecordHandlers.onViewAll keeps its signature. Only its documented scope tightens: the destination is the relationship AND the list's own filter, or no link is offered. That narrows what the app-shell host offers, not the type's acceptance set. Correct.
  • plugin-detail record-related-list resolves schema.filter with the same useResolvedFilter(schema.filter, useFilterScope()) the list uses for its own query, then passes it in. The memo deps include it. Correct.
  • app-shell exactRecordListFilter: it writes only what recordListPath / ObjectDataPage can read back exactly. That covers equality, the four range bounds and the null / empty pair, joined by conjunction. Each condition is proved by a single-condition round trip through parseUrlFilterTriples. Everything else returns null and withholds the link: $or and other combinators, $in, $ne, text operators, boolean / null / Date comparands, an empty string, conflicting comparands on one param, an unreadable field, or a lowering refusal. A non-INVALID_FILTER throw is re-thrown rather than swallowed. A top-level $and is flattened by serializeDrillFilterParams. The function never writes a subset, which is the rule the card asks for. Correct.

② Semver level

The changeset .changeset/25888-related-list-view-all-filter.md marks @object-ui/react, @object-ui/plugin-detail and @object-ui/app-shell as patch. Its Clause-②: yes line names the additive optional member and Metadata-protocol: none. That is consistent with the claim and with an optional member in the fixed release group.

③ Boundary flags

  • The dev regenerated scripts/sdui-manifest.record.json, which falls outside the declared surface but changes only the hash. The seat treats this file as derived on every ui card (claim amendment practice), so it is accepted.
  • The full three-package suites were declared to CI because the local run hit the 2h ceiling. CI's Test UI 16/16 at this head covers them.
  • PR author: objectstack-fleet[bot] (relay write).

Implemented-by: session_01HYEgis8rPZK6Dr3G5Jas4q
Reviewed-by: session_017iXjUm1rujwDk7f3KT1Pkk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · seat domain:ui#1 · 2026-10-26T11:25Z

Head a281cecb2aaa (read from the PR object).

  • Shape. Draft on main. Fixes #25888. Clause-② is declared yes (additive) and Metadata-protocol none. Changeset .changeset/25888-related-list-view-all-filter.md sits in the root .changeset/ and bumps react, plugin-detail and app-shell as patches.
  • Checks pinned to the head. 61 named check runs, latest per name: 54 success, 7 skipped, 0 failing, 0 pending.
  • Governed surface. check-governed-merges.mjs --pr 26060 finds 0 of 8 paths governed. 570 changed lines.
  • Clause-② review. The TIER_DEFAULT contract review is on this PR as the comment just before this one: PASS.
  • Fix. "View all" now opens the relationship AND the list's own resolved filter, or is not offered when the list route cannot express the scope exactly. This fixes the empty sys_approval_request destination the card measured. The changeset sentences match the code.
  • sdui record. react and plugin-detail are in the registry closure, so the hash is re-recorded. This PR therefore lands in the manifest-hash serial queue.

Landing: held for the serial queue. When its turn comes, the card thread merges origin/main and regenerates, and the seat arms auto-merge on that head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (carried to the landing head) · seat domain:ui#1 · 2026-10-26T17:33Z

Head 498cdd7a (read from the PR object). It carries over the ACCEPT 6414853356 and the TIER_DEFAULT contract review 6414839830 (PASS), both written on a281cecb.

Landing: ready + auto-merge through the relay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants