diff --git a/.changeset/25992-objectview-chrome-split.md b/.changeset/25992-objectview-chrome-split.md new file mode 100644 index 00000000000..c1265ee3644 --- /dev/null +++ b/.changeset/25992-objectview-chrome-split.md @@ -0,0 +1,12 @@ +--- +'@object-ui/app-shell': patch +--- + +refactor(app-shell): the Console `ObjectView`'s leaf pieces move into a sibling module + +Clause-②: no +Metadata-protocol: none + +The "object not found" state (`ObjectNotFoundState`), the viewport check that decides whether the saved-view tab strip is drawn, and the inline-edit toggle's session-only callback move verbatim out of `views/ObjectView.tsx` into `objectViewChrome.tsx` beside it. No logic, name or rendered output changes. `ObjectView.tsx` re-exports every name it exported before, 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 c80dfb5da63..455555f6f6d 100644 --- a/ui/packages/app-shell/src/views/ObjectView.tsx +++ b/ui/packages/app-shell/src/views/ObjectView.tsx @@ -49,7 +49,7 @@ import { EmptyDescription, NavigationOverlay, } from '@object-ui/components'; -import { Plus, Upload, Table as TableIcon, Lock, MoreHorizontal, RotateCcw } from 'lucide-react'; +import { Plus, Upload, Lock, MoreHorizontal, RotateCcw } from 'lucide-react'; import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js'; import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js'; import { useTenancyPosture } from '../hooks/useTenancyPosture.js'; @@ -125,6 +125,7 @@ import { isSameOptionsValue, dispatchViewPatches, } from './objectViewOptions.js'; +import { ObjectNotFoundState, useViewTabsFit, keepInlineEditModeForTheSession } from './objectViewChrome.js'; // objectstack#25992 slice 1: these module-level helpers moved to the sibling // `objectView*.ts` modules above. Every name this file exported stays @@ -155,6 +156,10 @@ export { dispatchViewPatches, } from './objectViewOptions.js'; +// objectstack#25992 slice 3: `ObjectNotFoundState` moved to `./objectViewChrome.tsx` +// and stays exported from here (`AppContent` imports it by this path). +export { ObjectNotFoundState } from './objectViewChrome.js'; + /** * Props of the Console `ObjectView`, the route-level object surface * `@object-ui/app-shell` exports (objectui#7483). @@ -186,94 +191,6 @@ export interface ConsoleObjectViewProps { externalRefreshKey?: number; } -/** - * The "object not found" state the route draws for an object it has no - * definition of. objectui#13052 — `packageDisabled`: the server answered that - * the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`), - * so the description says that instead. The console route draws it for that - * answer (`AppContent`); this module's `ObjectView` draws it for an absence. - * Not part of the package entry. - */ -export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) { - const { t } = useObjectTranslation(); - return ( -
- -
- -
- {t('console.objectView.objectNotFound')} - - {packageDisabled - ? t('console.objectView.objectPackageDisabled', { objectName }) - : t('console.objectView.objectNotFoundDescription', { objectName })} - {' '} - {t('console.objectView.objectNotFoundHint')} - -
-
- ); -} - -/** - * The list toolbar's inline-edit toggle, as this page wires it: it writes - * nothing (objectui#5144, triage's ruling E). - * - * The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's - * permission keys, and `normalizeListViewSchema` folds the first into the - * second. `editInline` is on by default since the v18 ruling (objectstack#22605, - * objectui#12086), so a view that declares neither key offers the toggle, and - * `editInline: false` withdraws it. This toggle used to persist a USER's edit - * mode into `inlineEdit` through `persistViewPatch`. After the fold, switching - * it off stored `inlineEdit: false`, which reads as "not offered", so the - * toggle was gone from the next load and nothing in the console could bring it - * back. - * - * The edit mode is now session state. `ListView` keeps it, and seeds it from - * the view's `inlineEdit` on each load. The callback stays wired because - * `ListView` offers the wide toolbar toggle only to a host that wires one. - * - * Recorded costs: the edit mode is not remembered across loads, and an overlay - * that already stores `inlineEdit: false` still reads off. That is existing - * data, and the maintainer's ruling rejects migrating it. Declaring - * `userActions.editInline: true` on such a view brings the toggle back. - */ -/** - * Tailwind's `sm` breakpoint (px): from here up the list page draws its - * saved-view tabs in the list toolbar; below it the app bar's mobile view - * switcher is the page's only view control (objectui#12440). - */ -const VIEW_TABS_MIN_WIDTH = 640; - -/** - * Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia - * idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is - * read synchronously: the tab strip is the first thing on the page's list row, - * and resolving it in an effect would draw the row once without it and then - * jump. objectui#12440 — this decides whether the strip is HANDED to the list - * at all, so below `sm` nothing is passed and nothing is drawn (the CSS - * `hidden sm:block` it replaces only hid it). - */ -function viewTabsFitNow(): boolean { - return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH; -} - -function useViewTabsFit(): boolean { - const [fits, setFits] = useState(viewTabsFitNow); - useEffect(() => { - if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; - const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`); - const onChange = () => setFits(viewTabsFitNow()); - mql.addEventListener('change', onChange); - return () => mql.removeEventListener('change', onChange); - }, []); - return fits; -} - -function keepInlineEditModeForTheSession(): void { - // Deliberately empty: see the docblock. -} - export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }: ConsoleObjectViewProps) { const { objectName } = useParams(); diff --git a/ui/packages/app-shell/src/views/objectViewChrome.tsx b/ui/packages/app-shell/src/views/objectViewChrome.tsx new file mode 100644 index 00000000000..a8ad49dfa90 --- /dev/null +++ b/ui/packages/app-shell/src/views/objectViewChrome.tsx @@ -0,0 +1,100 @@ +/** + * Leaf pieces of the Console `ObjectView` (`./ObjectView.tsx`): the "object + * not found" state, the viewport check that decides whether the saved-view tab + * strip is drawn, and the inline-edit toggle's session-only callback, moved out + * of that file verbatim (objectstack#25992, slice 3). `ObjectView.tsx` + * re-exports every name it exported before. + */ + +import { useState, useEffect } from 'react'; +import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components'; +import { Table as TableIcon } from 'lucide-react'; +import { useObjectTranslation } from '@object-ui/i18n'; + +/** + * The "object not found" state the route draws for an object it has no + * definition of. objectui#13052 — `packageDisabled`: the server answered that + * the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`), + * so the description says that instead. The console route draws it for that + * answer (`AppContent`); this module's `ObjectView` draws it for an absence. + * Not part of the package entry. + */ +export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) { + const { t } = useObjectTranslation(); + return ( +
+ +
+ +
+ {t('console.objectView.objectNotFound')} + + {packageDisabled + ? t('console.objectView.objectPackageDisabled', { objectName }) + : t('console.objectView.objectNotFoundDescription', { objectName })} + {' '} + {t('console.objectView.objectNotFoundHint')} + +
+
+ ); +} + +/** + * The list toolbar's inline-edit toggle, as this page wires it: it writes + * nothing (objectui#5144, triage's ruling E). + * + * The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's + * permission keys, and `normalizeListViewSchema` folds the first into the + * second. `editInline` is on by default since the v18 ruling (objectstack#22605, + * objectui#12086), so a view that declares neither key offers the toggle, and + * `editInline: false` withdraws it. This toggle used to persist a USER's edit + * mode into `inlineEdit` through `persistViewPatch`. After the fold, switching + * it off stored `inlineEdit: false`, which reads as "not offered", so the + * toggle was gone from the next load and nothing in the console could bring it + * back. + * + * The edit mode is now session state. `ListView` keeps it, and seeds it from + * the view's `inlineEdit` on each load. The callback stays wired because + * `ListView` offers the wide toolbar toggle only to a host that wires one. + * + * Recorded costs: the edit mode is not remembered across loads, and an overlay + * that already stores `inlineEdit: false` still reads off. That is existing + * data, and the maintainer's ruling rejects migrating it. Declaring + * `userActions.editInline: true` on such a view brings the toggle back. + */ +/** + * Tailwind's `sm` breakpoint (px): from here up the list page draws its + * saved-view tabs in the list toolbar; below it the app bar's mobile view + * switcher is the page's only view control (objectui#12440). + */ +const VIEW_TABS_MIN_WIDTH = 640; + +/** + * Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia + * idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is + * read synchronously: the tab strip is the first thing on the page's list row, + * and resolving it in an effect would draw the row once without it and then + * jump. objectui#12440 — this decides whether the strip is HANDED to the list + * at all, so below `sm` nothing is passed and nothing is drawn (the CSS + * `hidden sm:block` it replaces only hid it). + */ +function viewTabsFitNow(): boolean { + return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH; +} + +export function useViewTabsFit(): boolean { + const [fits, setFits] = useState(viewTabsFitNow); + useEffect(() => { + if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return; + const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`); + const onChange = () => setFits(viewTabsFitNow()); + mql.addEventListener('change', onChange); + return () => mql.removeEventListener('change', onChange); + }, []); + return fits; +} + +export function keepInlineEditModeForTheSession(): void { + // Deliberately empty: see the docblock. +}