diff --git a/.changeset/25992-objectview-page-header-overlay-split.md b/.changeset/25992-objectview-page-header-overlay-split.md new file mode 100644 index 00000000000..14a66af6d7c --- /dev/null +++ b/.changeset/25992-objectview-page-header-overlay-split.md @@ -0,0 +1,12 @@ +--- +'@object-ui/app-shell': patch +--- + +refactor(app-shell): the Console `ObjectView`'s page header and record overlay move into sibling sub-components + +Clause-②: no +Metadata-protocol: none + +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. + +What to do: nothing. diff --git a/ui/packages/app-shell/src/views/ObjectView.tsx b/ui/packages/app-shell/src/views/ObjectView.tsx index 657bf3eea4a..f469dfe870d 100644 --- a/ui/packages/app-shell/src/views/ObjectView.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.tsx @@ -9,7 +9,7 @@ * - ListView delegation for non-grid view types (kanban, calendar, chart, etc.) */ -import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, createElement, type ReactNode } from 'react'; +import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, type ReactNode } from 'react'; import { useParams, useSearchParams, useNavigate, useLocation, useHref } from 'react-router-dom'; import { DENSITY_MODE_TO_ROW_HEIGHT, normalizeListViewSchema, resolveAffordance, type FilterTokenScope, type SchemaLike } from '@object-ui/core'; import { buildListFilterKey, writeListFilterState } from './listFilterStorage.js'; @@ -27,20 +27,14 @@ import { ObjectView as PluginObjectView, ManageViewsDialog } from '@object-ui/pl // uses ComponentRegistry.registerLazy so heavy plugins stay code-split). // Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here. import { - Button, - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, NavigationOverlay, } from '@object-ui/components'; -import { Plus, Upload, MoreHorizontal, RotateCcw } from 'lucide-react'; -import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js'; +import { Plus } from 'lucide-react'; +import { useObjectListFavorite } from '../layout/SidebarItemMenu.js'; import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js'; import { useTenancyPosture } from '../hooks/useTenancyPosture.js'; import { useServedViewItems } from '../hooks/useServedViewItems.js'; import { viewTabLabel } from './viewTabLabel.js'; -import { getIcon } from '../utils/getIcon.js'; import type { DataSource, ListViewSchema, TreeViewConfig } from '@object-ui/types'; import { MetadataPanel, useMetadataInspector } from './MetadataInspector.js'; import { ViewConfigPanel } from './ViewConfigPanel.js'; @@ -53,10 +47,8 @@ import { PREVIEW_QUERY_FLAG, PREVIEW_QUERY_VALUE, } from '../preview/PreviewModeContext.js'; -import { PageHeader } from '../layout/PageHeader.js'; import { useMobileViewSwitcherRegistration } from '../layout/MobileViewSwitcherContext.js'; import type { MobileViewSwitcherItem } from '../layout/MobileViewSwitcherContext.js'; -import { ManagedByBadge } from '../components/ManagedByBadge.js'; import { isSetupAppSegment } from '../console/setupApp.js'; import { RecordDetailView } from './RecordDetailView.js'; import { IDENTITY_IMPORT_WRITE_MODES } from './identityImport.js'; @@ -71,10 +63,9 @@ import { useObjectActions } from '../hooks/useObjectActions.js'; import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n'; import { usePermissions } from '@object-ui/permissions'; import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth'; -import { ActionProvider, SchemaRenderer, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react'; +import { ActionProvider, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react'; import { toast } from 'sonner'; import { useConsoleActionRuntime } from '../hooks/useConsoleActionRuntime.js'; -import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar.js'; import { VIEW_TYPE_ICONS, FALLBACK_USER, @@ -108,6 +99,8 @@ import { useObjectViewListUrlState, useObjectViewUrlFilters } from './objectView import { useObjectViewTabBar } from './objectViewTabBar.js'; import { useObjectViewPluginSchema } from './objectViewPluginSchema.js'; import { useObjectViewHeaderChrome } from './objectViewHeaderChrome.js'; +import { ObjectViewPageHeader } from './objectViewPageHeader.js'; +import { ObjectViewRecordOverlay } from './objectViewRecordOverlay.js'; // objectstack#25992 slice 1: these module-level helpers moved to the sibling // `objectView*.ts` modules above. Every name this file exported stays @@ -1984,120 +1977,26 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co chrome that real-app conventions (Gmail / Notion / Linear) reclaim for content. The create / import / overflow actions move to a floating action button (see below). */} -
- } - subtitle={objectDef.description ? objectDesc(objectDef) : undefined} - // `createElement`, not a JSX tag: the looked-up icon is a value, - // which react-hooks/static-components refuses in tag position - // (the convention ObjectDataPage set). - icon={createElement(getIcon((objectDef as any)?.icon), { className: 'h-4 w-4' })} - actions={ - <> - {/* No favorite star here since objectui#12423: the one-row - header is icon, title, description and these actions. - The favorites entry is in the overflow menu at the end - of this row (objectui#12427). */} - - {/* Primary action (see `createButton`). */} - {createButton} - - {/* Data import — desktop only on phones. CSV imports - are inherently a desk/laptop workflow; the button - was eating header space on mobile next to the - primary "+" action. */} - {/* [#5142] `objectCanImport && importVisible` — the object-level - verdict, then the toolbar-scope `visibleWhen` layer on top of - it. Greyed, not gone, is the `disabledWhen` case below. */} - {importShown && ( - - )} - - {/* Schema-driven toolbar actions */} - {listActions?.some((a: any) => a.locations?.includes('list_toolbar')) && ( - isEnvironmentList ? ( - - ) : ( - - ) - )} - - {/* The page's overflow menu (objectui#12427), last in the - row: the favorites entry that replaced the header star. - Favorites is a once-and-forget act, so it lives behind - the ⋯ rather than in the header's most prominent slot. - "Reset view" (objectui#12418) is the same kind of act and - lives beside it, so the one-row header keeps its shape - as the user sorts. The ⋯ is drawn when either applies. */} - {(listFavorite || activeViewHasPreference) && ( - - - - - - {listFavorite && ( - - )} - {/* objectui#12418 — the user's own list state for - this view (sort, filters, column layout) back to - the view's. Only while they have stored some. */} - {activeViewHasPreference && ( - - - {t('console.objectView.resetView')} - - )} - - - )} - - {/* - Design Tools dropdown removed — its items were redundant: - - "Edit view" → use chevron menu's "Edit view config" - - "Add view" → use the `+` button on the tab bar or the "Add new view" footer in Manage Views - - "Metadata inspector" → still toggleable via the `?__debug` URL flag (see useMetadataInspector) - */} - - } - foldedActions={foldedHeaderActions} + {/* objectstack#25992 slice 10: the header moved to `./objectViewPageHeader.tsx`. */} + -
{/* Floating "+" action button — phone-only counterpart to the PageHeader's primary create action we just hid. Positioned @@ -2344,28 +2243,18 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co {/* Record Detail Overlay — navigation mode driven by objectDef.navigation */} {navOverlay.mode !== 'split' && ( - { if (!open) handleDrawerClose(); }} - title={objectLabel(objectDef)} - onExpand={handleExpandDrawer} - expandLabel={t('console.objectView.expandToPage', { defaultValue: 'Open as full page' })} - storageKey={`drawer-width:${objectDef.name}`} - > - {(record: Record) => { - const recordId = (record.id || record._id) as string; - return ( - - ); - }} - + /* objectstack#25992 slice 10: the overlay moved to `./objectViewRecordOverlay.tsx`. */ + )} {actionRuntime.dialogs} diff --git a/ui/packages/app-shell/src/views/objectViewPageHeader.tsx b/ui/packages/app-shell/src/views/objectViewPageHeader.tsx new file mode 100644 index 00000000000..d10a02b9516 --- /dev/null +++ b/ui/packages/app-shell/src/views/objectViewPageHeader.tsx @@ -0,0 +1,187 @@ +/** + * The page header of the Console `ObjectView` (`./ObjectView.tsx`): the + * object's plural title, managed-by badge, description and icon, with the + * header's actions (the create button, Import, the schema-driven toolbar and + * the overflow menu with favorites and "Reset view") and their folded face, + * moved out of `ObjectViewInner`'s JSX verbatim (objectstack#25992, slice 10). + * Every value is passed in as a prop. + */ + +import { createElement } from 'react'; +import type { Dispatch, SetStateAction } from 'react'; +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@object-ui/components'; +import { Upload, MoreHorizontal, RotateCcw } from 'lucide-react'; +import { SchemaRenderer } from '@object-ui/react'; +import type { useObjectLabel, useObjectTranslation } from '@object-ui/i18n'; +import { FavoriteMenuItem } from '../layout/SidebarItemMenu.js'; +import type { useObjectListFavorite } from '../layout/SidebarItemMenu.js'; +import { getIcon } from '../utils/getIcon.js'; +import { PageHeader } from '../layout/PageHeader.js'; +import { ManagedByBadge } from '../components/ManagedByBadge.js'; +import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar.js'; +import type { useObjectViewToolbarAffordances } from './objectViewToolbarAffordances.js'; +import type { useObjectViewHeaderToolbar } from './objectViewHeaderToolbar.js'; +import type { useObjectViewListUrlState } from './objectViewListUrlState.js'; +import type { useObjectViewHeaderChrome } from './objectViewHeaderChrome.js'; +import type { ConsoleObjectViewProps } from './ObjectView.js'; + +export function ObjectViewPageHeader({ + objectDef, + objectPluralLabel, + objectDesc, + createButton, + importShown, + setShowImport, + importDisabled, + t, + listActions, + isEnvironmentList, + headerToolbarActions, + environmentEntitlements, + toolbarBar, + listFavorite, + activeViewHasPreference, + handleResetView, + foldedHeaderActions, +}: { + objectDef: ConsoleObjectViewProps['objects'][number]; + objectPluralLabel: ReturnType['objectPluralLabel']; + objectDesc: ReturnType['objectDescription']; + createButton: ReturnType['createButton']; + importShown: ReturnType['importShown']; + setShowImport: Dispatch>; + importDisabled: ReturnType['importDisabled']; + t: ReturnType['t']; + listActions: ReturnType['listActions']; + isEnvironmentList: ReturnType['isEnvironmentList']; + headerToolbarActions: ReturnType['headerToolbarActions']; + environmentEntitlements: ReturnType['environmentEntitlements']; + toolbarBar: ReturnType['toolbarBar']; + listFavorite: ReturnType; + activeViewHasPreference: ReturnType['activeViewHasPreference']; + handleResetView: ReturnType['handleResetView']; + foldedHeaderActions: ReturnType['foldedHeaderActions']; +}) { + return ( +
+ } + subtitle={objectDef.description ? objectDesc(objectDef) : undefined} + // `createElement`, not a JSX tag: the looked-up icon is a value, + // which react-hooks/static-components refuses in tag position + // (the convention ObjectDataPage set). + icon={createElement(getIcon((objectDef as any)?.icon), { className: 'h-4 w-4' })} + actions={ + <> + {/* No favorite star here since objectui#12423: the one-row + header is icon, title, description and these actions. + The favorites entry is in the overflow menu at the end + of this row (objectui#12427). */} + + {/* Primary action (see `createButton`). */} + {createButton} + + {/* Data import — desktop only on phones. CSV imports + are inherently a desk/laptop workflow; the button + was eating header space on mobile next to the + primary "+" action. */} + {/* [#5142] `objectCanImport && importVisible` — the object-level + verdict, then the toolbar-scope `visibleWhen` layer on top of + it. Greyed, not gone, is the `disabledWhen` case below. */} + {importShown && ( + + )} + + {/* Schema-driven toolbar actions */} + {listActions?.some((a: any) => a.locations?.includes('list_toolbar')) && ( + isEnvironmentList ? ( + + ) : ( + + ) + )} + + {/* The page's overflow menu (objectui#12427), last in the + row: the favorites entry that replaced the header star. + Favorites is a once-and-forget act, so it lives behind + the ⋯ rather than in the header's most prominent slot. + "Reset view" (objectui#12418) is the same kind of act and + lives beside it, so the one-row header keeps its shape + as the user sorts. The ⋯ is drawn when either applies. */} + {(listFavorite || activeViewHasPreference) && ( + + + + + + {listFavorite && ( + + )} + {/* objectui#12418 — the user's own list state for + this view (sort, filters, column layout) back to + the view's. Only while they have stored some. */} + {activeViewHasPreference && ( + + + {t('console.objectView.resetView')} + + )} + + + )} + + {/* + Design Tools dropdown removed — its items were redundant: + - "Edit view" → use chevron menu's "Edit view config" + - "Add view" → use the `+` button on the tab bar or the "Add new view" footer in Manage Views + - "Metadata inspector" → still toggleable via the `?__debug` URL flag (see useMetadataInspector) + */} + + } + foldedActions={foldedHeaderActions} + /> +
+ ); +} diff --git a/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx b/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx new file mode 100644 index 00000000000..e62bd702ba5 --- /dev/null +++ b/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx @@ -0,0 +1,61 @@ +/** + * The record detail overlay of the Console `ObjectView` (`./ObjectView.tsx`) + * for every navigation mode but `split` (drawer, modal, popover): the + * `NavigationOverlay` that draws the opened record's `RecordDetailView`, moved + * out of `ObjectViewInner`'s JSX verbatim (objectstack#25992, slice 10). The + * `split` face stays in the component, where it wraps the list itself. Every + * value is passed in as a prop. + */ + +import { NavigationOverlay } from '@object-ui/components'; +import type { useObjectLabel, useObjectTranslation } from '@object-ui/i18n'; +import { RecordDetailView } from './RecordDetailView.js'; +import type { useObjectViewRecordNavigation } from './objectViewRecordNavigation.js'; +import type { ConsoleObjectViewProps } from './ObjectView.js'; + +export function ObjectViewRecordOverlay({ + navOverlay, + handleDrawerClose, + objectLabel, + objectDef, + handleExpandDrawer, + t, + dataSource, + objects, + onEdit, +}: { + navOverlay: ReturnType['navOverlay']; + handleDrawerClose: ReturnType['handleDrawerClose']; + objectLabel: ReturnType['objectLabel']; + objectDef: ConsoleObjectViewProps['objects'][number]; + handleExpandDrawer: ReturnType['handleExpandDrawer']; + t: ReturnType['t']; + dataSource: ConsoleObjectViewProps['dataSource']; + objects: ConsoleObjectViewProps['objects']; + onEdit: ConsoleObjectViewProps['onEdit']; +}) { + return ( + { if (!open) handleDrawerClose(); }} + title={objectLabel(objectDef)} + onExpand={handleExpandDrawer} + expandLabel={t('console.objectView.expandToPage', { defaultValue: 'Open as full page' })} + storageKey={`drawer-width:${objectDef.name}`} + > + {(record: Record) => { + const recordId = (record.id || record._id) as string; + return ( + + ); + }} + + ); +}