Skip to content

fix(plugin-view,app-shell): refresh the view in place after a write instead of remounting it (objectui#10035) - #10378

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-10035-objectview-refresh-not-remount
Sep 24, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-10035-objectview-refresh-not-remount

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #10035
Clause-②: no

What this changes

AGENTS.md commandment 8's corollary: refresh data, don't rebuild UI. Both ObjectView layers carried their refresh counter in a React key, so every write remounted the view. This PR takes the counter out of the key wherever the rendered view can refetch in place, and hands it over as a data signal instead:

  • @object-ui/app-shell renderListView: ListView is keyed on object + view id only (identityKey), and the combined counter (page counter + plugin ObjectView counter) now reaches it as refreshTrigger, which ListView's fetch effect already lists. Before this, the page's own counter (record actions, import, realtime, view edits, the console's externalRefreshKey) reached ListView through the key alone, so dropping the key without the relay would have silently lost those refetches.
  • @object-ui/plugin-view renderContent: the non-grid branch keys kanban / calendar / gallery / timeline / map / tree on the view identity only; they draw the rows the non-grid fetch effect re-reads when refreshKey moves.

⚠️ Partial on purpose, hence Part of: three renderers have no in-place refetch path, so they keep the counter in their key, each set named and commented (REMOUNT_TO_REFRESH_VIEW_TYPES in plugin-view, REMOUNT_TO_REFRESH_VISUALIZATIONS in app-shell). Fixing them needs a child edit outside this claim's file surface (see "The half that stays" below).

Measurement: what refetches each view after a write, once the key stops moving (dispatch assumption A1)

Read on origin/main cff4b7754.

site / branch renderer in-place refetch path key after this PR
plugin-view, renderListView branch host list view the key was never applied there; host receives refreshKey and schema.refreshTrigger unchanged
plugin-view, kanban ObjectKanban draws host data (fetch effect names refreshKey); with external data it does not subscribe itself ("Parent handles refresh") identity
plugin-view, calendar ObjectCalendar same external-data path identity
plugin-view, gallery / timeline / map ObjectGallery / ObjectTimeline / ObjectMap draw props.data; map's effect names dataProp identity
plugin-view, tree (host-only type) ObjectTree queries for itself, but its fetch effect names the host data, so a new array re-issues its query identity
plugin-view, gantt ObjectGantt none: the registered object-gantt renderer hands it only schema and dataSource; its reload effect names no counter, no onMutation, no invalidation bus identity + counter (kept)
plugin-view, chart (host-only type) ObjectChart none: own aggregate query off the node; reads neither host data nor a refresh input identity + counter (kept)
plugin-view, grid (and unknown types) ObjectGrid none from the host: its query moves only on its internal counter (pull-to-refresh, its own row/bulk actions, inline save); no onMutation, no refresh prop identity + counter (kept)
app-shell, view type chart ObjectChart in Suspense none (as above) identity + counter (kept; same string as before)
app-shell, ListView for grid / kanban / calendar / gallery / timeline / map / tree ListView refreshTrigger (now the combined counter) + its own onMutation subscription; children draw ListView's rows, tree re-queries on them identity
app-shell, ListView that can draw gantt or chart (its own viewType, or the author's appearance.allowedVisualizations through the in-list switcher) ObjectGantt / ObjectChart none; and this host cannot see which visualization the switcher shows identity + counter (kept)

A2 held: object, view id and view type stay in the key, so switching view / object / type is still a remount (the objectui#10046 view-switch control stays green).

A3, measured: neither notifyDataChanged nor its reader hook reaches any of these renderers. The bus readers (useDataInvalidation) are app-shell's record detail, related-record actions and signed-in-user locale hook, plugin-detail, and plugin-grid's per-record CRUD-verdict hook (a permission answer, not the grid's rows); ListView, ObjectGrid, ObjectKanban, ObjectCalendar, ObjectGantt, ObjectChart, ObjectGallery, ObjectTimeline, ObjectMap and ObjectTree do not subscribe. (useDataRefresh, the name in the dispatch, exists in @object-ui/react with zero src consumers.) So the smallest change is to keep refreshKey as the signal and relay it through refreshTrigger, the input ListView already reads — the same channel the Studio Data pillar's renderStudioGridList relies on.

A5: no existing test asserted remount-on-refresh. One docstring sentence in ObjectView.refreshSignal.test.tsx said the counter is read "by the child-view remount key"; it now says the remount key of the views that cannot refetch in place.

Tests

New pins, one file per site:

  • packages/plugin-view/src/__tests__/ObjectView.refreshInPlace-10035.test.tsx — a data-fed view (kanban, calendar, gallery, timeline, map, tree) keeps its instance across an onMutation write (a) and receives the refetched rows (b); gantt, chart and grid still get a fresh instance.
  • packages/app-shell/src/views/ObjectView.refreshInPlace-10035.test.tsx — the real page, plugin ObjectView and ListView; an externalRefreshKey bump (what the console sends after a record-form save, undo, redo) re-issues the list query exactly once (b) while a node read inside ListView is the same node afterwards (a); a list whose type is gantt, or whose whitelist offers gantt, still remounts.

Reverse run and ablations, every leg restored through a trap / ablation-replace wrap and proven by blob hash == HEAD and an empty git diff HEAD, at 42737c855:

leg mutation result
R both source files back to cff4b7754 8 red / 5 green: every in-place case red on (a) (instance 2 where 1; list node replaced), with (b) passing first — the pre-fix world refetched too, through the remount; the 5 kept-remount cases green
A1 app-shell refreshTrigger: combinedRefreshKey deleted 2 red / 2 green: both app-shell in-place cases red on (b) — the list never re-queried
A2 plugin-view non-grid fetch effect stops naming refreshKey 6 red / 3 green: every in-place case red on (b) (find called once, expected twice). ⚠️ The first A2 attempt was a no-op: its replacement text already occurred inside the anchor, and ablation-replace refused it (replacement count rose by 0); rerun with a distinct replacement
A3 app-shell whitelist dropped from the reachable-visualization list 1 red / 3 green: the whitelist case red
A4 both remount sets emptied 4 red / 9 green: plugin-view gantt + chart and both app-shell kept-remount cases red; grid keeps its own key

Gates (all at 42737c855)

  • pnpm exec vitest run packages/plugin-view/ — 46 files / 428 tests passed.
  • Every app-shell test file that mentions ObjectView, the new pin among them — 67 files / 698 tests passed (two runs: 52 + 15).
  • Closure build turbo run build --filter='@object-ui/app-shell^...' (28 tasks) exit 0, then type-check (tsc --noEmit && tsc -p tsconfig.test.json) exit 0 for @object-ui/plugin-view and @object-ui/app-shell.
  • ESLint on the 5 touched .ts/.tsx files, --format json: 5 files, 0 errors. Warnings sit on untouched lines of the two source files, plus no-explicit-any in the new tests (same as the sibling refresh test). Why this narrowing is complete: eslint.config.js sets no parserOptions.project / projectService and no custom rule in eslint-rules/ reads the disk, so a file's result depends only on its own text and the config, and this diff changes neither for any untouched file.
  • check-changeset-presence, check-changeset-no-major, check:new-line-citations (0 new), check:changeset-claims (5 pending bodies name a touched file; each paragraph re-read, none is about keys or refresh, all still true), check-vi-mock-override-shape, check-test-path-roots, check:control-bytes, check:esm-specifiers, check:self-import, check:handler-key-reads, check:phantom-deps, check:unreferenced-sources — all exit 0. check-governed-queue-guard --test: NOT GOVERNED.

The half that stays (why this is Part of)

ObjectGrid (plugin-grid), ObjectGantt (plugin-gantt) and ObjectChart (plugin-charts) read no refresh input, so a host can only show them a write by remounting them. The same gap exists inside ListView: its own toolbar Refresh and its onMutation refetch never reach a gantt or chart visualization, because those query for themselves. Closing it means a child edit (for example, each of the three following useDataInvalidation or a host refresh counter), which is outside this claim's surface; the report names it for the seat.

Acceptance notes

  • Kept remounts are named sets with a "do not drop a member" comment, and the kept-remount cases pin them: removing a member without a renderer change goes red by name (leg A4).
  • A list that whitelists gantt or chart remounts for every visualization, not only while the switcher shows gantt/chart — this host cannot see the switcher's state. Conservative on purpose: the alternative is a gantt that silently stops showing writes.
  • ListView may now fetch twice after a write through the data source (its onMutation handler plus refreshTrigger); its own comment already records that find() coalesces concurrent identical reads.

Changeset: .changeset/10035-objectview-refresh-in-place.md, patch for both packages.

Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C


Generated by Claude Code

…nstead of remounting it

Both ObjectView layers carried their refresh counter in a React key, so
every save, delete, import or realtime event remounted the whole view and
reset its component state (AGENTS.md #8: refresh data, don't rebuild UI).

- app-shell: renderListView keys ListView on object + view only and hands
  the combined counter to ListView as refreshTrigger, which its fetch
  effect already follows.
- plugin-view: the non-grid branch keys data-fed views (kanban, calendar,
  gallery, timeline, map, tree) on the view identity only; they draw the
  rows the fetch effect re-reads on refreshKey.

Renderers that fetch for themselves and read no refresh input (gantt,
chart, and plugin-view's standalone ObjectGrid) keep the counter in their
key, named in REMOUNT_TO_REFRESH_VIEW_TYPES / _VISUALIZATIONS, because the
remount is still the only way they show a write.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 5 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6726-find-envelope-records-arms.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    | module | what it does | | --- | --- | | components/src/hooks/related-count-store.ts | related-list tab badge count | | components/src/renderers/basic/data-list.tsx | element:repeater rows | | components/src/renderers/basic/elements.tsx | element:number client-side aggregate | | components/src/renderers/basic/record-picker.tsx | element:record_picker options | | plugin-detail/src/renderers/record-activity.tsx | record:activity self-fetch | | plugin-detail/src/renderers/record-history.tsx | record:history self-fetch | | plugin-view/src/ObjectView.tsx | non-grid (kanban / calendar / gallery / timeline) fetch |

.changeset/7070-no-invented-gantt-date-fields.md

  • names app-shell/src/views/ObjectView.tsx → packages/app-shell/src/views/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.
  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • app-shell/src/views/ObjectView.tsx — the console object page. The inline branch becomes ganttViewOptions, the sibling of calendarViewOptions and timelineViewOptions: the declared block spread whole, title floored at 'name', no date field invented. - plugin-list/src/ListView.tsx — the render branch AND the capability gate. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/7499-gantt-non-axis-floors-omitted.md

  • names plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    • plugin-list/src/ListView.tsx — the object-gantt render branch. - plugin-view/src/ObjectView.tsx — generateViewSchema, the authored object-view element route, which bypasses ListView entirely.

.changeset/8653-listview-title-retired-rowactiondefs-pinned.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    title — retired. ListView resolved its export filename through schema.label || (schema as any).title. @objectstack/spec/ui's ListViewSchema refuses title by name (unrecognized_keys: ['title']) while ObjectGridPropsSchema accepts it; packages/types mirrors the platform contract rather than ruling over it, so declaring title on ListViewSchema would have made this repo accept what the platform save gate rejects. That asymmetry is also why objectui#6639 could take the declare branch for ObjectGridSchema.title one package over and this site could not. A parse-based census of apps/ examples/ content/ and packages/ found zero list-view nodes authoring title, so the retirement costs no author a filename. Over that same corpus the instrument reports three object-grid nodes carrying the key: two authored ones, both in content/docs/api/schema-reference.md, plus one that is not authored at all — packages/plugin-view/src/ObjectView.tsx composes title: schema.table?.title onto a grid node it builds, so it is a producer writing the key rather than an author declaring it. ObjectGrid's own title reads are untouched — they remain declared, ruled and read.

.changeset/object-view-unmirrored-keys-7779.md

  • names packages/plugin-view/src/ObjectView.tsx → packages/plugin-view/src/ObjectView.tsx — edited by this change

    What was measured. Every reading was taken on the object-view node renderer (packages/plugin-view/src/ObjectView.tsx, registered by plugin-view/src/index.tsx) with schema.objectName / schema.layout as the positive controls of the same schema.KEY query, so each zero is a reading; the repo-wide census of viewTabBar finds the key in no source file outside @object-ui/types (two doc tables listed it as authorable and are corrected here). The spec side was read through the installed pin (@objectstack/spec@17.2.0, ui entry, 117 exported object schemas walked; control keys objectName / columns / navigation / listViews hit): the three spec-modelled keys are optional slots on ListViewSchema and ObjectListViewSchema; the six local keys have no spec slot anywhere.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 65f1e8dc6 (merge-base with origin/main): 5 file(s) changed outside .changeset/, read against 1347 pending declaration(s) that publish a body (1920 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3042.3 KB 3104.5 KB
Main entry chunk (gzip) 148.4 KB 350 KB
Entry file index-DjdkRzVR.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
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) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
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) 541.72KB 129.49KB
core (index.js) 8.94KB 3.59KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 222.47KB 61.83KB
fields (index.js) 254.86KB 64.50KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 34.99KB 11.45KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.15KB 11.05KB
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.75KB
mobile (index.js) 1.99KB 0.87KB
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) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.08KB 3.95KB
plugin-calendar (index.js) 51.44KB 14.62KB
plugin-charts (index.js) 72.59KB 20.30KB
plugin-chatbot (index.js) 198.27KB 47.18KB
plugin-dashboard (index.js) 133.19KB 35.25KB
plugin-designer (index.js) 216.12KB 44.37KB
plugin-detail (index.js) 260.94KB 67.93KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.34KB 37.47KB
plugin-gantt (index.js) 168.31KB 41.45KB
plugin-grid (index.js) 215.22KB 58.85KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.17KB 28.16KB
plugin-map (index.js) 21.74KB 7.07KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.72KB 8.96KB
plugin-tree (index.js) 10.74KB 3.76KB
plugin-view (index.js) 85.86KB 21.38KB
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.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
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 (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 42737c85576c3ecd6e0451c0b7f87d7ac65cea4b

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (107 of 107 at the review tier). Adopted by this seat.

① Derived judgments

  • Head and base. head.sha = 42737c85576c3ecd6e0451c0b7f87d7ac65cea4b (one commit); merge-base with origin/main is cff4b7754, the sha the PR says it read. Governing text confirmed in objectui AGENTS.md §5 feat: add live playground for interactive schema demonstration #8: "Corollary: refresh data, don't rebuild UI — after a save/action, invalidate the affected data (notifyDataChanged from @object-ui/react) so consumers refetch in place; never bump a key= to remount a subtree".
  • (a) In-place refetch path per view type, read at head. plugin-view ObjectView.tsx: the non-grid fetch effect's dependency list is [schema.objectName, dataSource, currentViewType, refreshKey, currentNamedViewConfig, activeViewQueryInputs, renderListView, objectSchemaReady, objectSchema, perms] and ends in setData(items); refreshKey is bumped by the dataSource.onMutation subscription (skipped only when a host supplies renderListView), handleDelete, handleBulkDelete and handleFormSuccess. generateViewSchema emits object-kanban / object-calendar / object-gallery / object-timeline / object-map / object-tree, and SchemaRenderer hands the host data prop through (its ...props spread is last; only the AUTHORED data key is refused, per the objectui#9571 block). Then, child by child:
    • kanban: registered directly ('object-kanban': ObjectKanban); rawData reads externalData at render, loading follows externalLoading by effect, own onMutation subscription skipped with external data ("Parent handles refresh"). No cached copy.
    • calendar: ObjectCalendarRenderer forwards data={externalData} loading={externalLoading}; ObjectCalendar mirrors by useEffect(..., [externalData, hasExternalData]) and syncs loading. currentDate, view and unscheduledOpen are the component state that now survives, which is the intended outcome, not staleness.
    • gallery: registered directly; items = props.data || ... at render, and the fetch effect names props.data.
    • timeline: renderer spreads {...props}; rawData = (props as any).data || ... at render; fetch effect names (props as any).data.
    • map: renderer spreads {...props}; effect names dataProp and does setData(dataProp). selectedMarkerId may point at a deleted row, but selectedMarker = markers.find(...) and every render site is guarded by selectedMarker &&, so the popup closes rather than showing a stale record.
    • tree: renderer spreads {...props}; ObjectTree's fetch effect names (rest as any).data, so the new array re-issues its own full-record query; expansion overrides persist (intended).
    • app-shell ListView path: ListView's fetch effect names schema.refreshTrigger and its own refreshKey (bumped by its dataSource.onMutation subscription and the imperative refresh); the page sets fullSchema.refreshTrigger = combinedRefreshKey (page counter + plugin counter, both monotonic, so the sum moves whenever either does). ListView re-seeds currentView from schema.viewType, currentSort from schema.sort, hiddenFields, inlineEdit and groupingConfig from the schema by effect, so view edits that used to arrive through the remount still arrive. Children draw ListView's data. ObjectGrid's selection is cleared by resetSelection on bulk dispatch / delete / dialog close and selectAllMatching resets on hostFindParamsKey. Stale responses are dropped by fetchRequestIdRef.
    • One observation, not a stale-data regression: ListView's pageResetSignature deliberately excludes the refresh inputs and serverPage, so a write that empties a page above 1 (bulk-deleting a whole later page, or a concurrent deleter) leaves the pager on that page with an empty grid and the first-run empty-state copy. The rows shown are the honest result of the requested window, and this is pre-existing ListView behaviour (its own onMutation refetch, toolbar Refresh and Studio's renderStudioGridList already behave so); on this page the remount's reset to page 1 masked it. A clamp belongs in ListView, outside this claim's surface; the report does not name it, so the seat should.
  • (b) Kept-remount sets are truthful. ObjectGantt: the registered object-gantt renderer destructures schema alone and renders ObjectGantt schema={bound} dataSource={dataSource} only; reload and the fetch effect name [adapterInputsKey, dataSource, apiFetch, resource, hasInlineData, dataProvider, schema.filter, schema.sort, searchTerm, searchFieldsKey, objectSchema, perms] with (rest as any).data intentionally untracked, no onMutation, no useDataInvalidation. ObjectChart: fetch effect names [schema.objectName, datasetKey, aggregateKey, filterKey, compareToKey, schema.xAxisKey, schema.chartType, runAggregate, filterScope, fieldOptionLabel], no refresh input, ignores host rows. ObjectGrid: fetch effect ends ..., dataConfig, refreshKey, perms.isLoaded, groupingProjectionKey] where refreshKey is its internal counter; no onMutation, refreshTrigger or bus read. Whitelist check: reachableVisualizations = [fullSchema.viewType, ...(fullSchema.appearance?.allowedVisualizations ?? [])] reads the same appearance the page hands ListView; ListView draws its switcher only when showViewSwitcher is set AND availableViews has more than one entry, this page sets showViewSwitcher only when allowedVisualizations has more than one entry, and the localStorage "saved view" restore in ListView is commented out, so with no whitelist no user can reach a gantt or chart visualization in this list. The check is conservative (remounts even when the whitelisted entry does not resolve), which the PR states. Both constants' docstrings name the two members, the reason each has no in-place path, the grid branch (plugin-view), and a ⛔ do-not-drop note; REMOUNT_TO_REFRESH_VISUALIZATIONS also explains why chart is about the in-list switcher on this page.
  • (c) Double fetch. After a data-source write on the page, ListView's own onMutation bump and refreshTrigger are two deps of one effect: one run if they land in the same commit, otherwise two, with the older response discarded by the stale-request guard; ObjectStackAdapter.find keeps an inflightFinds map keyed by resource + stableStringify(params) and returns the existing promise, so concurrent identical reads are one round trip (the claim in ListView's comment holds for that adapter). On base the same write produced an onMutation refetch on the doomed instance plus a full mount fetch on the new one, so this is not a regression. On the plugin-view standalone path the parent's subscription is the only bump (children skip theirs with external data): one fetch.
  • (d) Pins. plugin-view pin: 9 tests (6 in-place via it.each, each asserting (a) same data-instance from a useState initializer and (b) data-rows reaching 2 after find is called twice; 3 remount cases asserting a fresh instance; ObjectGrid mocked with the same stand-in). app-shell pin: 4 tests on the real page, plugin ObjectView and ListView (2 in-place: listQueries delta exactly 1 and the view-description node, which ListView renders, is the same node; 2 remount: own type gantt, gantt in whitelist). Reported counts re-derived from the assertions: R = 6+2 in-place red on (a), 3+2 kept green = 8/5; A1 (app-shell relay deleted) = both app-shell in-place red on (b), 2/2; A2 (fetch effect stops naming refreshKey) = 6 red on (b), 3 green = 6/3; A3 (whitelist dropped) = 1/3; A4 (both sets emptied) = plugin-view gantt+chart and both app-shell kept cases red, grid keeps its own key = 4/9. All consistent.
  • (e) Changeset. Every sentence of .changeset/10035-objectview-refresh-in-place.md checked: "for most view types" (6 plugin-view types plus the page's grid/kanban/calendar/gallery/timeline/map/tree lists in place; gantt, chart and standalone grid kept) — true; the calendar "jumped back to the current month and its default mode" — true (currentDate is useState(new Date()), view seeds from schema.defaultView || 'month', no persistence); the list "lost its scroll position and the visualization picked in its own switcher" — true (currentView is ListView state seeded from schema.viewType; the saved-view restore is disabled); the plugin-view bullet — true per (a); the last paragraph naming gantt and chart in both layers, whitelist lists, and the standalone grid — true per (b). One wording caveat: the app-shell bullet says the page "keys the list on the object and view alone" unconditionally; the final paragraph carves out the gantt/chart-capable lists, so the changeset as a whole is accurate.
  • (f) Docstring. The corrected sentence in ObjectView.refreshSignal.test.tsx — refreshKey "is read by the non-grid fetch effect, by the remount key of the views that cannot refetch in place (objectui#10035), and is forwarded to a host list view as refreshKey / refreshTrigger" — matches the code: the fetch effect names it, remountKey is applied for gantt/chart and the grid branch, and renderListView receives refreshTrigger: refreshKey and refreshKey.
  • A3 re-measured. useDataInvalidation readers in packages/*/src (non-test): RecordDetailView, useUserLocale, plugin-detail (DetailView, useRecordEditable), plugin-grid's useRecordCrudVerdicts (a permission answer); none of the rendered view components subscribes. useDataRefresh( has zero call sites outside its own test file. Consistent with the report.

② Semver level

patch for @object-ui/plugin-view and @object-ui/app-shell is appropriate. AGENTS.md's rule: a changeset is required whenever a fixed-group package's published src/ changes (line beginning "改完代码提交时:只要改了发版包 ... src/ 下的任何文件 ... 就必须新增一个 .changeset/*.md"), and "推论:changeset 里不要声明 major ... objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)". This is a behaviour fix with no declared key, schema or export moved and no breaking semantics, so patch is the right rung; major is absent, and check-changeset-no-major passed in CI.

③ Boundary flags

  • First body line is Part of #10035; no closing keyword (close/fix/resolve followed by an issue reference) in the title, body or the single commit message — regex scan over all three: 0 hits. The title's (objectui#10035) is a plain reference.
  • No model name or version in the PR title, body, commit subject, body or trailers (scan: 0 hits). The commit carries a bare co-author trailer for the assistant brand and a session-URL trailer; the PR body carries the product footer. No angle brackets in the PR body.
  • File surface matches the claim: packages/plugin-view/src/ObjectView.tsx (+37/−4), packages/app-shell/src/views/ObjectView.tsx (+65/−4), two new pins beside them, one docstring edit in the sibling ObjectView.refreshSignal.test.tsx (+3/−2), one changeset. packages/plugin-list/src/ListView.tsx is NOT touched (6 files total, none under plugin-list).
  • Pending changesets: 14 others mention remount, refreshKey, renderListView or renderContent; each read — they describe the relay's key set, the retired dead refresh callback (which still says refreshKey is bumped by onMutation, delete and form success — still true), or unrelated remounts. None claims the counter rides in the key, so none is made false.
  • Check-runs on 42737c85576c3ecd6e0451c0b7f87d7ac65cea4b: polled at 60 s; final at poll 2 — 43 total, 40 success, 3 skipped (Test (coverage), Test (coverage shard ${{ matrix.shard }}/4), dependabot — workflow-conditional skips), 0 failed, 0 in progress.
  • Deviation noted honestly by the dev: the dispatch asked for Fixes; the PR uses Part of because the card's defect still holds for gantt, chart and the standalone grid, and the remainder is reported as a sub-issue candidate in the os-dev-report.

Implemented-by: claude/issue-10035-objectview-refresh-not-remount
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

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.

2 participants