Repository navigation
fix(react,fields,charts,dashboard,report,list,core): surface filter refusals at render/click-time merges instead of throwing (objectui#10789) - #10830
Conversation
…ed-filter notice where render/click-time merges refused (objectui#10789) Render-time and click-time filter merges called the throwing converter form, so a malformed authored filter threw a FilterOperatorError out of render (or out of a click handler) instead of surfacing the refusal the way the sibling sites do. - ElementDataSourceGate / useElementDataSource: both merges (component filter with the composed binding; view filter with the binding's own) now report the refusal as `missing` + `filterRefusal`; the gate draws the malformed-filter notice the wrapped blocks draw (`view.malformedFilter`). - PeoplePicker recents and RecordPickerDialog mergedFilter lower through toFilterNodeSafely; the refusal lands in the picker's existing error state and no query runs without the filter. - ObjectChart, ObjectPivotTable, DatasetWidget and DatasetReportRenderer drills log the refusal and open/emit no drill (never a filter-less one). - ListView server export builds its filter inside its try, so the refusal is named in the export menu. - lowerLogicalGroup reads every $or member before the TRUE disjunct absorbs it, so a refused member is refused in either member order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…hed packages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…ctui#10789 drill pins Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Brings in objectui#10594 (the brief's merge condition), objectui#10788 (the converter's empty-operator-map tail) and objectui#10778 (the pivot's invalidation reader). One conflict, the pivot's import lines, resolved as the union: `useMemo` for this branch's drill composition, `useDataInvalidation` for objectui#10778. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
|
changeset-claim-re-read
|
✅ Console Performance Budget
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
Size Limits
|
…ter in check-i18n-dead-keys (objectui#10789) The malformed-filter panel objectui#10789 added to ElementDataSourceGate reads the `en` pack at render for a `t()` defaultValue, which makes the file a pack-object importer the header's enumeration must read by hand. Its reading: a class-4 read (two-segment key) that no leg sees and that the co-located `t()` literal keeps visible BY COINCIDENCE. The key itself is not at risk: the sibling malformed-filter states spell it as a literal `t()` argument too. The bullet is written first; the path joins ANALYSED_PACK_OBJECT_IMPORTERS after it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
Contract reviewServed-tier: Reviewed against the card objectui#10789 (body, triage, claim, dev report, claim amendment), the PR body and diff (19 files, merge-base ① Derived judgmentsAcceptance set — per site. Every touched site was judged on three questions: can a refused filter now reach an UNFILTERED query, drill, export or picker list; is anything beyond
Public face. CONFIRMED: Census. Every non-test caller of Census finding — the drill seam still throws for an authored filter (MEASURED). Pins. All eight files assert notice content (the refused operator in the headline or log line) or the envelope ( ② Semver level
③ Boundary flags
Defects, in fix order.
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…ct importer in check-i18n-dead-keys (objectui#10789)" This reverts commit cab1c7d; it was never pushed. The patch round takes the reviewer's option (b) instead: the gate's malformed-filter panel stops reading the `en` pack, so the file leaves the pack-object importer population and needs no bullet. An entry for a file outside the population would fail the gate's other direction, "EVERY analysed importer still exists". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
…p the gate's pack-object default (objectui#10789)
Contract review 5858758764 on the first head found two defects; this round
fixes both.
1. composeDrillFilter ends in the spec's parseFilterAST, which refuses comparand
shapes the converter passes through, e.g. an array-dialect widget filter
[['stage', 'in', 'won']] or { stage: { $in: 'won' } }. It threw a plain Error
with code INVALID_FILTER / status 400. The four drill sites catch
FilterOperatorError only, so the chart and pivot were still thrown into the
error boundary on a click. The seam now re-raises that refusal as a
FilterOperatorError with its message, so all four sites treat it exactly like
the converter's refusals: no drill opened or emitted, and a console.warn
naming the operator. Only INVALID_FILTER is re-raised; any other error
passes through.
2. ElementDataSourceGate's malformed-filter panel read the `en` pack object for
its defaultValue, which made the file an unclassified pack-object importer
(check-i18n-dead-keys). It now carries the sentence as a literal inline
default, as the sibling blocks carry theirs, held byte-identical to the pack
by check:i18n-keys. The gate pin now proves the text reaches the screen with
and without an I18nProvider.
The changeset's opening sentence is rewritten to hold at every site.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
✅ Console Performance Budget
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
Size Limits
|
…er isolate:false (objectui#10789) The `unit` project shares one module graph per worker, so a module-scope vi.mock of the spec only applied when no earlier file had loaded the seam. The pin now swaps the spec's lowering in a fresh module graph (vi.resetModules + vi.doMock + dynamic import) and undoes it in `finally`, so nothing leaks into later files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Delta review after the FAIL record (comment 5858758764, head ① Derived judgmentsDefect 1 (the Defect 2 (the spec's Pins for the new shape. New The ② Semver level
③ Boundary flags
Defects. None in the diff, the changeset or CI. One correction owed outside the diff: the PR body's three stale sentences named above. Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #10789
Clause-②: yes
The render-time and click-time filter merges called the THROWING converter form, so a malformed authored filter threw a
FilterOperatorErrorout of render (into the nearest error boundary) or out of a click handler, instead of surfacing the refusal the way the sibling sites already do. Every such site now keeps the refusal as a value and surfaces it on the channel its nearest sibling uses. A malformed filter stays refused: nothing refused becomes accepted, and no site falls back to "no filter".Dispatched by the
domain:uiseat #1 PM; implemented in sessionhttps://claude.ai/code/session_01DuWo5bdP9SdVebamn99GGk.Census (execution note 1)
Every caller of the throwing forms (
convertFiltersToAST,toFilterNode,mergeFilterNodes) and of the core utilities that wrap them (composeElementDataSource,composeDrillFilter,buildDatasetDrillFilter,buildEffectiveFilter), read onorigin/mainata14fb23b3. Cited by symbol, never by line (AGENTS.md #11).mainuseElementDataSourceSchema— componentfilterAND composed bindingmergeFilterNodestoFilterNodeSafely; refusal →status: 'missing'+filterRefusal;ElementDataSourceGatedraws the malformed-filter noticeuseElementDataSource→composeElementDataSource— view filter AND binding filtermergeFilterNodesElementDataSourceGate,element:record_picker)FilterOperatorErrorcaught at the hook →missing+filterRefusal; gate → notice; record picker → its own configuration-error panelPeoplePickerrecentFiltermergeFilterNodestoFilterNodeSafely(baseFilter); recents query disabled; refusal in the candidate area's error slotRecordPickerDialogmergedFilter(rule-array arm)mergeFilterNodestoFilterNodeSafely; query disabled; refusal in the dialog's error alertObjectChartdrillFiltercomposeDrillFilterconsole.warnnaming the operatorObjectPivotTablerenderDrillDrawercomposeDrillFilterconsole.warnDatasetWidgetopenDrillbuildDatasetDrillFilter→composeDrillFilterconsole.warnDatasetReportRendererrowdrilland matrixdrillCellbuildDatasetDrillFilter→composeDrillFilteronDrillNOT emitted (never withobjectFilterabsent);console.warnListViewhandleExport, server leg —buildEffectiveFilterran BEFORE the exporttrybuildEffectiveFilter→mergeFilterNodestry; refusal named in the export menuObjectGridschema.filter;RelatedListlist filter;LineItemsPanelfilter;ValueDataSource.findarray$filtertoFilterNodeSafelyListViewdata fetch;ListViewself-query filter (own catch);ObjectViewfinalFilter;ObjectGriddefaultFiltersleg;LookupFieldrecents;related-count-storefetchCount; data-objectstacktranslateFilterToAST(find, exportDownload) and the SDKfindleg;ViewDataProvider.resolveElementDataSource(async, no production caller)try/ asyncRelatedList/LineItemsPanelmergeFilterNodes(parentScope, listFilterNode)mergeFilterNodesDrillDownDrawerreport-drillmergedFilter$and)lowerLogicalGroup$orearly return (execution note 4)Design choice (brief, zone 2 item 2)
toFilterNodeSafelyper source, thenmergeFilterNodesover the lowered nodes — the idiomRelatedListandLineItemsPanelalready use. Re-lowering a lowered node is the identity (toFilterNodereturns an AST array orundefined), so a well-formed filter reaches the wire byte-for-byte as before.composeDrillFilterthrows one refusal type: the specparseFilterAST'sINVALID_FILTERis re-raised as aFilterOperatorErrorwith the same envelope, and any other error passes through untouched. The render/click caller catches ONLYFilterOperatorError— the ruletoFilterNodeSafelystates; anything else is rethrown.@object-ui/coregains nothing;@object-ui/reactgains one optional field,filterRefusal, on the two binding-hook result types (see Acceptance notes).view.malformedFilter, amberrole="alert", the refused operator in a separately addressable headline, the refusal's message beneath), with the gate'stestIdstem. Since patch round 1 its provider-less default is a literal inlinedefaultValue, held byte-identical to the pack bycheck:i18n-keys; the gate imports noenpack;missing;dataSource.findalready lands in; Retry hidden for a refusal (nothing was read);console.warn), the channelReportView's drill failure andValueDataSource's refused filter already use. Since patch round 1 this covers both refusal steps: the converter's, and the specparseFilterAST'sINVALID_FILTER, whichcomposeDrillFilterre-raises as aFilterOperatorError(any other error passes through untouched), so the four sites catch it with the catch they already had;Evidence
Nine new pin files: eight at round 1, one malformed filter per site plus a well-formed CONTROL per file; patch round 1 added the core drill-seam pin
drill-filter-spec-refusal-10789.test.ts, and a spec-refusal case in each drill-site pin. Assertions are the notice's content (the refused operator) or the refusal envelope (codeINVALID_FILTER,httpStatus400), plus the absence of any unfiltered read — never a baretoThrow.Red leg (one-shot, not left in the tree). The implementation was committed first (
9f1d0712e); the ten source files checked out at the basea14fb23b3(on-disk marker countobjectui#10789= 0 in each), new pins run, then restored withgit checkout HEAD --and proven bygit diff HEADempty plus a hash match of all ten blobs against HEAD:The 18 reds are the 18 site assertions (each fails with the
FilterOperatorErrorthe site threw, or with the notice absent); the 12 greens are the CONTROLs plus the member ordermainalready refused — they pass both ways by design.Local verification — at
664305cb3(this branch after mergingorigin/mainff94a12d2)Heavy runs went through the shared verify lock; each line quotes the lock's own verdict.
pnpm exec vitest run --maxWorkers=2 packages/core/ packages/react/plus the six other pin files →Test Files 285 passed (285),Tests 4931 passed (4931);VERDICT batch-last-exit 0(the batch's last part is the vitest run).Test Files 321 passed | 1 skipped (322),Tests 4722 passed | 7 skipped (4729);VERDICT command-exit 0.*elementDataSource*suite of calendar, detail, form ×4, gantt, grid, kanban, map, timeline; the consoleelement-data-source-input-injectionprobe; threeelement:record_pickersuites) →Test Files 258 passed (258),Tests 2565 passed (2565);VERDICT command-exit 0.packages/componentschanged no source, so only its record-picker suites ran.turbo run type-checkover core, react, fields, components, plugin-charts, plugin-dashboard, plugin-report, plugin-list with the^buildclosure →Tasks: 22 successful, 22 total;VERDICT command-exit 0.check-changeset-presence✅ (one changeset,.changeset/10789-render-time-filter-refusals.md,patchfor the seven packages whose shipped source changed);check-changeset-no-major✅.check:phantom-deps,check:self-import,check:element-data-source-declaration,check:esm-specifiers(specifier leg),check:i18n-keys,check:new-line-citations(0 new),check:test-path-roots,check-control-bytes,check-vi-mock-specifiers,check-vi-mock-inherit,check-vi-mock-override-shape.eslint --format jsonover the 18 files this branch changes (git diff --name-only HEAD^2 HEAD): 18 files, 0 errors; per source file and per rule, the warning count equals the base version of the same file (every delta zero; the new pin files carry warn-levelno-explicit-anyonly). Population:eslint.config.jsapplies to**/*.{ts,tsx}. Invariance: no type-aware linting is configured (eslint.config.jshas noparserOptions/projectService), so this diff cannot move a verdict on an untouched file. Repo-widepnpm lintis CI's.pnpm testandpnpm lint, thecheck:esm-specifiersload leg, the remainingcheck:*families.Acceptance notes
UseElementDataSourceResultandUseElementDataSourceSchemaResultgain an optionalfilterRefusal, and themissingstatus now also covers "a filter the binding combines is refused". Chosen over a new status value because every existing consumer already withholds its query onmissing— a consumer that has not learned the new field still cannot widen. The PM'sClause-②: norests on "no public face grows"; this additive optional field is flagged for that reading.lowerLogicalGroup(execution note 4) narrows, never widens:{ $or: [{}, { a: {} }] }is now refused like{ $or: [{ a: {} }, {}] }already was; a TRUE disjunct beside members that lower still absorbs (both orders pinned).element:record_pickershows its configuration-error panel (headline "data source could not be resolved") carrying the refusal's message, rather than the malformed-filter sentence: that panel is its own sibling state, and the gate's notice component is not exported.LookupField's recents effect catches every error into an empty rail (the main popover query still surfaces a refusal);DrillDownDrawer's report drill composes with a literal$andrather than the single sink. None measured through a public door; noted, not filed.RecordPickerDialog.tsxis shared with this seat's in-flight objectui#10594 (withoutDeniedFields); this diff touches only themergedFilterregion and the error block's Retry.Generated by Claude Code