Repository navigation
fix(plugin-view,app-shell): refresh the view in place after a write instead of remounting it (objectui#10035) - #10378
Conversation
…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
|
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
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Part of #10035
Clause-②: no
What this changes
AGENTS.md commandment 8's corollary: refresh data, don't rebuild UI. Both
ObjectViewlayers carried their refresh counter in a Reactkey, 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-shellrenderListView:ListViewis keyed on object + view id only (identityKey), and the combined counter (page counter + plugin ObjectView counter) now reaches it asrefreshTrigger, whichListView's fetch effect already lists. Before this, the page's own counter (record actions, import, realtime, view edits, the console'sexternalRefreshKey) reachedListViewthrough the key alone, so dropping the key without the relay would have silently lost those refetches.@object-ui/plugin-viewrenderContent: 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 whenrefreshKeymoves.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_TYPESin plugin-view,REMOUNT_TO_REFRESH_VISUALIZATIONSin 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/maincff4b7754.renderListViewbranchrefreshKeyandschema.refreshTriggerObjectKanbandata(fetch effect namesrefreshKey); with external data it does not subscribe itself ("Parent handles refresh")ObjectCalendarObjectGallery/ObjectTimeline/ObjectMapprops.data; map's effect namesdataPropObjectTreedata, so a new array re-issues its queryObjectGanttobject-ganttrenderer hands it onlyschemaanddataSource; itsreloadeffect names no counter, noonMutation, no invalidation busObjectChartdatanor a refresh inputObjectGridonMutation, no refresh propchartObjectChartinSuspenseListViewfor grid / kanban / calendar / gallery / timeline / map / treeListViewrefreshTrigger(now the combined counter) + its ownonMutationsubscription; children drawListView's rows, tree re-queries on themListViewthat can draw gantt or chart (its ownviewType, or the author'sappearance.allowedVisualizationsthrough the in-list switcher)ObjectGantt/ObjectChartA2 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
notifyDataChangednor 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,ObjectMapandObjectTreedo not subscribe. (useDataRefresh, the name in the dispatch, exists in@object-ui/reactwith zerosrcconsumers.) So the smallest change is to keeprefreshKeyas the signal and relay it throughrefreshTrigger, the inputListViewalready reads — the same channel the Studio Data pillar'srenderStudioGridListrelies on.A5: no existing test asserted remount-on-refresh. One docstring sentence in
ObjectView.refreshSignal.test.tsxsaid 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 anonMutationwrite (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 andListView; anexternalRefreshKeybump (what the console sends after a record-form save, undo, redo) re-issues the list query exactly once (b) while a node read insideListViewis 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-replacewrap and proven by blob hash == HEAD and an emptygit diff HEAD, at42737c855:cff4b7754refreshTrigger: combinedRefreshKeydeletedrefreshKeyfindcalled once, expected twice).ablation-replacerefused it (replacement count rose by 0); rerun with a distinct replacementGates (all at
42737c855)pnpm exec vitest run packages/plugin-view/— 46 files / 428 tests passed.ObjectView, the new pin among them — 67 files / 698 tests passed (two runs: 52 + 15).turbo run build --filter='@object-ui/app-shell^...'(28 tasks) exit 0, thentype-check(tsc --noEmit && tsc -p tsconfig.test.json) exit 0 for@object-ui/plugin-viewand@object-ui/app-shell..ts/.tsxfiles,--format json: 5 files, 0 errors. Warnings sit on untouched lines of the two source files, plusno-explicit-anyin the new tests (same as the sibling refresh test). Why this narrowing is complete:eslint.config.jssets noparserOptions.project/projectServiceand no custom rule ineslint-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) andObjectChart(plugin-charts) read no refresh input, so a host can only show them a write by remounting them. The same gap exists insideListView: its own toolbar Refresh and itsonMutationrefetch never reach a gantt or chart visualization, because those query for themselves. Closing it means a child edit (for example, each of the three followinguseDataInvalidationor a host refresh counter), which is outside this claim's surface; the report names it for the seat.Acceptance notes
ListViewmay now fetch twice after a write through the data source (itsonMutationhandler plusrefreshTrigger); its own comment already records thatfind()coalesces concurrent identical reads.Changeset:
.changeset/10035-objectview-refresh-in-place.md,patchfor both packages.Session:
https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33CGenerated by Claude Code