fix(app-shell): the object page renders a stored view's judged legacy options bag, as the interface page does (objectui#10380) - #11571
Conversation
… options bag, as the interface page does The flattened list overlay of @objectstack/spec 17.6.0 declares a legacy `options` bag, judges each `options.KIND` block key by key and stores it. InterfaceListPage forwarded the bag to ListView and ObjectView dropped it, so one stored row handed the renderer two different configs. ObjectView now forwards the bag. For each kind the bag carries, the bag's block replaces the page's synthesized block (the mapCfg rule) and the view's own top-level block goes out at the top level, so ListView's existing per-key merge lays it over the bag. InterfaceListPage's derived defaults no longer fill a kind the bag carries, the rule mapCfg already follows, so a derived guess no longer outranks the row's declaration. Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…tions bag parity pins Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…nough for tsc Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
…ns so tsc takes the relay's call Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2 Co-authored-by: Claude <noreply@anthropic.com>
|
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
|
Fixes #10380
Clause-②: no. No published schema, type, input or accept set moves.
@objectstack/spec17.6.0 already declares and judges the list overlay's legacyoptionsbag (ListViewOverlayOptionsSchema), and this change makes the object page render what that door accepts, the way the interface page already does. The one new export,storedLegacyOptionsin app-shell'sObjectViewmodule, is a helper exported for its pin, like its siblingskanbanViewOptionsandtimelineViewOptions.What was wrong
The flattened list overlay in
@objectstack/spec17.6.0 declares a legacyoptionsbag. The view write door judges eachoptions.KINDblock key by key with that kind's own block schema. It refuses an out-of-contract key by name and stores what it accepts. The spec describes the bag as an underlay: the top-levelKINDblock "wins per key where both set one".InterfaceListPageforwarded a stored row's bag toListView, andObjectViewdropped it. So one stored row handed the renderer two different configs. Measured onorigin/mainab18797, with rows the 17.6.0 door accepts:regionon the interface page and ungrouped on the object page (options.timeline.groupByField);regionon one page and bynameon the other (options.timeline.titleField,options.kanban.titleField);locationFieldbinding on the interface page and none on the object page (options.map).Ruling A (5824043998) keeps the interface page's forward ("what passes the door is legal"). The seat's answer 5971906253 directs the object page to honour the bag too, and to reuse
ListView's existing per-key merge, with no second merge in app-shell.What changed
ObjectView(app-shell). A new helper,storedLegacyOptions(viewDef), returns the stored row's bag, or{}when the row has no bag or the bag is not an object. The relay then:options. For each kind the bag carries, the bag's block replaces this page's synthesized block (kanbanViewOptionsand its siblings). This is the ruleInterfaceListPageapplies to itsmapCfg(view.options.map ?? derived). A synthesized block stands in only for a kind the bag does not carry, so a row with no bag is relayed exactly as before;InterfaceListPagedoes.ListView's existing per-key merge ({ ...options.KIND, ...KIND }in each render branch) then lays the declared block over the bag. That is the spec's precedence, and no merge is written in app-shell.InterfaceListPage(app-shell). A default binding the page derives from the object (defaultKanbanFromObject,defaultCalendarFromObject,defaultGalleryFromObject,defaultGanttFromObject) no longer fills a kind the row's bag carries. Before this change, the derived block sat at the top level and outranked the row's own bag declaration on every key it set. The object page, which derives nothing, therefore rendered such a row differently. This is the rulemapCfgalready follows, extended to the other derived kinds. The forward ofoptionsis unchanged (ruling A), and so is theoptions.maplegacy path and its CONTROL pin.ObjectView.relayRungCensus-7559.test.ts). The eight per-kind blocks now have a conditional top-level rung that readsviewDef, so the census counts them as relayed. It requires theirrelayed-nestedledger rows to go ("an absence that came back is not an absence"), and they are replaced by a comment that says why. Their nestedoptions.KINDpaths are still written for every row, and are still asserted by name in the census's derivation checks.@object-ui/app-shellpatch.What an existing row that still carries
optionsdoesBoth pages forward the stored bag in the same way, including a key the 17.6.0 door now refuses (for example
options.timeline.metaFields). The renderers ignore the retired keys:ObjectTimeline's chip list has been the constant status / priority pair since themetaFieldsretirement (objectui#10222), andListViewstrips the stray kanbangroupByand the calendardateField/endField. The row is served as stored until its next save, which the door refuses by name. No migration is done, per the maintainer's 「20051 不考虑现有的数据」.Tests
New pin:
ObjectView.storedOptionsBag-10380.test.tsx. Each case renders one stored flat overlay row through both pages, captures the schema each page handsListView, mounts the REALListViewon it with a spy renderer for the kind, and compares what the renderer receives. The cases are:options.timeline.titleField;options.map;options.timeline.groupByFieldunder a declared timeline;options.kanban.titleFieldunder a declared kanban);titleField; the kanban lane);storedLegacyOptions.Every row is one the 17.6.0 door accepts, judged with the spec's own
ViewMetadataSchema.safeParseanddiagnoseViewMetadatafrom the installed@objectstack/spec17.6.0:success=true,branch=listOverlay, withoptionskept in the parse.The direction of each run was written before it ran. Each run used repo-root
pnpm exec vitest run FILE, underos-verify-lock:origin/mainfirst. The fix was committed (4d0668d), thenObjectView.tsxandInterfaceListPage.tsxwere checked out atab18797.grep -c legacyOptionsread 0 in both files. Result:Tests 6 failed | 2 passed (8).storedLegacyOptionsis RED because the export does not exist there.regionon both pages). It failed on the card title: the object page flooredtitleField: 'name'and the interface page sent none for a row whose bag carries a kanban block. The fix removes that difference, because the bag's block replaces the synthesized one.git checkout HEAD --. The blob hashes matched HEAD andgit diff HEADwas empty.{ ...viewDef.KIND, ...legacyOptions.KIND }at the top level, for timeline and kanban). That is the second merge, with the wrong precedence, that the direction forbids. The anchors were checked on disk before the run: 1 → 0, and the injected text 0 → 1. Predicted: both collision cases RED. Observed:Tests 3 failed | 5 passed (8), which is both collisions plus theoptions.timeline.groupByFieldrow. That row turned red becauseListViewreadsgroupByFieldas a flat timeline key off the top-level block only, so the merged block exposed it on the object page alone. Restored and verified the same way.44e55cf:pnpm --filter @object-ui/app-shell type-checkexit 0. Itstsconfig.test.jsoncovers the new test: it reported a type error there on an earlier commit.pnpm exec vitest run packages/app-shell/src/views/ObjectView packages/app-shell/src/views/InterfaceListPage packages/app-shell/src/views/__tests__/PageView packages/app-shell/src/no-refresh-key-remount.ratchet.test.ts packages/app-shell/src/views/view-filter-fold.ratchet.test.tsgivesTest Files 55 passed (55)andTests 504 passed (504). This covers everyObjectView.*andInterfaceListPage.*suite, including theoptions.mapCONTROL pin, the relay census,titleFieldConvergence, the calendar / gallery / gantt / timeline binding pins andhostRerenderRefetch-10046.turbo run build --filter='@object-ui/app-shell^...' --concurrency=2gave 28 of 28 tasks successful.eslint --no-inline-configon the four touched.ts/.tsxfiles givesfiles=4, 0 errors.eslint.config.js's**/*.{ts,tsx}block. It usestseslint.configs.recommendedwith noparserOptions.projectand noprojectService, so linting is not type-aware and this diff cannot move a verdict on an untouched file.ab18797, base → head:ObjectView.tsx171 → 173 (oneanyin the helper's return type, and thereact-refresh/only-export-componentswarning every sibling exported helper carries),InterfaceListPage.tsx41 → 41, the census 0 → 0. The new test has 19no-explicit-anywarnings, in line with its siblings. No--max-warningsis set in this repo.44e55cf, all exit 0:pnpm check:control-bytes,pnpm check:new-line-citations(0 new citations),pnpm check:pending-changeset-literals,pnpm check:spec-symbols,pnpm check:self-importandpnpm check:phantom-deps;pnpm check:changeset-claims: report-only. It names7070-no-invented-gantt-date-fields.mdand7218-rowcolor-host-relay.md. Both paragraphs were re-read and both are still true;node scripts/check-changeset-presence.mjs,check-changeset-no-major.mjs,check-test-path-roots.mjs,check-vi-mock-specifiers.mjs,check-vi-mock-inherit.mjsandcheck-vi-mock-override-shape.mjs.Acceptance notes
type: 'chart'row whose chart block lives ONLY in the bag passes the 17.6.0 door. The object page renders chart views through its dedicatedObjectChartroute, which readsviewDef.chartalone and never reachesListView. So that row still renders the bag's chart on the interface page and the default binding on the object page. Aligning it means either a second chart resolution in app-shell (thechart || options.chartrule ofListView'sresolveListChartBinding), which the direction forbids, or exporting that resolver from@object-ui/plugin-list, which adds a published name. That is left to the seat. Evidence: the door acceptance was measured, and the route was read from source and not rendered.tree/chartblock.InterfaceListPage's schema literal forwardskanban,calendar,gallery,timeline,ganttandmap, and has notreeorchartrung.treeandchartare allowed visualizations, andCreateViewDialogwrites both blocks on the views it creates. A view'stree.parentFieldor chart binding therefore reachesListViewon the object page and not on the interface page. This is reported for the seat and not fixed here, because it is independent of the bag and would change the interface page for rows without one. Read from source.titleField: 'name'floor ofkanbanViewOptionslikewise still applies to rows without a bag.updateView, which merges the current row). The door then refuses that save with a 422 that names the key. This is landed objectstack behaviour (fix(metadata-protocol,spec)!: a saved view stores the parsed value of every key its body carried, and a ViewItem record's top-level options is refused by name (stage iv of #20051) objectstack#20868,9905e61c). Noted here and not filed, per the seat's answer.content/docs/or the app-shell README describes the bag or the interface page's derived defaults. The two pages that mention alistViewsentry'soptionsbag describe the authored object schema, where the bag is still refused, and they stay true.Session:
https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2(dispatched dev ofdomain:uiseat 1, claim 5971770388, as amended by 5971906253).Generated by Claude Code