Skip to content

feat(plugin-gantt): refresh in place on a real query change; the initial load keeps the placeholder (objectui#7237) - #10539

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-7237-gantt-refresh-in-place
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-7237-gantt-refresh-in-place

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #7237
Clause-②: yes — published component behaviour changes: a real query change no longer tears the chart down to a placeholder.

This is the A′ half of ruling C+A′ (comment 5749198903, maintainer 「同意」). The C half landed as objectui#10407. This is the last half, hence line 1 above. Dispatched by the domain:ui seat 2 PM (claim 5828052135), session session_014mXUNuFomfj24w7s1pZzhN. The dispatch ran as mode:subagent, so that id is also this run's.

What changes

ObjectGantt's fetch effect ran every reload non-silently. Once the chart had painted, a real change to the query (sort, filter or permissions) flipped loading. The if (loading) early return then swapped GanttView for the "Loading Gantt chart..." placeholder, and the chart lost its scroll position, collapsed groups and in-flight edits.

  • reload takes two independent options. inPlace keeps the chart mounted and sets refreshing. silent logs a failure and keeps the last good rows, and implies inPlace. Every existing silent: true caller behaves as before: the toolbar refresh, the four write-readbacks and the invalidation-bus reload.
  • The first load is separated from later ones. loadedOnceRef flips once, when a reload first commits rows. The fetch effect runs the initial load exactly as before, with the placeholder. Every later run is in place but not silent: a changed query that fails is still reported as an error, as on base. It does not leave the previous query's rows on screen with nothing marking them stale.
  • A visible refreshing state on the chart. The chart pane draws the shared RefreshIndicator from @object-ui/components while refreshing is set. It is the same indeterminate bar that ObjectGrid, ListView and ObjectChart draw over their rows, so there is no second convention. The pane gains relative to anchor the bar. Every absolute element inside GanttView and ResourceWorkload already has a positioned ancestor of its own.
  • A localized accessible name. New key gantt.aria.refreshing, in GANTT_DEFAULT_TRANSLATIONS and in all ten locale packs. Each value is copied from that pack's existing list.refreshing, so no new translation was authored. The component's default English label would repeat the objectui#3546 class: an English accessible name in every language.
  • The dependency lists are unchanged. reload's list and the fetch effect's list are byte-identical to base.
  • The objectui#7231 guard test is regenerated. It produced its overlap from a post-paint filter change and asserted the placeholder over it, which is exactly the teardown this ruling removes. Its pairs are regenerated from the ones that still put each flag in play. Every guard assertion is kept, and a post-paint refreshing twin is added. The file header records why. Leg 2 and Leg 3 below show each case still fails when the guard is removed or narrowed to its own mode.

Files (15, +502/−48): packages/plugin-gantt/src/ObjectGantt.tsx, packages/plugin-gantt/src/useGanttTranslation.ts, new packages/plugin-gantt/src/ObjectGantt.refreshInPlace-7237.test.tsx, packages/plugin-gantt/src/ObjectGantt.staleReloadFinally-7231.test.tsx, packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts (one line each), and .changeset/7237-gantt-refresh-in-place.md (@object-ui/plugin-gantt minor, @object-ui/i18n patch).

Dispatch hypotheses, measured

  • H1 holds. Measured on origin/main efbf18de5, which is after objectui#10494. The file already had refreshing, reload({ silent }), the sequence guard and the silent invalidation reload. The fetch effect called reloadRef.current() non-silently. Measured on the base: a sort, filter or permissions change unmounts GanttView (the pins below).
  • H2: the existing affordance was not visible enough to reuse on its own. refreshing only disabled the toolbar refresh button and set aria-busy on it. That is a dimmed icon with no animation, and it is absent entirely when there is no onRefresh (the inline value provider). So this reuses the repo's existing RefreshIndicator and the existing refreshing flag. No visible string is added, only the accessible name.
  • H2 route changed. The dispatch suggested a literal reload({ silent: true }). The re-query is in place but not silent, because silent would keep the previous query's rows on a failure with nothing marking them stale. Pinned: Leg 4 below reds exactly that pin.
  • H3. The pins cover (a) to (d) as asked, and (a) and (b) are red on the base. The "2 → 1 identities" reading is not used.
  • H4. ObjectGantt's record fetch still lists schema.filter and schema.sort by identity. ListView's filter hold shields it today. Neither dependency list was widened or narrowed.

Pins, red on the base, green on the head

ObjectGantt.refreshInPlace-7237.test.tsx. GanttView is a stand-in with a once-per-mount instance id, and every find is held open by hand. Cases:

  • Sort, filter and permissions (through a real PermissionProvider), each asserting:

    • (a) the same GanttView instance while the query is in flight and after it returns;
    • (b) the refreshing state visible while it is in flight, gone after;
    • (c) the rows replaced by the answer.
  • The re-query carries the changed $orderby.

  • (d) Control: the initial load shows the placeholder and no refreshing state.

  • A failed re-query is reported, not hidden behind the previous rows.

  • The zh pack labels the bar 刷新中….

  • Base (pin commit 68e742a16, base source): Tests 4 failed | 3 passed (7). Readings:

    • (a) GanttView was unmounted by the re-query …: expected 'unmounted' to be '1'
    • (b) no visible refreshing state on the chart …: expected 'absent' to be 'present'
    • the re-query tore the chart down to the loading placeholder: expected 'Loading Gantt chart...' to be 'no placeholder'
    • (a) after the answer: not the GanttView that painted the first rows: expected '2' to be '1'
    • zh: expected undefined to be '刷新中…'

    The (d) control, the re-query-carries-the-query case and the failure case are green on base, as expected.

  • Head c1041d311: all 11 cases green across both files (Leg 5).

Reverse run and mutations

All from the committed head c1041d311. The two test files run with --reporter=verbose. Every leg restores from HEAD under a trap: blob cdd9bca99 equals HEAD, and git diff HEAD is empty. Legs 2 to 4 went through ablation-replace.mjs, which checks the anchor count (1 → 0) and that the blob moved before the run.

Leg Mutation Predicted red Observed
1 Whole fix reverted to base (12 files), tests kept. On disk: loadedOnceRef 0, RefreshIndicator 0 3 change kinds + zh + the 7231 post-paint twin Tests 5 failed | 6 passed (11), exactly those five
2 7231 finally guard removed (if (true …)) 7231 cases 1, 1b, 3 3 failed | 8 passed, exactly those three
3 Guard narrowed to its own mode 7231 case 3 only 1 failed | 10 passed
4 Re-query made silent The failure pin only 1 failed | 10 passed
5 None none 11 passed (11)

Gates (at c1041d311)

Derived by hand from objectui's package.json and .github/workflows/, because dispatch-gates.mjs answers only for objectstack.

  • pnpm exec vitest run packages/plugin-gantt/ packages/i18n/: Test Files 147 passed (147), Tests 1762 passed (1762), VERDICT command-exit 0.
  • turbo run build --filter='@object-ui/plugin-gantt^...' --concurrency=2: 12/12 tasks.
  • pnpm --filter @object-ui/plugin-gantt --filter @object-ui/i18n run type-check: exit 0. Both tsc --noEmit and tsconfig.test.json ran, and the latter includes src/**/*.test.tsx.
  • run lint on both packages: exit 0, 0 errors. Warnings are the existing ones (plugin-gantt 436, i18n 33). None sits on a line this diff added in ObjectGantt.tsx, and none is in a locale pack.
  • Exit 0, each:
    • changesets: check-changeset-presence, -no-major, -overwrite, -fixed, check:changeset-claims, check:pending-changeset-literals;
    • text: check:new-line-citations (0 new), check:control-bytes;
    • i18n: check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:i18n-dead-keys (the new key is not reported);
    • sources: check:unreferenced-sources, check:phantom-deps, check:self-import, check:esm-specifiers, check:side-effects-array;
    • tests: check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape.
  • check-eager-locale-catalogues: NOT MEASURED. It answered PREREQUISITE NOT MET because apps/console/dist/eager-closure.json needs a console production build, so it is left to the performance-budget workflow.
  • check:changeset-claims named three pending paragraphs that mention a touched file (6475, 6661, 6939). I re-read each: they cover getGanttConfig's key set, console.nav strings, and the record-source order. None is falsified.

Acceptance notes

  • Where the bar shows. It shows on every in-place re-read, not only on a changed query: the toolbar refresh, the write-readbacks and the invalidation reload. It reads the one refreshing flag the toolbar button already read. The changeset says so.
  • Changing the bound object. A change to the bound source on a painted chart also refreshes in place. No shipped host does that without remounting by key (census 5744524200), and the object-schema gate window before such a re-query shows no bar, as on base.
  • Docs. No README or docs page describes the gantt's loading or refresh behaviour, so none went false. The objectui#10035 change to the same seam added none either, so docs stayed off this file surface.
  • Out of scope, class (a). Once any load fails, error is never cleared by a later successful reload, so the chart stays on the error screen. Measured at head with a throwaway probe (deleted, never committed): a failed first load, then a sort change. The second find ran with $orderby {start_date: desc} and succeeded, and the reading was errorShown: true, chartMounted: false, recoveredRowShown: false. The first load is non-silent in both trees, so base behaves the same and this diff does not move it. Handed to the seat to file.

Generated by Claude Code

The A-prime half of ruling C+A-prime on objectui#7237. Once the chart has
painted, a real sort, filter or permissions change must keep GanttView
mounted, show a visible refreshing state on the chart while the query
runs, and replace the rows when it returns; the initial load keeps the
loading placeholder. Red on the base: the re-query unmounts GanttView
through the placeholder and nothing marks the refresh.

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Co-authored-by: Claude <noreply@anthropic.com>
The A-prime half of ruling C+A-prime on objectui#7237. The fetch effect
ran every reload non-silently, so a sort, filter or permissions change on
a painted chart flipped `loading` and the early return swapped GanttView
for the placeholder, losing scroll, collapsed groups and in-flight edits.

`reload` now takes two independent options: `inPlace` (keep the chart
mounted, set `refreshing`) and `silent` (a failure keeps the last good
rows; implies in place). The fetch effect runs the initial load with the
placeholder as before, and every run after rows have committed in place,
not silent: a changed query that fails is still reported. The chart
pane draws the shared RefreshIndicator while `refreshing` is set, named
by the new `gantt.aria.refreshing` key in all ten locale packs.

The objectui#7231 guard test generated its overlap from a post-paint
filter change, which no longer holds `loading`; its pairs are regenerated
from the ones that still put each flag in play, with every guard
assertion kept and a post-paint `refreshing` twin added.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 3 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/6475-gantt-block-face-declared.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    The gantt nested-block spelling of a gantt config ({ type: 'object-gantt', gantt: { … } }) had no mirror entry at all: it rode through BaseSchema's .passthrough() entirely unvalidated, and the published TypeScript never taught the shape either — an author writing gantt: { … } got no completion, no type checking, no error on a misspelt member. It was the 28th and last of the keys getGanttConfig (plugin-gantt/src/ObjectGantt.tsx) reads off the schema that objectui#6051 (PR feat(types,plugin-gantt): declare the flattened GanttConfig face ObjectGantt reads (#6051) #6472) did not declare — severed into its own card because, unlike the other 27, declaring it changes what a published tool refuses.

.changeset/6661-app-launcher-nav-menu-renderers.md

  • names en.ts → packages/i18n/src/locales/en.ts — edited by this change

    Three new strings — the launcher's and the menu's accessible names, and the menu's empty state — are declared under console.nav in en.ts and its nine sibling packs. An inline defaultValue alone is not a fix: it renders English at one call site and leaves the string untranslatable everywhere (objectui#3517).

.changeset/6939-objectql-record-source-refinement.md

  • names plugin-gantt/src/ObjectGantt.tsx → packages/plugin-gantt/src/ObjectGantt.tsx — edited by this change

    Both renderers resolve their records from one of three keys, in this order — getDataConfig in plugin-map/src/ObjectMap.tsx and plugin-gantt/src/ObjectGantt.tsx: data, then staticData, then objectName. Both mirrors required objectName alone, so a document authored on staticData drew correctly and was refused by safeValidateSchema — six catalog entries, three per component.

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 111fa4c94 (merge-base with origin/main): 14 file(s) changed outside .changeset/, read against 1427 pending declaration(s) that publish a body (2009 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3044.8 KB 3104.5 KB
Main entry chunk (gzip) 147.8 KB 350 KB
Entry file index-BK-fB4na.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) 27.13KB 7.95KB
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) 544.97KB 130.34KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.86KB 62.25KB
fields (index.js) 257.02KB 65.07KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
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) 39.40KB 12.91KB
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.28KB 11.09KB
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.33KB 3.07KB
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.01KB 3.93KB
plugin-calendar (index.js) 51.38KB 14.60KB
plugin-charts (index.js) 74.95KB 20.88KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.59KB 35.39KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.25KB 61.40KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 146.50KB 37.51KB
plugin-gantt (index.js) 169.64KB 41.92KB
plugin-grid (index.js) 215.49KB 58.91KB
plugin-kanban (index.js) 49.30KB 15.39KB
plugin-list (index.js) 114.55KB 28.29KB
plugin-map (index.js) 23.82KB 7.42KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 88.00KB 21.97KB
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.03KB 1.86KB
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: c1041d311423e550c3bee759149fd28eddc9b017

① Derived judgments

Head confirmed. git rev-parse refs/review/pr-10539 = c1041d311…; merge-base with main = efbf18de5; 2 commits on the branch (68e742a16 test-only, +288 lines = the new 7237 pin file only; c1041d311 the fix). 15 files, +502/-48, matches the PR body.

A′ as ruled (5749198903), read against the diff:

  • Initial load keeps the placeholder: loadedOnceRef starts false, the fetch effect calls reloadRef.current(loadedOnceRef.current ? { inPlace: true } : {}); the {} run takes setLoading(true) and the untouched if (loading) early return (head line 2020) still returns the placeholder. Pin (d) controls this and would go red if the ref started true.
  • Later real change: loadedOnceRef flips only inside the two isCurrent() commit branches (head 820, 910), so a query change BEFORE the first paint still runs non-in-place; after paint every effect run is inPlace ⇒ setRefreshing(true), loading never flips again, GanttView stays mounted. Visible state: RefreshIndicator active=refreshing ariaLabel=t('gantt.aria.refreshing') inside the chart pane, which gained relative (the component's own doc requires a positioned ancestor). Source read of GanttView.tsx (root relative, line 3634) and ResourceWorkload.tsx (rows position: relative) confirms no absolute child re-anchors; NavigationOverlay is a Sheet portal.
  • Dependency lists byte-identical: diff of both }, [adapterInputsKey… lines base vs head is empty (reload deps base 927 / head 951; effect deps base 975 / head 1011).
  • Races: sequence guard unchanged (seq/isCurrent), setData/setRowCeiling/setError all gated by isCurrent(), so a stale response cannot commit; the objectui#7231 finally still clears BOTH flags when current (head 936-940). refreshing cannot stick: the newest reload is always current at its own finally. Unmount mid-flight: identical to base (no mounted-ref on either tree; React 18 no-ops). One narrow edge, non-blocking: the "a failed changed query is reported" guarantee holds only while the changed query is the NEWEST reload. Because the chart now stays mounted during the refresh, a silent reload (write-readback after a drag, or the invalidation bus) can supersede it; if the changed query then fails it is dropped (else if (isCurrent()) false), and if that silent run also fails the previous query's rows remain with only a console.error. Double-failure ordering; not reachable on base only because base tore the chart down. Worth a line in the acceptance notes, not a stop.
  • Route deviation (in place, not silent): the ruling's execution line says "every reload after the first is silent with a refreshing state", but its body defines the observable as no teardown, visible refreshing state, rows replaced when the query returns; the card body itself names the stale-rows lie. The dev split silent (failure keeps last good rows) from inPlace (mount kept) and kept failure reporting for a changed query. That preserves every observable the ruling names and hides nothing; AGENTS.md #0.1 is about renderer-side metadata fallbacks and is not the governing rule here, but the repo's written stance against silent false-greens (§9) points the same way. Accepted as consistent with the ruling, not a contradiction. Pinned (the "failed re-query reports the error" case); Leg 4 is the dev's own words, not re-run here. Cost: a failed changed query lands on the pre-existing never-clearing error screen, exactly as a failed non-silent one did on base.
  • objectui#7231 test rewrite, per cell: it( 3 → 4, expect( 23 → 41, all three base case names kept. Case 1 moved its overlap pre-paint (both runs hold loading); every base assertion kept (placeholder up, stale settle does not release it, gantt-view null, fresh settle paints fresh rows, stale rows absent). Case 2 control: waitFor(gantt-view) then getByText(fresh) became waitFor(getByText(fresh)) (equivalent, the row renders inside the mocked view) plus a new data-refreshing=false assertion. Case 3 rebuilt pre-paint via notifyDataChanged (silent) then a query change (non-silent); all base assertions kept, plus indicator null and the mount load answering last. New 1b twin: both runs hold refreshing; after the stale settle it asserts data-refreshing=true and the indicator present, so an unguarded finally would fail it. Nothing weakened or deleted. Three other gantt tests reference the placeholder (invalidationRefetch-10035, contractEnvelope-6839, referenceArms-6837); none encodes the post-paint teardown.
  • Pins red on base for the right reason: base has RefreshIndicator count 0 in ObjectGantt.tsx and every effect reload was non-silent, so (a) remounts GanttView (instance id changes) and (b) has no indicator; zh red because the key did not exist. The pin commit is base source + the one new test file, as claimed.
  • i18n: key in GANTT_DEFAULT_TRANSLATIONS ('Refreshing…') and under gantt.aria in all 10 packs; scripted compare of each pack's list.refreshing to its new gantt.aria.refreshing: 10/10 SAME. useGanttTranslation resolves host-first per key, so zh yields 刷新中… (pinned, with a not-equal-to-English guard). Key parity is owned by all-locales-key-parity.test.ts; check:i18n-keys is the call-site gate (dev reports exit 0; own words).
  • Overlap: objectui#10534's locale hunks sit at @@ -3567…-3968; this PR's at 772…941: disjoint. /pulls/10425 returns Not Found and no open PR mentions 10425; the other 12 open PRs touch no locale pack and no plugin-gantt/src file. Main has 7 commits since merge-base and none touches any of the 15 files.

② Semver level

.changeset/config.json: one fixed group of 40 containing both @object-ui/plugin-gantt and @object-ui/i18n, so the group takes the highest declared bump either way. AGENTS.md §9 forbids only major (Changeset Bump Policy: success). minor on plugin-gantt is defensible: a new user-visible busy state (the card's facet ④ calls it a new declared surface) shipped as feat; precedent cuts the other way too (objectui#10035's in-place invalidation reload on plugin-gantt was patch), so patch would also have been consistent; not a blocker. patch on i18n matches precedent: of 24 i18n CHANGELOG paragraphs mentioning new keys, 21 sit under Patch Changes. Changeset prose, sentence by sentence: sort/filter/permissions/search/bound source are all in the effect's dependency list; scroll/collapsed groups/inline edit survive because GanttView is not remounted; "same RefreshIndicator that ObjectGrid, ListView and ObjectChart draw" verified by grep (ObjectGrid.tsx:5691, ListView.tsx:4828, ObjectChart.tsx:1516); the bar also shows on silent re-reads because silent implies inPlace; "the only sign of them was a disabled refresh button" true (GanttView.tsx 3526-3528, disabled/aria-busy); "a changed query that fails is still reported" true when it is the newest reload (edge above); "only a re-read of the same query keeps its last good rows" true; the i18n sentence true. No false sentence.

③ Boundary flags

  • CI on the head: 42 check runs, 29 success, 3 skipped (coverage lanes, dependabot), 10 in_progress: Type Check, Spec Main Shape Gate, Test shards 1/8 through 8/8. Lint, Build & E2E, Test (dist pins), Bundle Analysis, Line Citation Gate, Control Byte Scan and all five changeset gates are success. The unit suite has not finished on CI; the 147-file / 1762-test green is the dev's own reading.
  • Class-a finding is real at source and pre-existing: setError is called only on failure (base line 896, head 920) and setError(null) appears nowhere in either tree, so after one failed load a later successful reload still renders the error screen. Not created or worsened here; it is the screen a failed in-place changed query now lands on.
  • RefreshIndicator English default accessible name in siblings, verified by grep: ObjectGrid.tsx:5691, ListView.tsx:4828, ObjectChart.tsx:1516, ObjectDataTable.tsx:1053 all render it without ariaLabel (default "Refreshing"). Four, as reported. Out of scope.
  • PR is a draft, assignees empty (the dev's assign write was refused), labels plugin / tests / package: i18n. Body line 1 Fixes #7237, line 2 Clause-②: yes, both correct for the last half.
  • Nothing stops landing. Non-blocking notes for the seat: the superseded-failure edge in ①; a bound-source switch on a painted chart now keeps the old object's rows under the bar during the schema-gate window (dev's acceptance note; no shipped host does this without a key remount). Merge only once the 10 in_progress checks conclude.

Implemented-by: claude/issue-7237-gantt-refresh-in-place
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 07:55
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit db3896c Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-7237-gantt-refresh-in-place branch September 25, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant