Skip to content

Commit a7f4b99

Browse files
refactor(app-shell): move ObjectView's page header and record overlay into sibling sub-components (slice 10) (#26434)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9VtKf3F9dzJPJM4RRtABaEc)_ Part of #25992 (slice 10 of the move-only split; slices 1 to 9 landed as #26152, #26233, #26310, #26336, #26353, #26374, #26396 and #26412, and the card stays open for slice 11). Clause-②: no Metadata-protocol: none ## What moved Base `4ef75053` (slice 9 merged, latest `main` merged in; `main` has not touched `ObjectView.tsx` since). `ui/packages/app-shell/src/views/ObjectView.tsx` is 2374 lines there. Two blocks of `ObjectViewInner`'s JSX return move verbatim into sub-components in two new sibling modules. In each, every value the block read is passed as a prop, typed from its source (no `any`), the props keep the names the block used, no identifier is renamed, no context is added, and the rendered DOM is the same. ### Q-header: the page header → `objectViewPageHeader.tsx` Lines 1987–2100 (the `hidden sm:block` wrapper and its `PageHeader`) move into `ObjectViewPageHeader`, mounted in their place with the props `objectDef`, `objectPluralLabel`, `objectDesc`, `createButton`, `importShown`, `setShowImport`, `importDisabled`, `t`, `listActions`, `isEnvironmentList`, `headerToolbarActions`, `environmentEntitlements`, `toolbarBar`, `listFavorite`, `activeViewHasPreference`, `handleResetView` and `foldedHeaderActions`. That covers the plural title (objectui#11696), the managed-by badge (objectui#12194, #12606), the `createElement` icon, the create button, Import, the schema-driven toolbar or `EnvironmentListToolbar`, the overflow menu with favorites and "Reset view" (objectui#12427, #12418), and the folded face (objectui#12705). The "1. Header with breadcrumb + description" comment above it stays in place. ### Q-overlay: the record detail overlay → `objectViewRecordOverlay.tsx` Lines 2347–2368 (the `NavigationOverlay` drawn for every mode but `split`, with its `RecordDetailView` render function) move into `ObjectViewRecordOverlay`, with the props `navOverlay`, `handleDrawerClose`, `objectLabel`, `objectDef`, `handleExpandDrawer`, `t`, `dataSource`, `objects` and `onEdit`. The `navOverlay.mode !== 'split' &&` guard around it stays in the component. ### What stays in the return, and why - **The import wizard.** `plugin-form/src/affordanceGrantMap-12082.test.tsx` finds the `ImportWizard` mounts by walking JSX element names per file, and requires each mount to sit in a file the `listImport` site names (`CONSOLE_LIST` = `ObjectView.tsx`, which also reads the `listImport` row). A mount in a new file would fail that pin. Re-pointing it would change the site's `readBy` list rather than a path, so the block stays. - **The `split` face of the overlay.** It wraps the list itself (`mainContent` holds the `PluginObjectView`), so moving it would mean passing that subtree in as a prop, which changes lines. - The phone FAB, `ManageViewsDialog`, the managed-users invite line, the list area, `MetadataPanel`, `ViewConfigPanel` and `CreateViewDialog` stay. They are short, or they are the views' own config surface, which slice 11's handlers feed. ### Imports Each import moves with its only readers. `createElement`, `Button`, the four `DropdownMenu*` parts, `Upload`, `MoreHorizontal`, `RotateCcw`, `SchemaRenderer`, `FavoriteMenuItem`, `getIcon`, `PageHeader`, `ManagedByBadge` and `EnvironmentListToolbar` go to the header module. `NavigationOverlay` and `RecordDetailView` are still drawn by the `split` face, so the overlay module imports them too. Nothing new is exported from `ObjectView.tsx`, and the package entry is untouched. `ObjectView.tsx` goes from 2374 to 2263 lines. Move-only proof: the multiset of non-blank lines of the base `ObjectView.tsx` is contained in new `ObjectView.tsx` plus the two modules. The only base lines missing are four import lines that lost a moved name: the `react`, `lucide-react`, `../layout/SidebarItemMenu.js` and `@object-ui/react` imports. Each is replaced by its trimmed form. Removed outright are the `Button` and `DropdownMenu*` entries of the `@object-ui/components` list and the `getIcon`, `PageHeader`, `ManagedByBadge` and `EnvironmentListToolbar` imports, whose lines reappear verbatim in the header module. The added lines are the trimmed imports and the two new imports, the module headers and imports, the component signatures and prop types, `return (` / `);`, and in `ObjectView.tsx` the two mounts with their props and one-line comments. ## Test edits None. No source reader of `ObjectView.tsx` names a moved line. The readers that mention `PageHeader`, `NavigationOverlay`, `RecordDetailView` or `EnvironmentListToolbar` read other files, mock the module, or assert an absence (`navRunAction.privateConventionRetired`: no `runAction` literal, which holds in every file). The `affordanceGrantMap-12082` mount walk sees no host-gated component mounted in either new module. ## Seam table rows this PR changes (measured on `4ef75053`) The full table (regions A to Q) is in #26336. | # | Concern | Lines on `4ef75053` | Reads | Read by | Slice | |---|---|---|---|---|---| | Q-header | `PageHeader`: title, badge, icon, create, Import, toolbar, overflow menu, folded face | 1987–2100 | A, C, D, E (`setShowImport`), G, H, P | — | **10, this PR** | | Q-overlay | `NavigationOverlay` for every mode but `split` | 2347–2368 | A, C, L | — | **10, this PR** | | Q (rest) | Providers, FAB, import wizard, tab row, dialogs, list area, `split` overlay, config panel | 1968–2346 less Q-header | everything | — | stays (see above) | ## Remaining slice (closes the card) - **Slice 11 — B + I + O + G-columns:** the view-config and view-tab CRUD handlers, `renderListView` and the column flags only it reads. The `writeRefusalCensus-11583` rows, the `relayRungCensus-7559` read, the `check-i18n-call-site-keys.test.ts` `deleteViewConfirm` row and the `tree-view-config-readers-8253` `CONSOLE_COMPOSITION` read of the tree `labelField` rung are re-pointed (path and file-key changes only). ## Verification See the os-dev-report comment on the card for the full readings: app-shell `type-check` after rebuilding its dependency closure, every `ObjectView.*` test in `views/` (header fold, favorites entry, list title plural, managed-by badge, setup list chrome, import and create predicates, record navigation and overlay tests among them), the `AppContent*` tests, every other test that reads `ObjectView.tsx` as source, eslint on the 3 touched files, the derived dispatch gates, and a Console build. ## Acceptance notes - eslint: 0 errors, no new warning. The three files report 110 warnings, the same count as the base `ObjectView.tsx` alone; the two `exhaustive-deps` warnings on `handleViewChange` only moved lines. Changeset: `.changeset/25992-objectview-page-header-overlay-split.md` (patch, `@object-ui/app-shell`). --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QciHBvFJXnhZqXmdqpDtpz --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 11cb1d5 commit a7f4b99

4 files changed

Lines changed: 297 additions & 148 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
refactor(app-shell): the Console `ObjectView`'s page header and record overlay move into sibling sub-components
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Two blocks of `ObjectViewInner`'s JSX in `views/ObjectView.tsx` move verbatim into sub-components. The page header (title, managed-by badge, icon, create button, Import, the toolbar actions, the overflow menu and the folded face) moves into `ObjectViewPageHeader` in `objectViewPageHeader.tsx`. The record detail overlay drawn for every navigation mode but `split` moves into `ObjectViewRecordOverlay` in `objectViewRecordOverlay.tsx`. The component passes every value in as a prop. No logic, name or rendered output changes, and the package entry is unchanged.
11+
12+
What to do: nothing.

‎ui/packages/app-shell/src/views/ObjectView.tsx‎

Lines changed: 37 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
1010
*/
1111

12-
import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, createElement, type ReactNode } from 'react';
12+
import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, type ReactNode } from 'react';
1313
import { useParams, useSearchParams, useNavigate, useLocation, useHref } from 'react-router-dom';
1414
import { DENSITY_MODE_TO_ROW_HEIGHT, normalizeListViewSchema, resolveAffordance, type FilterTokenScope, type SchemaLike } from '@object-ui/core';
1515
import { buildListFilterKey, writeListFilterState } from './listFilterStorage.js';
@@ -27,20 +27,14 @@ import { ObjectView as PluginObjectView, ManageViewsDialog } from '@object-ui/pl
2727
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
2828
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
2929
import {
30-
Button,
31-
DropdownMenu,
32-
DropdownMenuContent,
33-
DropdownMenuItem,
34-
DropdownMenuTrigger,
3530
NavigationOverlay,
3631
} from '@object-ui/components';
37-
import { Plus, Upload, MoreHorizontal, RotateCcw } from 'lucide-react';
38-
import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js';
32+
import { Plus } from 'lucide-react';
33+
import { useObjectListFavorite } from '../layout/SidebarItemMenu.js';
3934
import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js';
4035
import { useTenancyPosture } from '../hooks/useTenancyPosture.js';
4136
import { useServedViewItems } from '../hooks/useServedViewItems.js';
4237
import { viewTabLabel } from './viewTabLabel.js';
43-
import { getIcon } from '../utils/getIcon.js';
4438
import type { DataSource, ListViewSchema, TreeViewConfig } from '@object-ui/types';
4539
import { MetadataPanel, useMetadataInspector } from './MetadataInspector.js';
4640
import { ViewConfigPanel } from './ViewConfigPanel.js';
@@ -53,10 +47,8 @@ import {
5347
PREVIEW_QUERY_FLAG,
5448
PREVIEW_QUERY_VALUE,
5549
} from '../preview/PreviewModeContext.js';
56-
import { PageHeader } from '../layout/PageHeader.js';
5750
import { useMobileViewSwitcherRegistration } from '../layout/MobileViewSwitcherContext.js';
5851
import type { MobileViewSwitcherItem } from '../layout/MobileViewSwitcherContext.js';
59-
import { ManagedByBadge } from '../components/ManagedByBadge.js';
6052
import { isSetupAppSegment } from '../console/setupApp.js';
6153
import { RecordDetailView } from './RecordDetailView.js';
6254
import { IDENTITY_IMPORT_WRITE_MODES } from './identityImport.js';
@@ -71,10 +63,9 @@ import { useObjectActions } from '../hooks/useObjectActions.js';
7163
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
7264
import { usePermissions } from '@object-ui/permissions';
7365
import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth';
74-
import { ActionProvider, SchemaRenderer, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react';
66+
import { ActionProvider, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react';
7567
import { toast } from 'sonner';
7668
import { useConsoleActionRuntime } from '../hooks/useConsoleActionRuntime.js';
77-
import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar.js';
7869
import {
7970
VIEW_TYPE_ICONS,
8071
FALLBACK_USER,
@@ -108,6 +99,8 @@ import { useObjectViewListUrlState, useObjectViewUrlFilters } from './objectView
10899
import { useObjectViewTabBar } from './objectViewTabBar.js';
109100
import { useObjectViewPluginSchema } from './objectViewPluginSchema.js';
110101
import { useObjectViewHeaderChrome } from './objectViewHeaderChrome.js';
102+
import { ObjectViewPageHeader } from './objectViewPageHeader.js';
103+
import { ObjectViewRecordOverlay } from './objectViewRecordOverlay.js';
111104

112105
// objectstack#25992 slice 1: these module-level helpers moved to the sibling
113106
// `objectView*.ts` modules above. Every name this file exported stays
@@ -1984,120 +1977,26 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
19841977
chrome that real-app conventions (Gmail / Notion / Linear)
19851978
reclaim for content. The create / import / overflow
19861979
actions move to a floating action button (see below). */}
1987-
<div className="hidden sm:block">
1988-
<PageHeader
1989-
// The page lists the object's records, so it is titled with
1990-
// the plural, as the nav entry that opens it is
1991-
// (objectui#11696); the record page keeps the singular.
1992-
title={objectPluralLabel(objectDef)}
1993-
// The kind chip, in the header's own slot after the title
1994-
// (objectui#12606).
1995-
badge={<ManagedByBadge managedBy={(objectDef as any)?.managedBy} />}
1996-
subtitle={objectDef.description ? objectDesc(objectDef) : undefined}
1997-
// `createElement`, not a JSX tag: the looked-up icon is a value,
1998-
// which react-hooks/static-components refuses in tag position
1999-
// (the convention ObjectDataPage set).
2000-
icon={createElement(getIcon((objectDef as any)?.icon), { className: 'h-4 w-4' })}
2001-
actions={
2002-
<>
2003-
{/* No favorite star here since objectui#12423: the one-row
2004-
header is icon, title, description and these actions.
2005-
The favorites entry is in the overflow menu at the end
2006-
of this row (objectui#12427). */}
2007-
2008-
{/* Primary action (see `createButton`). */}
2009-
{createButton}
2010-
2011-
{/* Data import — desktop only on phones. CSV imports
2012-
are inherently a desk/laptop workflow; the button
2013-
was eating header space on mobile next to the
2014-
primary "+" action. */}
2015-
{/* [#5142] `objectCanImport && importVisible` — the object-level
2016-
verdict, then the toolbar-scope `visibleWhen` layer on top of
2017-
it. Greyed, not gone, is the `disabledWhen` case below. */}
2018-
{importShown && (
2019-
<Button
2020-
size="sm"
2021-
variant="outline"
2022-
onClick={() => setShowImport(true)}
2023-
disabled={importDisabled}
2024-
className="hidden sm:inline-flex shadow-none gap-1.5 sm:gap-2 h-8 sm:h-9"
2025-
title={t('console.objectView.importTitle')}
2026-
data-testid="object-view-import-button"
2027-
>
2028-
<Upload className="h-4 w-4" />
2029-
<span className="hidden sm:inline">{t('console.objectView.import')}</span>
2030-
</Button>
2031-
)}
2032-
2033-
{/* Schema-driven toolbar actions */}
2034-
{listActions?.some((a: any) => a.locations?.includes('list_toolbar')) && (
2035-
isEnvironmentList ? (
2036-
<EnvironmentListToolbar
2037-
actions={headerToolbarActions}
2038-
entitlements={environmentEntitlements}
2039-
/>
2040-
) : (
2041-
<SchemaRenderer schema={toolbarBar} />
2042-
)
2043-
)}
2044-
2045-
{/* The page's overflow menu (objectui#12427), last in the
2046-
row: the favorites entry that replaced the header star.
2047-
Favorites is a once-and-forget act, so it lives behind
2048-
the ⋯ rather than in the header's most prominent slot.
2049-
"Reset view" (objectui#12418) is the same kind of act and
2050-
lives beside it, so the one-row header keeps its shape
2051-
as the user sorts. The ⋯ is drawn when either applies. */}
2052-
{(listFavorite || activeViewHasPreference) && (
2053-
<DropdownMenu>
2054-
<DropdownMenuTrigger asChild>
2055-
<Button
2056-
size="sm"
2057-
variant="ghost"
2058-
className="shadow-none h-8 sm:h-9 px-2"
2059-
aria-label={t('detail.moreActions', { defaultValue: 'More actions' })}
2060-
data-testid="object-view-more-actions"
2061-
>
2062-
<MoreHorizontal className="h-4 w-4" />
2063-
</Button>
2064-
</DropdownMenuTrigger>
2065-
<DropdownMenuContent align="end">
2066-
{listFavorite && (
2067-
<FavoriteMenuItem
2068-
isFavorite={listFavorite.isFavorite}
2069-
onToggle={listFavorite.toggle}
2070-
testId="object-view-favorite"
2071-
/>
2072-
)}
2073-
{/* objectui#12418 — the user's own list state for
2074-
this view (sort, filters, column layout) back to
2075-
the view's. Only while they have stored some. */}
2076-
{activeViewHasPreference && (
2077-
<DropdownMenuItem
2078-
onSelect={handleResetView}
2079-
title={t('console.objectView.resetViewTitle')}
2080-
data-testid="object-view-reset-view-button"
2081-
>
2082-
<RotateCcw className="h-4 w-4" />
2083-
<span>{t('console.objectView.resetView')}</span>
2084-
</DropdownMenuItem>
2085-
)}
2086-
</DropdownMenuContent>
2087-
</DropdownMenu>
2088-
)}
2089-
2090-
{/*
2091-
Design Tools dropdown removed — its items were redundant:
2092-
- "Edit view" → use chevron menu's "Edit view config"
2093-
- "Add view" → use the `+` button on the tab bar or the "Add new view" footer in Manage Views
2094-
- "Metadata inspector" → still toggleable via the `?__debug` URL flag (see useMetadataInspector)
2095-
*/}
2096-
</>
2097-
}
2098-
foldedActions={foldedHeaderActions}
1980+
{/* objectstack#25992 slice 10: the header moved to `./objectViewPageHeader.tsx`. */}
1981+
<ObjectViewPageHeader
1982+
objectDef={objectDef}
1983+
objectPluralLabel={objectPluralLabel}
1984+
objectDesc={objectDesc}
1985+
createButton={createButton}
1986+
importShown={importShown}
1987+
setShowImport={setShowImport}
1988+
importDisabled={importDisabled}
1989+
t={t}
1990+
listActions={listActions}
1991+
isEnvironmentList={isEnvironmentList}
1992+
headerToolbarActions={headerToolbarActions}
1993+
environmentEntitlements={environmentEntitlements}
1994+
toolbarBar={toolbarBar}
1995+
listFavorite={listFavorite}
1996+
activeViewHasPreference={activeViewHasPreference}
1997+
handleResetView={handleResetView}
1998+
foldedHeaderActions={foldedHeaderActions}
20991999
/>
2100-
</div>
21012000

21022001
{/* Floating "+" action button — phone-only counterpart to the
21032002
PageHeader's primary create action we just hid. Positioned
@@ -2344,28 +2243,18 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
23442243

23452244
{/* Record Detail Overlay — navigation mode driven by objectDef.navigation */}
23462245
{navOverlay.mode !== 'split' && (
2347-
<NavigationOverlay
2348-
{...navOverlay}
2349-
setIsOpen={(open: boolean) => { if (!open) handleDrawerClose(); }}
2350-
title={objectLabel(objectDef)}
2351-
onExpand={handleExpandDrawer}
2352-
expandLabel={t('console.objectView.expandToPage', { defaultValue: 'Open as full page' })}
2353-
storageKey={`drawer-width:${objectDef.name}`}
2354-
>
2355-
{(record: Record<string, unknown>) => {
2356-
const recordId = (record.id || record._id) as string;
2357-
return (
2358-
<RecordDetailView
2359-
dataSource={dataSource}
2360-
objects={objects}
2361-
onEdit={onEdit}
2362-
objectNameOverride={objectDef.name}
2363-
recordIdOverride={recordId}
2364-
embedded
2365-
/>
2366-
);
2367-
}}
2368-
</NavigationOverlay>
2246+
/* objectstack#25992 slice 10: the overlay moved to `./objectViewRecordOverlay.tsx`. */
2247+
<ObjectViewRecordOverlay
2248+
navOverlay={navOverlay}
2249+
handleDrawerClose={handleDrawerClose}
2250+
objectLabel={objectLabel}
2251+
objectDef={objectDef}
2252+
handleExpandDrawer={handleExpandDrawer}
2253+
t={t}
2254+
dataSource={dataSource}
2255+
objects={objects}
2256+
onEdit={onEdit}
2257+
/>
23692258
)}
23702259
</div>
23712260
{actionRuntime.dialogs}

0 commit comments

Comments
 (0)