Skip to content

plugin-view: drop the unreferenced handleRefresh from ObjectView - #5568

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-4568-objectview-dead-refresh
Aug 21, 2026
Merged

plugin-view: drop the unreferenced handleRefresh from ObjectView#5568
os-sales merged 1 commit into
mainfrom
claude/issue-4568-objectview-dead-refresh

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#4568

ObjectView declared a handleRefresh callback that bumped refreshKey and was referenced by nothing — not passed to the toolbar, not exposed on a handle, not wired to any control. It advertised a refresh entry point the component does not have, which cost #4549 a detour to rule out.

Triage ruled deletion rather than wiring it to a new toolbar Refresh button: the button is a feature with no recorded pull, and the affordance already exists downstream. That ruling is not re-litigated here.

The reference surface, proven empty before deleting

A dead-code deletion is mechanical only after the reference surface is proven empty, so every zero-hit below is paired with a control probe on a term known to be present in the same scope — a bare zero-hit proves nothing about the search itself. All measured on the merged ref at claim time (f24195aeb).

1. Whole repo, all tracked files.git grep -n "handleRefresh" -- . returns 17 hits across 7 files. Six of those files (app-shell/.../ExternalDatasourcePanel.tsx, plugin-ai/AIFormAssist.tsx, plugin-dashboard/DashboardGridLayout.tsx, plugin-dashboard/DashboardRenderer.tsx, plugin-report/ReportViewer.tsx, plugin-timeline/ObjectTimeline.tsx) declare their own module-local handleRefresh and each wires it to something; none of them can see this one. In packages/plugin-view the identifier appears exactly once — the declaration.

Control: the same command shape for handleDelete returns hits across apps/, packages/, tests and CHANGELOGs, so the scope is live and repo-wide. The sharpest contrast is in this very file: handleDelete shows a declaration (:817) and a use (:1501); handleRefresh showed a declaration and nothing.

2. Untracked files too. A filesystem grep -rn excluding node_modules/.git/dist/.turbo gives the same 17 hits, 1 in plugin-view. Control:setRefreshKey in the identical scope returns 6 hits inside the same file, so the scope reaches the file's interior rather than stopping at its name.

3. Case-insensitive.git grep -in "handlerefresh" -- packages/plugin-view/ returns only the declaration. Control:git grep -in "HANDLEREFRESH" matches that same mixed-case line, proving -i was actually in effect.

4. Not exported — the public-surface flip condition does not trigger.handleRefresh was a const inside the ObjectView component body (component opens at :509), so it is lexically unreachable from outside. The package's only entry point is .dist/index.*, built from src/index.tsx, and that barrel re-exports ObjectView, ViewSwitcher, FilterUI, SortUI, SharedViewLink, ViewTabBar, ManageViewsDialog, the recordSurface helpers and the view-config-utils helpers — no handleRefresh. This is an internal cleanup, not a public-surface removal.

Where the deletion stopped, and what did not fall out

Nothing was orphaned. useCallback still has 13 uses in the file, setRefreshKey still has 5, and refreshKey itself keeps four live readers (the non-grid fetch effect's dep array :709, the child-view remount key :1356, and the refreshTrigger / refreshKey props forwarded to a host list view). No import became unused; no helper became unreferenced. The diff is exactly the five deleted lines plus a test and a changeset.

Where the real refresh button lives

Not in this component. packages/plugin-list/src/ListView.tsx renders a toolbar Refresh button gated on toolbarFlags.showRefresh (derived from userActions.refresh, :796) driving ListView's own counter (:3080). ObjectView reaches it by delegating through renderListView, and deliberately skips its own mutation auto-subscribe in that mode. That delegation is why the deleted callback never had a toolbar to attach to.

Test

packages/plugin-view/src/__tests__/ObjectView.refreshSignal.test.tsx is new. It pins the paths that survive rather than asserting the absence of the one that went — the package previously had zero coverage of refresh or onMutation at all:

  • a mutation reported on the same object re-fetches;
  • a mutation on a different object does not (the event.resource === schema.objectName gate);
  • a host-supplied renderListView suppresses the auto-subscribe entirely.

Reverse-verified. Direction predicted before running: test 1 only should go red. Ablating the setRefreshKey call inside the onMutation handler (mutation confirmed on disk by grepping the injected marker — 1 — and the anchor text, setRefreshKey 5→4) gave Tests 1 failed | 2 passed, exit 1 — exactly the predicted direction. The restore leg is proven byte-identical to the commit (marker absent, setRefreshKey back to 5, empty git diff against HEAD). The mutation and restore legs needed no rebuild: the test imports ../ObjectView by relative path, so it resolves to source and never reads dist/.

Gates

All run on the final commit f882f07a9, working tree byte-identical to it; each verdict quoted from the gate's own output, with exit codes captured before any pipe.

GateResult
vitest run packages/plugin-view/Test Files 18 passed (18) / Tests 183 passed (183)
turbo run type-check --filter @object-ui/plugin-view16 successful, 16 total (includes tsconfig.test.json)
turbo run lint --filter @object-ui/plugin-view2 successful, 2 total; 224 problems (0 errors, 224 warnings) — all warnings pre-existing except 2 no-explicit-any in the new test, matching the sibling ObjectView.kanbanConditionalFormatting.test.tsx convention for test doubles
check:control-bytesOK (scanned 4625 tracked text file(s))
check-changeset-presence2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-majorNo changeset declares a major bump.
check-changeset-fixedAll workspace packages are in the changeset fixed group.
check:self-importNo package names itself inside its own src/.
check:phantom-depsEvery in-scope import is declared by the package that publishes it.
check:published-distNo published package's build output carries tooling material. (full 39-package build, 221s — independently proves plugin-view builds clean after the deletion)

The changeset has empty frontmatter: this is a purely internal deletion with no released behaviour change, and the gate confirms that form is "the explicit exemption and a complete answer to this gate."

Declared narrowing. Repo-wide pnpm lint (turbo run lint across all 44 packages) was not run locally; CI runs the farm regardless. The narrowing is a measurement, not a gap: (1) the changed-file population is the complete git diff --name-only against the merge-base — three files, all in packages/plugin-view/src/ and .changeset/; (2) the file count comes from eslint's own --format json output, 33 files linted in @object-ui/plugin-view, 0 errors; (3) eslint.config.js has zero matches for projectService/parserOptions, so type-aware linting is not enabled and this diff cannot move a verdict in any file it does not itself contain.

Out of scope

While proving the reference surface I measured a second, adjacent dead declaration and filed it separately as #5567 rather than fixing it here: the object-view registry declares showRefresh as a designer input, defaults it to true, and the docs describe it as a working toolbar toggle — but ObjectView never reads it and does not forward it. That key lives in index.tsx (outside this card's file surface) and is authorable surface, so its disposition is a decision rather than a cleanup. Not addressed here.


Generated by Claude Code

…tView
`ObjectView` declared a `handleRefresh` callback that bumped `refreshKey` and
was referenced by nothing — not passed to the toolbar, not exposed on a handle,
not wired to any control. It advertised a refresh entry point the component does
not have, which cost objectui#4549 a detour to rule out.
Triage ruled deletion rather than wiring it to a new toolbar Refresh button: the
button is a feature with no recorded pull, and the affordance already exists
downstream in `plugin-list`'s `ListView`, which ObjectView reaches by delegating
through `renderListView`.
Reference surface verified empty on the merged ref before deleting: the
identifier appears exactly once in `packages/plugin-view` (the declaration), is
a component-local `const` that no barrel re-exports, and the package's only
entry point is `src/index.tsx`. Nothing else fell out — `useCallback`,
`setRefreshKey` and `refreshKey` all keep live readers.
Adds `ObjectView.refreshSignal.test.tsx`, pinning the refresh paths that
survive: a same-object mutation re-fetches, a different-object mutation does
not, and a host-supplied list view suppresses the auto-subscribe.
Part of #4568
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3784.8 KB3867.2 KB
Main entry chunk (gzip)151.2 KB350 KB
Entry fileindex-Ca1X-pSF.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 (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
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)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
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.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
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)506.94KB113.63KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)237.61KB59.63KB
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.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
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.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
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.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.51KB32.96KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
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.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.48KB20.67KB
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)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
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-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
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 (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
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

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 13:36
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit 993ce05Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4568-objectview-dead-refresh branch August 21, 2026 13:36
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-view ObjectView: handleRefresh is declared and never referenced — a dead refresh entry point

2 participants

@os-sales@claude