Skip to content

Commit e638193

Browse files
refactor(app-shell): move ObjectView's leaf pieces into objectViewChrome.tsx (slice 3) (#26310)
<!-- 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 3 of the move-only split, under the standing claim and the seat's continuation note; slices 1 and 2 landed as #26152 and #26233, and the card stays open for the component body). Clause-②: no Metadata-protocol: none ## Seam table (measured on `main` 96635f5) `ui/packages/app-shell/src/views/ObjectView.tsx` was 3254 lines. What was still module-level besides imports and the slice 1-2 re-exports: | Piece | Kind | Who reads it | This slice | |---|---|---|---| | `ConsoleObjectViewProps` | exported interface | `ObjectView` / `ObjectViewInner`, `views/index.ts` type re-export, 4 tests by path | stays: it is the component's own props contract | | `ObjectNotFoundState` | exported React component | the `ObjectView` wrapper, `console/AppContent.tsx` (imports it from `views/ObjectView.js`) | moves | | `VIEW_TABS_MIN_WIDTH`, `viewTabsFitNow`, `useViewTabsFit` | file-local constant, function, hook | `ObjectViewInner` (the tab strip decision) | moves | | `keepInlineEditModeForTheSession` (with its docblock) | file-local no-op callback | `ObjectViewInner` (the list's inline-edit toggle) | moves | | `ObjectView`, `ObjectViewInner` | the component | | stays (later slices, by concern) | No test reads any of the moved lines out of `ObjectView.tsx` as source text (checked over every test that reads `ObjectView.tsx`), so this slice needs no test edit at all. ## What moved The contiguous block from the `ObjectNotFoundState` docblock through `keepInlineEditModeForTheSession` moves verbatim, in its original order, into a new sibling `ui/packages/app-shell/src/views/objectViewChrome.tsx`. `ObjectView.tsx` imports `ObjectNotFoundState`, `useViewTabsFit` and `keepInlineEditModeForTheSession` back and re-exports `ObjectNotFoundState`, the one moved name it exported, from the same path. `useViewTabsFit` and `keepInlineEditModeForTheSession` gain the `export` keyword in the new module because the component still calls them; `ObjectView.tsx` does not re-export them, so it publishes no new name. `Table as TableIcon` leaves `ObjectView.tsx`'s lucide import with its only reader. The package entry is untouched. `ObjectView.tsx` goes from 3254 to 3171 lines. Move-only proof: the multiset of non-blank lines of the base `ObjectView.tsx` equals new `ObjectView.tsx` plus `objectViewChrome.tsx`, after normalising the two added `export` keywords. The only differing lines are imports, the re-export and its comment, and the new module's header. ## Test edits None. ## Verification At 668d82f, under the shared verify lock: `pnpm install --frozen-lockfile`; app-shell dependency closure build; `pnpm --filter @object-ui/app-shell type-check` (src and `tsconfig.test.json`); 117 test files (every `views/ObjectView.*` test, every `console/__tests__/AppContent*` test, and every other test that reads `ObjectView.tsx` as source) passed, 1425 tests; Console build (turbo, 71/71 tasks). eslint on both source files: 0 errors. The 30 derived dispatch gates: all exit 0. The rest of the app-shell suite is left to CI `Test UI`: it reaches these names only through the unchanged import path. ## Acceptance notes - `ObjectNotFoundState`'s docblock still says "this module's `ObjectView` draws it for an absence"; the wrapper that draws it is now in the sibling file. Kept verbatim because the slice is move-only. - The inline-edit docblock sits directly above the view-tabs docblock, as it did in `ObjectView.tsx`; the order is unchanged. Changeset: `.changeset/25992-objectview-chrome-split.md` (patch, `@object-ui/app-shell`). --- _Generated by [Claude Code](https://claude.ai/code/session_01QciHBvFJXnhZqXmdqpDtpz)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 14b5597 commit e638193

3 files changed

Lines changed: 118 additions & 89 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 leaf pieces move into a sibling module
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
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.
11+
12+
What to do: nothing.

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

Lines changed: 6 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import {
4949
EmptyDescription,
5050
NavigationOverlay,
5151
} from '@object-ui/components';
52-
import { Plus, Upload, Table as TableIcon, Lock, MoreHorizontal, RotateCcw } from 'lucide-react';
52+
import { Plus, Upload, Lock, MoreHorizontal, RotateCcw } from 'lucide-react';
5353
import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js';
5454
import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js';
5555
import { useTenancyPosture } from '../hooks/useTenancyPosture.js';
@@ -125,6 +125,7 @@ import {
125125
isSameOptionsValue,
126126
dispatchViewPatches,
127127
} from './objectViewOptions.js';
128+
import { ObjectNotFoundState, useViewTabsFit, keepInlineEditModeForTheSession } from './objectViewChrome.js';
128129

129130
// objectstack#25992 slice 1: these module-level helpers moved to the sibling
130131
// `objectView*.ts` modules above. Every name this file exported stays
@@ -155,6 +156,10 @@ export {
155156
dispatchViewPatches,
156157
} from './objectViewOptions.js';
157158

159+
// objectstack#25992 slice 3: `ObjectNotFoundState` moved to `./objectViewChrome.tsx`
160+
// and stays exported from here (`AppContent` imports it by this path).
161+
export { ObjectNotFoundState } from './objectViewChrome.js';
162+
158163
/**
159164
* Props of the Console `ObjectView`, the route-level object surface
160165
* `@object-ui/app-shell` exports (objectui#7483).
@@ -186,94 +191,6 @@ export interface ConsoleObjectViewProps {
186191
externalRefreshKey?: number;
187192
}
188193

189-
/**
190-
* The "object not found" state the route draws for an object it has no
191-
* definition of. objectui#13052 — `packageDisabled`: the server answered that
192-
* the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`),
193-
* so the description says that instead. The console route draws it for that
194-
* answer (`AppContent`); this module's `ObjectView` draws it for an absence.
195-
* Not part of the package entry.
196-
*/
197-
export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) {
198-
const { t } = useObjectTranslation();
199-
return (
200-
<div className="h-full p-4 flex items-center justify-center">
201-
<Empty data-testid={packageDisabled ? 'object-package-disabled' : 'object-not-found'}>
202-
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
203-
<TableIcon className="h-6 w-6 text-muted-foreground" />
204-
</div>
205-
<EmptyTitle>{t('console.objectView.objectNotFound')}</EmptyTitle>
206-
<EmptyDescription>
207-
{packageDisabled
208-
? t('console.objectView.objectPackageDisabled', { objectName })
209-
: t('console.objectView.objectNotFoundDescription', { objectName })}
210-
{' '}
211-
{t('console.objectView.objectNotFoundHint')}
212-
</EmptyDescription>
213-
</Empty>
214-
</div>
215-
);
216-
}
217-
218-
/**
219-
* The list toolbar's inline-edit toggle, as this page wires it: it writes
220-
* nothing (objectui#5144, triage's ruling E).
221-
*
222-
* The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's
223-
* permission keys, and `normalizeListViewSchema` folds the first into the
224-
* second. `editInline` is on by default since the v18 ruling (objectstack#22605,
225-
* objectui#12086), so a view that declares neither key offers the toggle, and
226-
* `editInline: false` withdraws it. This toggle used to persist a USER's edit
227-
* mode into `inlineEdit` through `persistViewPatch`. After the fold, switching
228-
* it off stored `inlineEdit: false`, which reads as "not offered", so the
229-
* toggle was gone from the next load and nothing in the console could bring it
230-
* back.
231-
*
232-
* The edit mode is now session state. `ListView` keeps it, and seeds it from
233-
* the view's `inlineEdit` on each load. The callback stays wired because
234-
* `ListView` offers the wide toolbar toggle only to a host that wires one.
235-
*
236-
* Recorded costs: the edit mode is not remembered across loads, and an overlay
237-
* that already stores `inlineEdit: false` still reads off. That is existing
238-
* data, and the maintainer's ruling rejects migrating it. Declaring
239-
* `userActions.editInline: true` on such a view brings the toggle back.
240-
*/
241-
/**
242-
* Tailwind's `sm` breakpoint (px): from here up the list page draws its
243-
* saved-view tabs in the list toolbar; below it the app bar's mobile view
244-
* switcher is the page's only view control (objectui#12440).
245-
*/
246-
const VIEW_TABS_MIN_WIDTH = 640;
247-
248-
/**
249-
* Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia
250-
* idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is
251-
* read synchronously: the tab strip is the first thing on the page's list row,
252-
* and resolving it in an effect would draw the row once without it and then
253-
* jump. objectui#12440 — this decides whether the strip is HANDED to the list
254-
* at all, so below `sm` nothing is passed and nothing is drawn (the CSS
255-
* `hidden sm:block` it replaces only hid it).
256-
*/
257-
function viewTabsFitNow(): boolean {
258-
return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH;
259-
}
260-
261-
function useViewTabsFit(): boolean {
262-
const [fits, setFits] = useState<boolean>(viewTabsFitNow);
263-
useEffect(() => {
264-
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
265-
const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`);
266-
const onChange = () => setFits(viewTabsFitNow());
267-
mql.addEventListener('change', onChange);
268-
return () => mql.removeEventListener('change', onChange);
269-
}, []);
270-
return fits;
271-
}
272-
273-
function keepInlineEditModeForTheSession(): void {
274-
// Deliberately empty: see the docblock.
275-
}
276-
277194
export function ObjectView({ dataSource, objects, onEdit, externalRefreshKey }: ConsoleObjectViewProps) {
278195
const { objectName } = useParams();
279196

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/**
2+
* Leaf pieces of the Console `ObjectView` (`./ObjectView.tsx`): the "object
3+
* not found" state, the viewport check that decides whether the saved-view tab
4+
* strip is drawn, and the inline-edit toggle's session-only callback, moved out
5+
* of that file verbatim (objectstack#25992, slice 3). `ObjectView.tsx`
6+
* re-exports every name it exported before.
7+
*/
8+
9+
import { useState, useEffect } from 'react';
10+
import { Empty, EmptyTitle, EmptyDescription } from '@object-ui/components';
11+
import { Table as TableIcon } from 'lucide-react';
12+
import { useObjectTranslation } from '@object-ui/i18n';
13+
14+
/**
15+
* The "object not found" state the route draws for an object it has no
16+
* definition of. objectui#13052 — `packageDisabled`: the server answered that
17+
* the object exists and its package is disabled (`OBJECT_PACKAGE_DISABLED`),
18+
* so the description says that instead. The console route draws it for that
19+
* answer (`AppContent`); this module's `ObjectView` draws it for an absence.
20+
* Not part of the package entry.
21+
*/
22+
export function ObjectNotFoundState({ objectName, packageDisabled = false }: { objectName: string | undefined; packageDisabled?: boolean }) {
23+
const { t } = useObjectTranslation();
24+
return (
25+
<div className="h-full p-4 flex items-center justify-center">
26+
<Empty data-testid={packageDisabled ? 'object-package-disabled' : 'object-not-found'}>
27+
<div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-muted">
28+
<TableIcon className="h-6 w-6 text-muted-foreground" />
29+
</div>
30+
<EmptyTitle>{t('console.objectView.objectNotFound')}</EmptyTitle>
31+
<EmptyDescription>
32+
{packageDisabled
33+
? t('console.objectView.objectPackageDisabled', { objectName })
34+
: t('console.objectView.objectNotFoundDescription', { objectName })}
35+
{' '}
36+
{t('console.objectView.objectNotFoundHint')}
37+
</EmptyDescription>
38+
</Empty>
39+
</div>
40+
);
41+
}
42+
43+
/**
44+
* The list toolbar's inline-edit toggle, as this page wires it: it writes
45+
* nothing (objectui#5144, triage's ruling E).
46+
*
47+
* The view's `inlineEdit` and `userActions.editInline` are the AUTHOR's
48+
* permission keys, and `normalizeListViewSchema` folds the first into the
49+
* second. `editInline` is on by default since the v18 ruling (objectstack#22605,
50+
* objectui#12086), so a view that declares neither key offers the toggle, and
51+
* `editInline: false` withdraws it. This toggle used to persist a USER's edit
52+
* mode into `inlineEdit` through `persistViewPatch`. After the fold, switching
53+
* it off stored `inlineEdit: false`, which reads as "not offered", so the
54+
* toggle was gone from the next load and nothing in the console could bring it
55+
* back.
56+
*
57+
* The edit mode is now session state. `ListView` keeps it, and seeds it from
58+
* the view's `inlineEdit` on each load. The callback stays wired because
59+
* `ListView` offers the wide toolbar toggle only to a host that wires one.
60+
*
61+
* Recorded costs: the edit mode is not remembered across loads, and an overlay
62+
* that already stores `inlineEdit: false` still reads off. That is existing
63+
* data, and the maintainer's ruling rejects migrating it. Declaring
64+
* `userActions.editInline: true` on such a view brings the toggle back.
65+
*/
66+
/**
67+
* Tailwind's `sm` breakpoint (px): from here up the list page draws its
68+
* saved-view tabs in the list toolbar; below it the app bar's mobile view
69+
* switcher is the page's only view control (objectui#12440).
70+
*/
71+
const VIEW_TABS_MIN_WIDTH = 640;
72+
73+
/**
74+
* Is the viewport at least {@link VIEW_TABS_MIN_WIDTH} wide? The matchMedia
75+
* idiom of `useIsMobile` / `useIsWideViewport`, except the first answer is
76+
* read synchronously: the tab strip is the first thing on the page's list row,
77+
* and resolving it in an effect would draw the row once without it and then
78+
* jump. objectui#12440 — this decides whether the strip is HANDED to the list
79+
* at all, so below `sm` nothing is passed and nothing is drawn (the CSS
80+
* `hidden sm:block` it replaces only hid it).
81+
*/
82+
function viewTabsFitNow(): boolean {
83+
return typeof window !== 'undefined' && window.innerWidth >= VIEW_TABS_MIN_WIDTH;
84+
}
85+
86+
export function useViewTabsFit(): boolean {
87+
const [fits, setFits] = useState<boolean>(viewTabsFitNow);
88+
useEffect(() => {
89+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return;
90+
const mql = window.matchMedia(`(min-width: ${VIEW_TABS_MIN_WIDTH}px)`);
91+
const onChange = () => setFits(viewTabsFitNow());
92+
mql.addEventListener('change', onChange);
93+
return () => mql.removeEventListener('change', onChange);
94+
}, []);
95+
return fits;
96+
}
97+
98+
export function keepInlineEditModeForTheSession(): void {
99+
// Deliberately empty: see the docblock.
100+
}

0 commit comments

Comments
 (0)