Skip to content

feat(plugin-list): read the spec's view-level map block on list views - #5908

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-5042-listview-map-config
Aug 23, 2026
Merged

feat(plugin-list): read the spec's view-level map block on list views#5908
os-zhuang merged 2 commits into
mainfrom
claude/issue-5042-listview-map-config

Conversation

@claude

@claudeclaudeBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#5042

The spec's view-level map block on a list view was authorable, validated, and never read. This makes it reach the renderer, and closes the same hole in the visualization switcher.

The dispatch gate, re-measured on the installed pin

This card was stopped three times by "merged upstream ≠ published ≠ pinned here", so the probe was re-run against the build this repo actually consumes — not the framework's main, and not node_modules absence (the trap the 2026-08-18 probe recorded):

$ node -e "require('@objectstack/spec/package.json').version"
17.2.0 # lockfile-exact; 0 entries left below it
$ grep -c ListMapConfigSchema …/@objectstack/spec/dist/ui/index.mjs
3
$ grep -c ListViewSchema …/@objectstack/spec/dist/ui/index.mjs # counter-probe
11

Functionally, against the installed build:

PIN: ListViewSchema accepts 7-key map block: true
parsed .map = {"latitudeField":…,"longitudeField":…,"locationField":…,
"titleField":…,"descriptionField":…,"zoom":12,"center":[1,2]}
PIN: strict — misspelled map key rejected: true unrecognized_keys(titelField)

All three states are true. premise_still_valid: true.

And the contract half is already live in this repo, which is worth stating because it is why this is a pure reader fix: map is absent from LIST_VIEW_LOCAL_OVERRIDES, so specFieldsExcept imports it into objectui's own ListViewSchema by reference. Measured:

objectui ListViewSchema has `map` in shape: true
safeParse ok: true parsed .map = {"locationField":"location","titleField":"title"}

No local mirror of ListMapConfigSchema was added or needed. (Stated from that direct measurement rather than from check:spec-symbol-derivation's green, which #5899 measured as skipping non-exported declarations.)

Precedence — read off the siblings, not invented

Every sibling visualization in ListView.tsx already puts the view-level block ahead of the legacy options.<kind> bag. The direction is unanimous across all seven; only the granularity differs:

blockexpressiongranularity
kanban{...(schema.options?.kanban || {}), ...(schema.kanban || {})}per-key merge
calendar...(schema.options?.calendar || {}), ...(schema.calendar || {})per-key merge
gallery{...(schema.options?.gallery || {}), ...(schema.gallery || {})}per-key merge
timeline{...(schema.options?.timeline || {}), ...(schema.timeline || {})}per-key merge
gantt...(schema.options?.gantt || {}), ...(schema.gantt || {})per-key merge
tree(schema as any).tree || schema.options?.tree || {}whole-block
chart(schema as any).chart || schema.options?.chart || {}whole-block

map follows the five that merge — which are also the five that flatten config into props the way the map branch does. The two that use || are the two that consume a single config object wholesale. This is reported as an open question in the structured report, since the siblings do disagree on granularity even while agreeing on direction.

What changed

  • ListView.tsx — a shared resolveListMapConfig merges the view-level block over the bag, per key, through the existing objectui#5177 whitelist. case 'map' forwards it.
  • ListView.tsx, the switcher — the capability gate that decides which visualizations are offered read options.map alone, so a view binding its coordinates in the spec block was filtered out of its own appearance.allowedVisualizations (whitelist ∩ resolvable) and fell back to ['grid']. It now asks the same merged config the render seam forwards, so the gate and the seam cannot disagree — including for a binding split across the two sources.
  • InterfaceListPage.tsx — the ADR-0047 interface-page path built its map binding from options.map alone, dropping the block one seam above ListView. It now forwards view.mapalongside the auto-derived binding rather than folding it in with ?? like the sibling bindings: ?? would let a partial authored block (map: { titleField: 'title' }) replace the derivation wholesale and drop the derived locationField, rendering no markers at all.

Deliberately unchanged: the branch still emits the flat form. A nested map key wins outright at getMapConfig (objectui#5018), which would turn this per-key merge into whole-block replacement and would trip warnOnShadowedFlatMapKeys; that rule is written around the flattener ("neither flattener emits a map key at all"). No defaults are added for zoom/center — objectui#5000's "declared camera wins, no declaration means fit to the queried records" is untouched, and a test asserts the keys are absent when undeclared.

Per the card's ruling, no second validation of the seven keys was added at the ListView seam; the reading stays getMapConfig's.

Verification

Union at 304ad7579 (the pushed HEAD): 27 passed (4 files). Affected-package suites: plugin-list + plugin-map 757 passed (60 files); app-shell/views 2948 passed, 1 skipped (306 files).

The discriminating pin is end-to-end, not a spy: ObjectMap.listViewMapConfigReach.test.tsx drives the real ListView into the real ObjectMap and asserts the consequence — marker titles and description read from the declared fields, and a declared camera suppressing the fit (bounds undefined, exact lat/lng/zoom). That seam needs its own test because getMapConfig reaches the flat form only through if (schema.locationField || schema.latitudeField), a gate ListView currently satisfies via its locationField … || 'location' default.

Reverse verification — two ablations, each with a restore trap

Ablation A — remove the view-level read from resolveListMapConfig. Mutation confirmed on disk by grepping the removed text (1 → 0) and the text that had to survive (pickFlatMapConfig(schema.options?.map) still 1), never an editor exit code:

Tests 11 failed | 11 passed (22)

Red: all seven-key/flat-form/precedence/whitelist/camera arms, both switcher arms that author the block, and all four plugin-map reach arms. Green and named non-discriminating: options.map-alone controls, the no-config control, the "merge keeps a bag-only key" arm (its locationField comes from the bag either way), the fit-to-records control, and every InterfaceListPage arm — that seam is a different file.

Ablation B — remove the InterfaceListPage forward, the seam A leaves untouched:

Tests 2 failed | 20 passed (22)

Exactly the two arms that author a view-level block on the referenced view; the two InterfaceListPage controls and all 20 other tests stayed green. The two ablations are complementary, which is what shows each seam is independently load-bearing.

Both scripts carry trap '<restore>' EXIT INT TERM; git diff HEAD --stat was empty after each. The suites resolve @object-ui/plugin-list through the root vitest alias table to packages/plugin-list/src, so these read source, not dist/ — which the red results themselves prove, since a dist/-resolved suite would have stayed green under a source-only mutation.

Gates

Each quoted from the gate's own verdict line, exit code captured before any pipe:

check-changeset-presence EXIT=0 ✅ 5 source file(s) of 3 released package(s) changed, and this
change declares 1 changeset(s)
check-changeset-no-major EXIT=0 ✅ No changeset declares a `major` bump.
check-control-bytes EXIT=0 ✅ OK (scanned 4912 tracked text file(s); skipped 85 binary)
check-phantom-dependencies EXIT=0 ✅ Every in-scope import is declared by the package that publishes it
check-spec-symbol-derivation EXIT=0 ✅ 1300 files scanned against 4959 spec export names

Re-derived from the actual diff and also run: check-published-dist-tooling (EXIT=0 — "No published package's build output carries tooling material", which covers the new src/-level test file in plugin-map), check-package-self-import (EXIT=0), check:esm-specifiers (EXIT=0), sync-quick-reference-release --check (EXIT=0).

type-check for all three packages after building their dependency closures: each echoed its package name (packages/plugin-list type-check: Done, and the same for plugin-map / app-shell), so this was not a zero-match --filter exiting 0.

Lint was not narrowed. The full repo scan ran: eslint . --no-inline-config --format json3608 files, 89 errors in 74 files, none of them mine (counter-probe: all 5 of my changed files were present in that run). Those 89 are pre-existing in packages that carry no lint script, which turbo run lint — what CI runs — skips by design. The CI-equivalent path is green: plugin-list / plugin-map / app-shell each Done with 0 errors (392 / 103 / 2625 warnings, all pre-existing in kind; --max-warnings is deliberately unset in this repo).

One dependency edge, deliberately

packages/plugin-map gains @object-ui/plugin-list as a devDependency, for the end-to-end test file alone. It is dev-only (no runtime source in plugin-map imports it) and acyclic (plugin-list does not depend on plugin-map). It is declared rather than left phantom because check-phantom-dependencies judges tooling files against the package's own dependency fields or the workspace root's, and neither declared it. The alternative homes cannot host the test: plugin-list cannot resolve react-map-gl to mock it, and apps/console — which declares both plugins — cannot either.

Not done here, and why

The card's "Done when" also names the showcase map view. That metadata lives in the framework repo, not this one — examples/app-showcase/src/ui/pages/task-visualizations.pages.ts — where TaskMapPage already declares appearance.allowedVisualizations: ['map']. So the allowedVisualizations wiring the card asks for is already correct and is not an objectui change; what remains there is declaring a map: { titleField: 'title', locationField: 'location' } block on that page to exercise this forward. Filed separately rather than reached across repos from this PR.


Generated by Claude Code

`ListMapConfigSchema` (objectstack#9340) has been authorable and validated
since the @objectstack/spec 17.1.0 pin — it flows into this repo's own
`ListViewSchema` by reference — but nothing read it. `ListView`'s `case 'map'`
forwarded only the legacy `schema.options.map` bag, so declaring the typed
block changed nothing at runtime and marker titles fell back to the renderer's
placeholder.
The block now merges over that bag, per key, which is the precedence the
sibling visualization blocks in the same file already use. Both sources go
through the existing objectui#5177 whitelist and the branch still emits the
flat form, keeping `getMapConfig`'s objectui#5018 rule true.
The switcher capability gate had the same hole with a sharper consequence: a
view binding coordinates in the spec block was filtered out of its own
`appearance.allowedVisualizations` and fell back to `['grid']`. It now asks the
same merged config the render seam forwards.
`InterfaceListPage` forwards the referenced view's block alongside the
auto-derived binding rather than replacing it, so a partial authored block
keeps the derived coordinates.
No defaults are added for zoom/center — objectui#5000's fit-to-records
behaviour is unchanged.
Fixes#5042
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
…ot a control
Ablation A measured it RED: it authors a view-level `map` block, so removing
the forward breaks it. Calling it a CONTROL claimed the opposite and would
have misread the next ablation's output.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EuPCi56cnGyykygi3z9w4m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3230.4 KB3990.2 KB
Main entry chunk (gzip)153.6 KB350 KB
Entry fileindex-CL7Ew482.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.13KB3.77KB
app-shell (runtime-config.js)13.57KB4.78KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)504.16KB114.08KB
core (index.js)4.92KB1.97KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)164.55KB45.67KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)187.32KB44.31KB
plugin-dashboard (index.js)133.32KB34.42KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)244.01KB61.72KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.63KB30.64KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.86KB27.22KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.57KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)52.40KB17.45KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.88KB1.85KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

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

Projects

None yet

2 participants

@os-zhuang@claude