Skip to content

fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist - #3081

Merged
os-zhuang merged 1 commit into
mainfrom
claude/filter-source-merge-normalization
Jul 31, 2026
Merged

fix(view,list,core): a view's filter no longer disappears, or arrives as a predicate on columns that don't exist#3081
os-zhuang merged 1 commit into
mainfrom
claude/filter-source-merge-normalization

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Swept the other $filter producers after #3078. ObjectView was the only other one merging two filter vocabularies, and it had two live defects. Everything else came back clean — the audit table is at the bottom.

1. An object filter was dropped — and only for non-grid views

ObjectView fetches its own data for calendar / kanban / gallery / timeline; grid delegates to ObjectGrid. The merge tested baseFilter.length > 0, which is undefined > 0 for an object — and table.defaultFilters is declared Record<string, any>.

So the filter vanished and the view returned every record. ObjectGrid assigns the same value straight to params.$filter and was unaffected — meaning one view definition filtered correctly as a grid and returned everything as a calendar.

2. Rule objects were spread into the and, not wrapped

['and', ...baseFilter, ...userFilter] is only correct when the source is an array of AST nodes. activeView.filter is a spec ViewFilterRule[], so spreading put bare rule objects where the AST expects nodes. Measured against the spec's own functions:

isFilterAST(['and', {field:'stage',operator:'eq',value:'won'}, ['owner','=','me']])
// false  → 400 since objectstack#4121

parseFilterAST(same)
// {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}

That second line is a predicate over three columns named field, operator, value — which don't exist. Reachable whenever a view with a filter meets a user filter value. ['and', ...x] survived because it is right when x is an array of nodes.

The fix

New in @object-ui/core: toFilterNode normalizes one source (rule array / AST / MongoDB object), mergeFilterNodes combines sources as siblings under one and. ObjectView and ListView.buildEffectiveFilter both use them, so the three shapes are reconciled in one place rather than by hand at each renderer.

ObjectStackAdapter also translates a bare rule object sitting directly under a logical node — the chokepoint defence for any producer still emitting the spread shape. Only rule-shaped objects are touched; a child with no field is a genuine MongoDB condition ({status:'active'}) and passes through untouched.

Correcting a comment I shipped in #3078

buildEffectiveFilter documented the dropped-object case as unreachable — "nothing in this repo produces one for a list view". That was wrong. ObjectView:1010 passes mergedFilters straight into that schema's filter, and its last fallback is table.defaultFilters. The case is now handled instead of explained away, and a test pins it.

The rest of the sweep — no action needed

producer verdict
dashboard metric / chart / pivot / data-table one resolved filter, no cross-vocabulary merge
gallery, gantt, calendar, map forward schema.filter unchanged
ObjectGrid assigns defaultFilters directly — never had the .length bug
filter-converter's ['and', ...conditions] spreads tuples it built itself — correct
DashboardFilterBar, lookup/field widgets, app-shell reads single-source $filter objects

Verification

19 tests across the four packages. Reverting each source file fails the tests covering it (adapter 2, ObjectView 1, ListView 2, plus the core merge suite). Emitted filters are asserted against the spec's own isFilterAST / parseFilterAST rather than restated literals, including an executable pin on what the old spread shape produced — so the fix can't be undone by someone who finds the wrapping verbose.

Full suite 761 files / 8856 tests green; tsc clean across core, data-objectstack, plugin-list, plugin-view; eslint 0 errors.

Refs #3078, #3072, objectstack#4121

🤖 Generated with Claude Code

… as a predicate on columns that don't exist

Sweeping the other `$filter` producers after #3078 turned up two live defects in
`ObjectView`, which fetches its own data for calendar / kanban / gallery /
timeline (grid delegates to `ObjectGrid`).

1. AN OBJECT FILTER WAS DROPPED, AND ONLY FOR NON-GRID VIEWS.
`table.defaultFilters` is declared `Record<string, any>`, and the merge tested
`baseFilter.length > 0` — `undefined > 0` for an object. The filter vanished and
the view returned EVERY RECORD. `ObjectGrid` assigns the same value straight to
`params.$filter`, so one view definition filtered correctly as a grid and
returned everything as a calendar.

2. RULE OBJECTS WERE SPREAD INTO THE `and`, NOT WRAPPED.
`['and', ...baseFilter, ...userFilter]` is only correct when the source is an
array of AST nodes. `activeView.filter` is a spec `ViewFilterRule[]`, so
spreading put bare rule objects where the AST expects nodes:

    isFilterAST(['and', {field:'stage',operator:'eq',value:'won'}, ['owner','=','me']])
    // false → 400 since objectstack#4121
    parseFilterAST(same)
    // {$and:[{field:'stage',operator:'eq',value:'won'}, {owner:'me'}]}

The second line is a predicate over three columns named `field`, `operator` and
`value` — which do not exist. Reachable whenever a view with a filter meets a
user filter value.

New in core: `toFilterNode` normalizes one source (rule array / AST / MongoDB
object) and `mergeFilterNodes` combines sources as siblings under one `and`.
`ObjectView` and `ListView.buildEffectiveFilter` both use them, so the three
filter shapes are reconciled in one place instead of by hand at each renderer.

The adapter also now translates a bare rule object sitting directly under a
logical node — the chokepoint defence for any producer still emitting the spread
shape. Only rule-SHAPED objects are touched; a child with no `field` is a
genuine MongoDB condition and passes through untouched.

CORRECTING A COMMENT SHIPPED IN #3078. `buildEffectiveFilter` documented the
dropped-object case as unreachable, "nothing in this repo produces one for a
list view". That was wrong: `ObjectView` passes `mergedFilters` straight into
that schema's `filter`, and its last fallback is `table.defaultFilters`. The
case is now handled rather than explained away.

Also checked and clean: the dashboard widgets (metric/chart/pivot/data-table)
pass a single resolved filter with no cross-vocabulary merge; gallery, gantt,
calendar and map forward `schema.filter` unchanged; `filter-converter`'s own
`['and', ...conditions]` spreads tuples it built itself. `ObjectView` was the
only other producer merging two vocabularies.

Verification: 19 tests across the four packages; reverting each source file
fails the ones covering it. Emitted filters are asserted against the spec's own
`isFilterAST` / `parseFilterAST`, including an executable pin on what the old
spread shape produced. Full suite 761 files / 8856 tests green; tsc clean;
eslint 0 errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 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 Jul 31, 2026 12:40am

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.26KB 2.99KB
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.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
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) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 471.25KB 102.82KB
core (index.js) 2.16KB 0.78KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.10KB 34.56KB
fields (index.js) 222.07KB 54.35KB
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.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 25.17KB 5.80KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.45KB 10.67KB
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.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.76KB 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 (retry.js) 3.48KB 1.61KB
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.90KB 12.35KB
plugin-charts (index.js) 60.52KB 17.11KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.59KB 28.74KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 221.81KB 54.28KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 110.71KB 26.67KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 182.21KB 48.24KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 103.85KB 24.80KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.32KB 10.53KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.36KB 2.81KB
plugin-view (index.js) 85.79KB 20.99KB
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
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 3.47KB 1.54KB
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 (index.js) 2.07KB 0.99KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
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) 1.08KB 0.64KB
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

@os-zhuang
os-zhuang merged commit c4d7b20 into main Jul 31, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/filter-source-merge-normalization branch July 31, 2026 00:45
@os-zhuang

Copy link
Copy Markdown
Contributor Author

Correction to this PR's second finding.

I reported two ObjectView defects here. Only the first was live:

  • Object table.defaultFilters dropped — real. baseFilter.length > 0 is undefined > 0 for an object, and that was the always-taken path.
  • Rule objects spread into the and — I wrote that this was "reachable whenever a view with a filter meets a user filter value." That is wrong. The branch required a non-empty userFilter, and setFilterValues is never called anywhere in the component — so filterValues is permanently {} and the branch could not run.

The shape is genuinely broken (a live server answers ['and', {rule}, [tuple]] with 400 INVALID_FILTER — measured against a real backend), and the adapter-level defence added here is still warranted for any producer that emits it. But this particular site was dead code, not a live defect, and I described it as live.

The changeset is unreleased, so #3087 amends it rather than letting the claim ship in release notes, and removes the dead filter/sort machinery that made the misreading possible.

os-zhuang added a commit that referenced this pull request Jul 31, 2026
…connected (#3087)

`ObjectView` carried its own filter and sort bar: `filterValues` / `sortConfig`
state, a `filter-ui` schema and a `sort-ui` schema, ~80 lines of field
introspection to build them. None of it was wired. No setter was ever called and
neither schema was ever rendered — both states sat at their initial empty value
for the component's entire life.

Removed rather than wired, because the real filter and sort UI belongs to the
renderer this component delegates to. `showFilters`, `showSort` and
`filterableFields` are forwarded downstream and `ListView` implements them for
real (its own filter panel, and a `filterableFields` whitelist). Connecting the
local copy would have produced a SECOND filter bar competing with that one.

The dead state was not inert, though — it left a branch in every merge path that
could never run, and those branches read as live code:

- the fetch path merged `baseFilter` with a `userFilter` that was always `[]`;
- `mergedFilters` (what the `renderListView` slot receives, used by the Studio
  design surface) opened with a branch that REPLACED the view's filter with the
  user's instead of combining them — a real bug, had the state ever been written.

CORRECTION TO #3081. That PR reported two ObjectView defects. Only the first was
live: the object `table.defaultFilters` drop sat on the always-taken path. The
second — rule objects spread into the `and` — required a non-empty user filter,
so it could not run here. The shape is genuinely broken (a live server answers
it with a 400, measured) and the adapter-level defence added alongside is still
warranted for any producer that emits it, but that site was dead code, not a
live defect. #3081's changeset is unreleased and now carries the correction.

Keeping code that looks live and cannot run is what made that misreading
possible — twice in one session — which is the argument for deleting it rather
than leaving it for the next reader.

No behaviour change: every removed branch was unreachable. Net -85 lines. The
surviving paths are pinned by 4 new tests covering what the component hands the
delegated renderer, alongside the 5 from #3081 covering what it queries with.
Full suite 763 files / 8915 tests green; tsc clean; eslint 0 errors.

Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant