Skip to content

fix(plugin-list): a held user filter does not narrow the fetch on a dataset-bound chart (objectui#10512) - #10597

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10512-dataset-chart-applied-filter
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-10512-dataset-chart-applied-filter

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10512

Clause-②: no

What changes

objectui#10327 withholds the toolbar Filter builder and the UserFilters chips while a dataset-bound chart is on screen (datasetChartOnScreen), but only the controls. A group the host restored at mount (initialFilters), or one set on a grid before an in-list switch, still reached ListView's own fetch, and so the record-count bar. This PR withholds the APPLIED user filter too, in the shape objectui#10326 gave Search (searchTerm derived empty beside the held searchInput), as the triage ruled (comment 5829668693).

  • appliedFilters / appliedUserFilterConditions are derived beside the held currentFilters / userFilterConditions: empty while datasetChartOnScreen, the held value otherwise.
  • The held state is not touched: a switch back to a view that offers the controls applies it again, and onFilterChange is not called for the switch.
  • The view's own schema.filter is not the user's filter and stays in the query.
  • Each held source is swapped for a module constant only when it holds something, so a switch into the dataset chart with nothing held hands the fetch effect the identities it already had and re-issues no query (the objectui#7394 class).
  • ⛔ The toolbar gate (showFilters / showUserFilters) from objectui#10327 is unchanged: no diff line touches either flag.

Readers of the user filter

Switched to the applied value:

  • the fetch: buildEffectiveFilter in the fetch effect, and the effect's dependency list;
  • the server export: buildEffectiveFilter in the export callback, and its dependency list (the client export writes data, the fetch's own answer);
  • pageResetSignature;
  • selfQueryFilter, with no behaviour change: its consumers (the gantt, tree and object-bound chart nodes) are views on which datasetChartOnScreen is false, so applied equals held there, and the dataset node carries no filter (objectui#10327). Switched so that its comment, "the SAME effective filter this component's own fetch sends", stays literally true;
  • the grid empty-state hasActiveQuery, with no behaviour change (grid only, where applied equals held).

Left on the held state, deliberately:

  • the Filter builder's value, and its trigger's hasFilters highlight and badge count: the control that shows and edits the held group, not rendered on the dataset chart;
  • UserFilters, which keeps its own selection state and is not rendered on the dataset chart;
  • the host write-back (onFilterChange, onUserFilterSelectionsChange), fired only by the controls, so the host's storage keeps the held state.

H0, measured first (objectui origin/main a506001)

A component-level probe (not committed): the real ListView, an object-chart stand-in that records the node it is handed, a dataset-bound chart block, and initialFilters restoring status equals open over three rows. Readings on the base:

  • the chart node was the dataset shape (dataset: task_ds, no filter key);
  • ListView's fetch ran, 1 call, with $filter = ["status","=","open"];
  • the record-count bar rendered and read 1 record (3 rows unnarrowed);
  • neither the Filter builder nor the chips were on screen.

The fetch runs AND the count bar draws, so the narrowing is user-visible and the fix lands. ⛔ Not rendered in a browser: the count bar is ListView's own DOM, independent of the chart renderer.

Pins

ListView.datasetChartAppliedFilter-10512.test.tsx in packages/plugin-list/src/__tests__, 6 cases. Each asserts the query the data source receives (the fake answers from the fields its $filter names, so the count bar reads a different number for a narrowed query):

  1. CONTROL: on a grid, a restored group narrows the fetch, and the count bar reads 1 record.
  2. CONTROL: on a chart bound to the list object, a restored group still narrows the fetch.
  3. On a dataset-bound chart the fetch runs and the count bar draws, and a restored group narrows neither: no $filter in any call, the bar reads 3 records, onFilterChange not called.
  4. The view's own filter stays in the query on the dataset chart, alone: every call's $filter equals the base filter, the bar reads 2 records.
  5. A group restored on a grid plus a chip toggled there stop narrowing the fetch on a switch to the dataset chart, and the group applies again back on the grid; the builder badge still reads 1 and onFilterChange is never called.
  6. CONTROL (objectui#7394): with nothing held, the switch to the dataset chart re-issues no query.

Reverse verification (from the committed fix)

ListView.tsx checked out at the base a506001 (on-disk blob 03798ad equal to the base blob; 0 occurrences of appliedFilters on disk), the pin run, then restored with git checkout HEAD -- under a trap (restored blob d86a5db equal to the HEAD blob, git diff HEAD 0 bytes):

  • base leg: exit 1, Tests 3 failed | 3 passed (6); red are cases 3, 4 and 5, green are the three controls;
  • fix leg: exit 0, Tests 6 passed (6).

Ablation (the objectui#7394 control can fail)

Predicted before running: an unconditional swap of the Filter group turns case 6 red and nothing else. Run through ablation-replace.mjs (anchor datasetChartOnScreen && currentFilters.conditions && currentFilters.conditions.length > 0, 1 hit, anchor count 1 to 0, blob d86a5db to d364171): exit 1, Tests 1 failed | 5 passed (6), the failed case is case 6. Restored: blob equal to HEAD, git diff HEAD empty. Observed direction matches the prediction.

Verification (final head ad119b4, after merging origin/main f5178a2)

  • pnpm exec vitest run packages/plugin-list/ --maxWorkers=2: exit 0, Test Files 94 passed (94), Tests 1131 passed (1131).
  • pnpm turbo run type-check --filter @object-ui/plugin-list --concurrency=2: exit 0, Tasks: 14 successful, 14 total, plugin-list a cache miss; tsconfig.test.json lists the new pin (--listFilesOnly, 1 hit).
  • pnpm exec eslint on ListView.tsx and the new pin: exit 0, 0 errors; per-rule warning counts on ListView.tsx equal to the base file's (compared in one --format json run).
  • pnpm check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:new-line-citations (0 new citations), check:changeset-claims, check:pending-changeset-literals, check:shell-escape-residue, check:handler-key-reads, check:i18n-keys, check:unreferenced-sources: each exit 0.
  • node scripts/check-changeset-presence.mjs, check-changeset-no-major.mjs, check-type-check-coverage.mjs, check-lint-coverage.mjs: each exit 0.
  • ⛔ The repo-wide pnpm lint and the full pnpm test are CI's.

check:changeset-claims names 7 pending changesets that mention ListView.tsx (7070-no-invented-gantt-date-fields, 7499-gantt-non-axis-floors-omitted, 7773-kanban-adapter-groupfield-write, 7780-object-kanban-record-source, 8990-object-kanban-groupby-optional, list-user-actions-collision-5398, listview-comment-pair-4559). Each paragraph was read; none is about how the user filter is applied, and none is made false by this diff.

Changeset

.changeset/10512-dataset-chart-applied-filter.md, patch for @object-ui/plugin-list.

Acceptance notes

  • Chip state across an in-list switch (pre-existing, not changed here). Measured on the base a506001 with the same probe harness: toggle a UserFilters chip ON on a grid, switch to the dataset chart, switch back. The chip renders OFF again (its button carries the outline variant), while ListView's held userFilterConditions still sends ["priority","!=",null] and the count bar reads 2 records. UserFilters keeps its selection in its own state, and the objectui#10327 gate unmounts it on the dataset chart. This PR leaves the grid leg exactly as it was (applied equals held there); the pin deliberately does not assert the chip's return. Reported to the seat as a separate finding. Dropdown and tab chips were not measured.
  • An unfinished builder row. The swap keys on the held group's row count, not on its compiled filter (compiling at render could throw a FilterOperatorError). A held group whose every row is unfinished therefore still swaps on a switch into the dataset chart, and that one switch re-issues a query equal to the one before it.

The session that implemented this: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy (domain:ui seat 5 dispatch).


Generated by Claude Code

…ataset-bound chart (objectui#10512)

objectui#10327 withholds the toolbar Filter builder and the UserFilters chips
while a dataset-bound chart is on screen, but only the controls: a group the
host restored at mount, or one set on a grid before a switch, still reached
ListView's own fetch and so narrowed the record-count bar with nothing on
screen to show or clear it.

The applied user filter (appliedFilters / appliedUserFilterConditions) is now
derived empty while datasetChartOnScreen, beside the held state, as searchTerm
is beside searchInput (objectui#10326). The fetch, the export, the page-reset
signature, selfQueryFilter and the empty-state copy read the applied values;
the builder, its badge, the chips and the host write-back keep the held state,
so a switch back applies it again. schema.filter stays applied. Each source is
swapped only when it holds something, so a switch with nothing held re-issues
no query (objectui#7394).

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
Also notes, in the switch pin, why the chip's own return is not asserted.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#10512)

Keeps the derivation from adding a no-explicit-any finding.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 7 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7070-no-invented-gantt-date-fields.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7773-kanban-adapter-groupfield-write.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Who is NOT affected — the boundary is node-local. Every VIEW-LEVEL groupField read is untouched and still live: it is a legacy alias of the spec's groupByField on the kanban view config, mapped by normalize-list-view.ts, and both adapters still resolve lanes through it (ObjectView.tsx's kanbanCfg.groupField ||, ListView.tsx's groupByField || groupField). Authoring options.kanban.groupField on a list-view or object-view keeps working exactly as documented in packages/plugin-list/README.md. groupField is dead only on the generated object-kanban NODE.

.changeset/7780-object-kanban-record-source.md

.changeset/8990-object-kanban-groupby-optional.md

  • names packages/plugin-list/src/ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    • packages/plugin-list/src/ListView.tsx generates the node as groupBy: laneField. objectDef loads asynchronously, so laneField is undefined on every load until it lands, and stays undefined whenever the object offers no stageField hint and none of status / stage / state / phase. The renderer serves that node; both published faces refused it. - content/docs/utilities/data-objectstack.mdx documents an object-kanban node that is exactly { type, dataSource }, with no groupBy. ⚠️ This one is weaker and is cited for what it is: that fragment is still refused after this change, at RECORD_SOURCE_REQUIRED, because dataSource is not a rung of the record-source ladder. It shows a lane-less board is a documented authoring; it is not a document this change admits.

.changeset/list-user-actions-collision-5398.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    The harvest now reads the object block only. Both userActions read sites in ListView.tsx carry a comment naming the collision, and __tests__/ListView.userActionsCollision.test.tsx pins each clause of it: the two shapes, a producer that manufactures the view one, the harvest's blindness to it, and the projection that must keep the object's operand with a toolbar block — or an empty block — present on the view.

.changeset/listview-comment-pair-4559.md

  • names ListView.tsx → packages/plugin-list/src/ListView.tsx — edited by this change

    Two comment corrections in ListView.tsx (objectui#4559, objectui#4966). No runtime behaviour changes and the emitted bundle is byte-identical; the published .d.ts does change, which is why this is a patch rather than an empty frontmatter.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 50e41f738 (merge-base with origin/main): 2 file(s) changed outside .changeset/, read against 1463 pending declaration(s) that publish a body (2045 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3047.3 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-CY8DH9gO.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
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.68KB 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) 546.32KB 130.64KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 258.79KB 65.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.59KB 61.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.60KB 28.31KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
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.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ad119b4c346d98b4d39dc7a1555e4324b105d924

Refs: refs/review/pr-10597 = ad119b4c346d98b4d39dc7a1555e4324b105d924 (equals the PR's reported head). refs/review/main = 21d34d5e2eff7d41a52ff03e3703cebddf83c2d2. Merge-base f5178a2729959814264d41ac96130c83bbef4097. Diff merge-base to head: 3 files, +309 / -8, 4 commits (fix e41d5cd, changeset 7ee0250, typing 61e16e5, merge of origin/main f5178a2 as ad119b4). Every line below was read with git show refs/review/pr-10597:PATH; the shared checkout was not entered or edited. The os-dev-report comment was not used as evidence.

① Derived judgments

  • (a) The fix, in the ruled shape. Triage 5829668693 binds: 「measure first whether the count bar draws on a dataset chart; then objectui#10326's shape (derive the applied filter as empty while withheld, keep the held state, re-apply on switch back).」 At head packages/plugin-list/src/ListView.tsx L1515-1522 derives appliedFilters (the module constant WITHHELD_FILTER_GROUP, L691, while datasetChartOnScreen and the held group has at least one row; otherwise the held currentFilters itself) and appliedUserFilterConditions (WITHHELD_USER_FILTER_CONDITIONS, L692, while datasetChartOnScreen and the held chip array is non-empty; otherwise the held userFilterConditions itself). datasetChartOnScreen (L1171-1172) is unchanged and is the same primitive objectui#10477 defined. This is the searchTerm shape of objectui#10326 (L1251, searchlessViewOnScreen ? '' : searchInput) applied to the Filter half: the held state is a separate value, the applied value is derived. Right against the card and the triage.
  • (b) Readers switched to the applied value. The fetch's buildEffectiveFilter call (L2190) and the fetch effect's dependency list (L2699); the server export's buildEffectiveFilter (L4000) and its dependency list (L4102); pageResetSignature (L2710); selfQueryFilter (L3078); the grid empty-state hasActiveQuery (L4969-4970). Judged one by one: the fetch is the reader the card names and is the fix; the export mirrors the fetch by the file's own rule (L3998 「The same three filter sources as the data fetch」, and the client export writes data, L4053, which is the fetch's answer); pageResetSignature and hasActiveQuery follow the 10326 shape, where the same two readers were moved to the derived term. selfQueryFilter is behaviour-neutral: its consumers are the gantt node (L3445), the tree node (L3569) and the object-bound chart node (L3633), on all of which datasetChartOnScreen is false so applied equals held by identity, and the dataset chart node (L3600-3612) carries no filter at all. Nothing not asked was done; the reader switches are the derivation being read, not a second change.
  • (c) Held state kept, host write-back untouched. The only writers of the held state are setCurrentFilters inside the FilterBuilder's onChange (L4366-4369, which is also the only caller of onFilterChange) and setUserFilterConditions handed to UserFilters (L4217). No effect touches either on a view change, so a switch into or out of the dataset chart neither clears the group nor tells the host to. The builder's value (L4365), the trigger's hasFilters highlight and badge (L3654, L4346-4353) and UserFilters (L4212-4221) still read the held state, which is right: none of them renders while the dataset chart is on screen (the L1228-1229 gate), and on a switch back they show the group that is applied again. Matches the triage's 「keep the held state, re-apply on switch back」.
  • (d) The toolbar gate is untouched. The showFilters / showUserFilters lines (L1228-1229) and the toolbarFlags memo are not in the diff, as the claim's ⛔ required. schema.filter (the view's own filter) is still the first argument at every buildEffectiveFilter site, so the authored scope is not withheld: right, since objectui#10327's ruling concerns the user's controls and ListViewSchema already refuses an authored filter on a dataset chart.
  • (e) The "measure first" gate, verified at source. The fetch effect (L2120 onward) is gated only by ganttOwnsData (L2163) and objectDefLoaded (L2171), so it runs on a chart view. The record-count bar (L5139) renders when surfaceDrawsFetchedRows (L2118, currentView !== 'gantt') and rows are present, so it draws on a dataset chart and reads serverTotal ?? data.length (L5150). Both halves hold, so the finding does not reduce to wasted narrowing and the fix is owed. The pin's case 3 (test L172-182) asserts the same two facts before asserting the fix, so the measurement is pinned, not only reported.
  • (f) No re-query with nothing held (objectui#7394 class). Each source is swapped only when it holds something (L1516, L1520). With nothing held both applied values keep the identity of the state they were derived from, so the fetch effect, which lists them (L2699) and not currentView (its L2663 comment says so by design), does not re-run on the switch. Pinned as case 6 (test L231-245). With something held, the identity flips to the module constant and back, which is exactly the two re-fetches the card asks for.
  • (g) Existing pins. The diff edits or deletes no existing test. The sibling pins ListView.datasetChartToolbarFilter-10327.test.tsx and ListView.searchWithheldTreeChart-10326.test.tsx are untouched, and CI's eight test shards are green at the head.
  • (h) New pins can fail. packages/plugin-list/src/__tests__/ListView.datasetChartAppliedFilter-10512.test.tsx, 6 cases; the fake data source (L93-110) answers each query from the fields its $filter names, so the count bar reads a different number for a narrowed query. Lit controls: case 1 (L156, grid, restored group narrows, bar reads 1 record), case 2 (L163, object-bound chart, restored group narrows) and case 6 (L231, no re-query) pass at base and at head, and each is sensitive: case 2 goes red if the withhold keys on currentView === 'chart' alone, case 1 if it keys on everything, case 6 if the swap is unconditional (the identity flip re-runs the fetch effect, which lists appliedFilters). Negative legs, by reading against the base file: case 3 (L172) asserts every host $filter is undefined, and the base fetch sends ["status","=","open"]; case 4 (L184) asserts every $filter equals the base filter alone, and the base sends the base filter combined with the user's; case 5 (L196) waits for a chart fetch whose $filter is undefined, and at base the switch re-issues no query, so the last call still names status and the wait times out. Not vacuous: the every assertions at L179 and L192 follow a toHaveBeenCalled wait (L177, L190), so the call list is non-empty; every case first awaits its view's stand-in. Case 5 also pins the re-application on the switch back (L222) and that onFilterChange was never called (L228). I did not run the suite (git-only rule); the head's CI is the independent signal.
  • (i) Nothing asked is missing. Measure first: done and pinned (e). Fix shape: done (a). Pin that a restored filter does not narrow the fetch and the count on a dataset chart and does again on grid: cases 3, 5 and 1. One .changeset/10512-…md: present. File surface equals the claim: ListView.tsx, one pin beside the two sibling pins, one changeset.
  • (j) Source prose added by the diff, checked. The docblock addition at L1156-1163 (「Two readers, one answer: the toolbar flags below, and the APPLIED user filter … The held state itself is kept … schema.filter … stays applied」) and the derivation comment at L1508-1514 are each true of the code beside them. The L686-690 comment on the constants (「every render hands the fetch effect the same identity」) is true. No README or content/docs prose is added, and the card asked for none.
  • (k) One observation, not a defect. The derivation reads userFilterConditions.length unguarded (L1520) where the older empty-state reader guarded with Array.isArray (L4969). The state initialises to [] (L1506) and UserFilters' contract is (filters: any[]) (UserFilters.tsx L94), with every emission site passing an array (L470, L826, L874, L899, L1050), so there is no throwing path.

② Semver level

.changeset/10512-dataset-chart-applied-filter.md declares '@object-ui/plugin-list': patch. objectui AGENTS.md 版本号策略 at head: 「minor/patch 独立演进 —— objectstack 没动时不必跟发;objectui 自己的改动照常用 changeset 推进」 and 「changeset 里不要声明 major …… objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)」. The rule forbids major and reserves minor for objectui's own breaking changes. This diff exports nothing new (the two WITHHELD_* constants at L691-692 are module-private), touches no type, schema or accept set, and narrows no published face; it is a bug-card fix inside one component. Patch is the honest level, and it matches the precedent of objectui#10326's 10326-toolbar-search-withheld-tree-chart.md (plugin-list: patch) for the same shape. Changeset Bump Policy, Changeset Declaration and Changeset Fixed Group Check are green at the head. Not a docs-only change, so an empty-frontmatter changeset is not the case here.

Changeset prose, sentence by sentence against the diff:

  • 「On a chart list view bound to a semantic dataset, a user filter that is already set no longer narrows ListView's own fetch (objectui#10512).」 True: L1515-1522 and L2190.
  • 「The toolbar withholds the Filter builder and the userFilters chips on such a view (objectui#10327), but a filter group the host restored at mount through initialFilters, or a group or chip set on another view before switching, was still applied to ListView's own fetch of the object's rows.」 True: L1228-1229 gate; the base file's fetch read currentFilters and userFilterConditions directly (base L2157).
  • 「That fetch still runs on the chart and feeds the record-count bar, so the count was narrowed by a filter nothing on screen showed or could clear.」 True: (e) above.
  • 「While a dataset-bound chart is on screen, ListView's fetch and its export apply the view's own filter only; the toolbar's filter group and the chips are not sent.」 True: L2190, L4000, and the client export writes data (L4053).
  • 「The held state is kept rather than cleared: switching back to a view that offers the controls applies the same group again, and onFilterChange is not called for the switch.」 True: (c) above; pinned at test L222-228.
  • 「A chart bound to the list object, and every other view, apply the toolbar's filter group and the chips as before.」 True: datasetChartOnScreen is false there, so both applied values are the held state by identity; pinned as cases 1 and 2.
  • 「A switch into a dataset chart with no user filter set issues no new request.」 True: (f) above; pinned as case 6.

Pending changesets naming ListView.tsx at head were re-read for falsification by this diff: 10327-dataset-chart-view-filter.md (toolbar only, still true), 10326-toolbar-search-withheld-tree-chart.md (Search only, still true), 10250-self-querying-views-toolbar-state.md (its last paragraph, 「the toolbar state still does not reach the query of a chart view bound to a semantic dataset」, is still true: the dataset node still carries no filter), 7394-listview-view-switch-refetch.md (the no-re-query invariant, kept by (f)). None is made false.

③ Boundary flags

  • The PR body's own flags. 「Not rendered in a browser」: acceptable; the count bar is ListView's own DOM (L5139-5153) and the pin reads it by testid. Acceptance note 1 (a UserFilters chip renders OFF after a switch back while its held condition still narrows the grid): confirmed pre-existing at source, ToggleFilters keeps activeToggles in its own state (UserFilters.tsx L1033 onward), the L4212 gate unmounts it on the dataset chart, and ListView's userFilterConditions (L1506) survives; this diff leaves the grid leg identical, and the pin says at L218-226 why the chip's return is not asserted. Outside this card's surface; correctly disclosed, not fixed here. Acceptance note 2 (a held group whose every row is unfinished still swaps and re-issues one equal query): accurate to L1516, which keys on the row count and not on the compiled filter; compiling at render would have to handle the FilterOperatorError that buildEffectiveFilter can throw (L3086-3090), so the choice is defensible and the cost is one equal query in an edge case, not a narrowing. No open questions are listed in the body.
  • CI at the head. 43 check-runs on ad119b4c346d98b4d39dc7a1555e4324b105d924: 40 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard …/4), the same three the merged PRs objectui#10477 and objectui#10510 skipped), 0 failure, 0 in progress. No re-poll was needed.
  • Draft. Yes, the PR is a draft (draft: true), mergeable: true, mergeable_state: clean, labels plugin and tests.
  • Clause-② no holds. No export line of ListView.tsx is in the diff; the new constants are not exported; ListViewProps, ListViewHandle, buildEffectiveFilter's signature and @object-ui/types are untouched; no accept set moves. Governed Surface Queue Guard and Spec Main Shape Gate are green.
  • Main under the PR's files. refs/review/main is 7 commits past the merge-base f5178a2; git log f5178a2..refs/review/main over the three PR paths is empty, and ListView.tsx is blob 03798ad at both the merge-base and main's head, so nothing moved under the diff. Main holds neither the changeset nor the pin.
  • Closing keyword and identity. Fixes #10512 is the only closing keyword. Commit messages carry a session URL and a product co-author line; no model identifier appears in the title, body or commits.

Implemented-by: claude/issue-10512-dataset-chart-applied-filter
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

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

Projects

None yet

2 participants