Repository navigation
fix(plugin-list): a URL-restored quick filter applies when dropdown chips derive from the object definition (objectui#12001) - #12006
Merged
objectstack-fleet[bot] merged 3 commits intoOct 9, 2026
Conversation
…d chips arrive (objectui#12001) A list whose dropdown chips derive from the object definition mounts DropdownFilters with no fields, because the definition loads after the list. The restored selection (initialSelections, the host's uf_* URL params) was read only in the useState initializer, so it had nothing to attach to and a shared `?uf_status=open` link opened unfiltered. A field that appears after mount now gets the starting selection the mount would have given it (author default, then the restored value, coerced to the option types, single-choice clamped), exactly once per field name, and the bar emits the conditions through onFilterChange. Settling each name once keeps a later rebuild of the field list from overwriting a value the user has since chosen or cleared. The mount's starting-value and coercion rules move into two helpers both paths call. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…lter chips (objectui#12001) Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
❌ 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. Which half objected:
📦 Bundle Size Report
Size Limits
|
This was referenced Oct 8, 2026
Brings in 4babf40 (objectui#11982, the re-pinned first-load ceiling) and the rest of main since 5c7d753. No conflicts. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ 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
|
objectstack-fleet
Bot
deleted the
claude/issue-12001-restored-derived-chips
branch
October 9, 2026 01:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12001
Clause-②: no
What was wrong
userFilters: { element: 'dropdown' }with nofieldsis the shorthandListViewfills from the object definition (itsresolvedUserFiltersmemo). The definition is fetched after the list mounts, soDropdownFiltersfirst renders with no fields. It read the restored selection (initialSelections, whichObjectView,ObjectDataPageandInterfaceListPagefill fromuf_*URL params since objectui#11915) only in itsuseStateinitializer, against that empty field list. So a link such as?uf_status=openopened unfiltered while the address bar still carried the filter.Premise measured on
mainb1f0dcc00before any source edit: the new suite's DERIVED, list-level ONCE PER FIELD and ARRIVAL cases were red against the unmodified source, and its CONTROL and TABS cases were green.The change (
packages/plugin-list/src/UserFilters.tsx,DropdownFiltersonly)defaultValues, then the restored value, coerced to the option value types, with a single-choice field kept to one value. The chip shows it, and the bar emits the conditions throughonFilterChange, so the query carries it.fieldsarray, whichListViewrebuilds on every definition render (AGENTS.md rule 10, never key on a memo's identity).startingSelections(the initializer's body, unchanged) andcoerceToOptionTypes(the mount effect's coercion loop, unchanged; it returns the same object when nothing moved).onSelectionsChangeis not called for an adopted selection, the same as a restore at mount today: the selection came from the host's URL, so there is nothing to echo back. Measured with a one-off probe: 0 calls on the declared path and on the derived path. This departs from the dispatch's suggested route ("then notify as a user selection would"); parity with the mount path is what it keeps.fieldssettles them all at mount, so nothing is adopted there. Behaviour changes only if a declared list GROWS while mounted (a live config edit; a view switch remounts the list since objectui#11992): the new field now gets its default or restored value instead of nothing.No export, prop, type or language-pack key changes. The new helpers and the selection type are not exported.
The other two modes, measured
tabs: not affected, not changed. Presets come from the view config, andListView's derivation hands atabsconfig through as authored, so the tab list is complete at mount. Pinned as TABS in the new suite: a restored_tabunder the same gated late definition puts the preset's filter on the lastfind. Green before and after this change.toggle: does not share the defect, not changed. One-off jsdom probe (not committed), derived and declared alike: a restored{ status: ['open'] }applies in neither, and a toggle click reports 0onSelectionsChangecalls.ToggleFilterstakes noinitialSelectionsand the bar writes nouf_*param for it, so there is no restore to lose, and the derived and declared readings are identical. See Acceptance notes.Pins
New file
packages/plugin-list/src/__tests__/UserFilters.restoredDerivedChips-12001.test.tsx. The definition waits on a promise the test releases by hand, and each derived case first asserts the bar is still empty, so a definition that resolves at once cannot hide the defect.fields: [status]plus restored{ status: ['open'] }. The chip's clear affordance is present, and the lastfindsends$filter['status', '=', 'open'].{ element: 'dropdown' }plus the same restore. The chip counts1, and the lastfindsends the control's$filter.ListViewderives the fields again from a new-but-equaluserFilters. The chip stays cleared and no furtherfindis issued.{ status: ['open', 'closed'], is_active: ['true'] }. The bar emits[['status','in',['open']], ['is_active','in',[true]]].Reverse verification
One-shot, both legs from the committed fix
f6c1e4bb5, throughablation-replace(anchor hit-counted, write and restore checked on disk):const arrived = fields.filter(...)replaced byconst arrived: typeof fields = [];, anchor 1 to 0, blob06022371d72dto4caa278b82ef): 3 failed, 3 passed. DERIVED, ONCE PER FIELD (list) and ARRIVAL are red; CONTROL, TABS and ONCE PER FIELD (bar) stay green. Restored: blob equals HEAD06022371d72d,git diff HEADempty.[fields]instead of[fieldNamesKey], andconst arrived = fields;): 2 failed, 4 passed, exactly the two ONCE PER FIELD pins. Restored: blob equals HEAD06022371d72d,git diff HEADempty.Direction observed in both legs: red, as expected.
Local gates
All at head
dab82f632, run from the worktree root. That head is a pure merge ofmainat5d77c099b(which carries4babf40, objectui#11982) into5c7d7533e: no conflicts, and the PR's diff against thatmainis identical, line for line, to its diff against the original baseb1f0dcc00(the same three files). The suite now reads 120 files, not 119, because the merge brought in one newplugin-listtest file frommain.pnpm exec vitest run packages/plugin-list/Test Files 120 passed (120),Tests 1265 passed (1265)pnpm --filter @object-ui/plugin-list type-check(after building its 12-package dependency closure)tsc --noEmit && tsc -p tsconfig.test.json, no diagnostics;--listFiles(read at5c7d7533e; the merge touched notsconfig) reads the new test file andUserFilters.tsxpnpm exec eslinton the two touched files0 errors, 34 warnings(2 files, from--format json); onUserFilters.tsxthe per-rule counts equal the original base's (30, same four rules)pnpm check:control-bytescheck-control-bytes: OKpnpm check:test-path-rootscheck-test-path-roots: OKpnpm check:changeset-claimsNo pending changeset names a file this change touches.pnpm check:pending-changeset-literalsNo test source names a pending changeset.pnpm check:new-line-citations0 new citation(s)node scripts/check-changeset-presence.mjs2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjs,-fixed,-overwriteThe lint run is a narrowing, declared: 2 files linted (count from
--format json), none ignored by the config;eslint.config.jsenables no type-aware linting and no custom rule reads another file, so this diff cannot move an untouched file's verdict. Left to CI: the fullpnpm test,pnpm lint, the console build andBundle Analysis. Ondab82f632all 43 check runs completed: 40 success, 3 skipped (Test (coverage), its shard matrix,dependabot), none failed. No locale pack or package export changed, so the i18n and readme-exports gates are not owed.First-load bytes
@object-ui/plugin-listis NOT lazy in the console:apps/console/src/main.tsximports./register-plugins, which imports@object-ui/plugin-liststatically. So this change adds first-load bytes.vite build, the two legs differing only inUserFilters.tsx, restore proven by blob hash): +128 B gzip (+395 B raw) onb1f0dcc00, and +136 B gzip (+397 B raw) onaa67386a5. All of it lands in theplugins-viewschunk.4babf40) re-pinnedMAX_EAGER_CLOSURE_GZIP_BYTESto 3,281,467 under ruling 6070754914 (letter D). Its standing rule is "a fix that adds first-load bytes within the margin lands without a ruling", and this change's 128 to 136 B is well inside that margin.Bundle Analysisondab82f632: PASS. The budget comment reads eager closure 3160.1 KB gzip across 289 chunks, against a budget of 3204.6 KB, so 44.5 KB of headroom. The main entry chunk is 71.6 KB against 350 KB.Acceptance notes
findon the derived path. The definition landing and the fetch gate opening happen in the same commit, soListView's fetch effect runs once with the conditions it held before the bar emits, then again with them. One-off probe: derived issues 2 finds ($filterabsent, then["status","=","open"]), declared issues 1. The last request always carries the filter, and the stale-request guard drops the first answer whenever it lands after the second request starts. Removing that first request belongs toListView.tsx, which is off this claim's surface. Noted, not filed.initialSelections, noonSelectionsChange. The mode is deprecated in the spec (ADR-0047 section 3.4a: kept so stored configs render). Noted, not filed.typecomes only from the definition (fields: [{ field: 'is_active' }], boolean in the definition) keeps the URL string, because the mount coerces before the definition loads and nothing coerces afterwards. One-off probe under the same gated late definition with a restored{ is_active: ['true'] }: declared-untyped sends$filter["is_active","=","true"]and checks no box while the chip counts 1; declared-typed and derived (this change) send["is_active","=",true]with the True box checked. Outside this PR by the triage ruling (a list with declared fields stays unchanged).Changeset:
.changeset/12001-restored-derived-chips.md,'@object-ui/plugin-list': patch.Session:
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8Generated by Claude Code