Skip to content

Commit c064d3a

Browse files
perf(console): write app navigation at page and dashboard create and delete sites (#28783)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbNiiAtyW6uY3eTDyWYkBLff)_ Fixes #28680 Fixes #28645 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:页面或仪表盘在控制台被创建(首次发布)或删除时,由该处直接写入应用导航;`NavigationSyncEffect` 不再在每个应用页读取整张 `/meta/page` 和 `/meta/dashboard` 列表做差异比较。 - 为什么改:这是 #28645 的第二步,按维护者裁定 A(#28680)。去掉这两次列表读取后,页面路由 26 → 24 次请求,对象列表路由 33 → 31。 - 风险与回滚:在本标签页之外创建或删除的无包页面/仪表盘(服务端智能体、另一个标签页、直接调用 REST)不再自动进入导航,只能由其写入方处理;包安装和 CLI 部署的条目带 `_packageId`,原本就不经过差异比较。回滚即还原本 PR,无数据或协议变更。 - 先看:`ui/packages/app-shell/src/hooks/useNavigationSync.ts`(`useNavigationWrites`)、`ui/packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx`(`doPublish`、`runReset`)、`ui/packages/app-shell/src/preview/usePublishAllDrafts.ts`。 - 两分钟验证:`cd ui && pnpm exec vitest run packages/app-shell/src/views/metadata-admin/ResourceEditPage.navigationWrites-28680.test.tsx packages/app-shell/src/hooks/__tests__/useNavigationSync.test.tsx`,预期 22 个用例全绿。 - 需要你做的:无,席位落地。仪表盘路由仍读 `/meta/dashboard`(见 Deviations),可另开卡处理。 ## What changes `NavigationSyncEffect` read the whole `/meta/page` and `/meta/dashboard` lists on every app page, after the first paint, only to diff their name sets and add or remove navigation entries. Maintainer ruling A on #28680 retires it. The console's sites now write navigation themselves, through a new door in `useNavigationSync.ts`: - **`useNavigationWrites()`** returns `created(type, name, item?)` and `deleted(type, name, item?)`. They call the existing `sync…CreatedAllApps` / `sync…DeletedAllApps` writers and keep the diff's rules: only a `page` or a `dashboard`; never a platform artifact (`isPlatformArtifact`: a `sys_` name, or `_packageId` / `_provenance: 'package'`); nothing in draft preview (ADR-0037, where `apps` may be draft bodies); only into apps that may be written (`isNavigationSyncableApp`). Entries are written label-less, as before (objectui#9868). - **The metadata editor's Publish** (`ResourceEditPage` `doPublish`): when the item held no layer before the publish (its draft was its only layer, `/layers` 404) and the editor is not bound to a package, the page or dashboard is created and joins navigation. A later publish of the same item is an update and writes nothing. - **The metadata editor's Delete** (`runReset` with no baseline): an unbound real delete removes the item from navigation. A reset to a packaged baseline writes nothing. - **Publish all** (`usePublishAllDrafts`): before publishing each package-less page or dashboard draft, it asks the item's layers. The ones that had none are created and join navigation after the batch. A failed layers read is not taken for a create. - **A create no longer duplicates an entry.** `syncPageCreated` / `syncDashboardCreated` skip an app whose navigation already references the target at any depth (`navigationReferences`). `addNavigationItem` itself still appends. - **`NavigationSyncEffect`** stays exported, `@deprecated`, and renders nothing. `AppContent` no longer mounts it. No rename site exists in the console today, so the `sync…RenamedAllApps` writers keep no caller. ### Who relied on the diff (the claim's pre-code check) Nothing created outside the console. The diff dropped every platform artifact, and package installs and CLI deploys carry `_packageId` (checked on the running showcase: `showcase_start_here`'s layers answer `_packageId: 'com.example.showcase'`, `_provenance: 'package'`). Studio always saves with a package. The diff acted only on package-less pages or dashboards that appeared or vanished in this tab's lists mid-session, and its baseline re-seeded on every load. **What no longer reaches navigation:** a package-less page or dashboard created or deleted outside this tab (a server-side agent, another tab, a raw REST call). It now reaches navigation only through its own writer. The changeset says the same. ## Tests - New `ResourceEditPage.navigationWrites-28680.test.tsx` (5 cases, the real page): the first publish adds the page to the app once and a **second publish adds no second entry**; publishing an item that already had a layer writes nothing; a package-bound publish writes nothing; a real delete removes the entry; a reset leaves navigation alone. - New `usePublishAllDrafts.navigationWrites-28680.test.tsx` (2 cases, real `MetadataClient`): new package-less page and dashboard join; an updated page, a packaged page and a flow do not; layers are asked before each publish; a failed layers read writes nothing. - `useNavigationSync.test.tsx`: the diff's cases are replaced by `useNavigationWrites` cases (platform artifacts in both directions, other types, draft preview, locked apps, a second create of a referenced page or dashboard), `navigationReferences`, and a pin that `NavigationSyncEffect` reads neither list and writes nothing. - `labelInherits-9868`, `standardEntryLocalizes-11201` and `chromeReads.afterFirstPaint-12980` now drive the new door, or pin that the retired effect reads nothing. - Ablations, each one red: always-first-publish, publish ignoring the package binding, no create write, no delete write, delete on reset too, publish-all without the layers check, publish-all without the write, writer without dedupe, no preview skip, no platform skip, no type filter (11 of 11). ## Acceptance notes Measured on a production console build (`pnpm build`, `pnpm console:build`, `pnpm dev -- --fresh`), `showcase_app`, admin, API calls of a warm reload, two runs each with the same count: | Route | Base `2678600d` | PR 1 (#28754) | This PR | Difference from PR 1 | |---|---|---|---|---| | `page/showcase_start_here` | 26 | 26 | **24** | `/meta/page` −1, `/meta/dashboard` −1 | | `dashboard/showcase_ops_dashboard` | 35 | 35 | **34** | `/meta/page` −1 | | `showcase_task` (object list) | 33 | 33 | **31** | `/meta/page` −1, `/meta/dashboard` −1 | On the running server, a page saved as a draft answers `/layers` 404 and its first publish turns it into a layer with `resettable: false`, which is the editor's create and delete verdict this PR reads. The probe page was deleted afterwards. Deviations: - **The dashboard route is 34, not the expected 33.** Its own route gate in `AppContent` waits for the `dashboard` list (`routeReads`), and the dashboard view reads its definition from it. The effect shared that one request, so removing the effect saves only `/meta/page` there. Reading the dashboard route bundle-first, as PR 1 did for the page route, would remove it. That is `AppContent`'s route gate and the dashboard view, outside this claim's surface, so it is left for a follow-up card. - **Publish all from the draft-preview bar writes no navigation.** The door writes nothing in preview, where `apps` may be draft bodies. The diff wrote nothing there either. - `AppContent.tsx` changes only to drop the mount. `useNavTargetLabel.ts` keeps a comment that says the console's `NavigationSyncEffect` loads the dashboard list; it is outside the surface and left as it is. ## Verification <details><summary>Derived gates (34)</summary> Run at `f31794f6` (this PR merged with `origin/main` at `cd0ec76b`). `dispatch-gates --ran`: 34 derived, 34 run, 0 NOT-MEASURED. ``` node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-sdui-manifest.mjs :: exit 0 node scripts/check-sdui-manifest.mjs --self-test :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --dir ui exec vitest run scripts/ :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:engine-double-contract :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:published-files :: exit 0 pnpm check:query-options-erasure :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:where-matcher :: exit 0 ``` </details> <details><summary>Tests and type-check</summary> - 295 test files naming the touched modules (`useNavigationSync`, `ResourceEditPage`, `usePublishAllDrafts`, `AppContent`, `DraftPreviewBar`, `HomePage`), 5 locked batches at `--maxWorkers=3`: 295 passed, 2493 tests. The full app-shell suite is left to CI's Test UI shards. - `pnpm turbo run type-check --filter=@object-ui/app-shell`: 65/65 tasks, exit 0. - `eslint --quiet` over the changed files: exit 0. - Merged `origin/main` at `cd0ec76b` with no overlap on touched files and no parallel fix found. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (run as `eslint --quiet` over the changed ui files: 0 errors; the whole-workspace lint is left to CI) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. (except the 维护者速读 block the dispatch rules require) - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jUjXYpqdaAkEWBHbqG --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 899782b commit c064d3a

20 files changed

Lines changed: 798 additions & 450 deletions
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
perf(console): write app navigation where a page or dashboard is created or deleted, and stop reading the whole `/meta/page` and `/meta/dashboard` lists on every app page
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`NavigationSyncEffect` read both lists on every app page after the first paint, only to diff their name sets and add or remove navigation entries. The console's sites now write navigation themselves:
11+
12+
- **The metadata editor's Publish** adds a package-less page or dashboard to every writable app the first time it is published (the draft was its only layer). A later publish of the same item is an update and writes nothing.
13+
- **The metadata editor's Delete** removes a package-less page or dashboard from every writable app when the delete leaves no baseline. A reset to a packaged baseline writes nothing.
14+
- **Publish all** asks each package-less page or dashboard draft's layers before publishing it, and adds the ones it creates to navigation.
15+
16+
The diff's rules are kept: platform artifacts (a `sys_` name, or `_packageId` / `_provenance: 'package'`) never write navigation, write-protected apps are skipped, and nothing is written in draft preview. A create no longer adds a second entry to an app that already references the page or dashboard.
17+
18+
What no longer reaches navigation: a package-less page or dashboard created or deleted outside this tab (a server-side agent, another tab, a raw REST call). The diff noticed one only when it appeared in this tab's list mid-session; it now reaches navigation only through its own writer. Items from a package install or a CLI deploy carry `_packageId` and never reached navigation through the diff.
19+
20+
`NavigationSyncEffect` is removed. `@object-ui/app-shell` is private and nothing outside it imported the effect.

‎ui/packages/app-shell/src/chrome/CommandPalette.navLabelInherits-9868.test.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
* The ⌘K palette names an app navigation entry the way the sidebar does,
1111
* including an entry with NO `label` (objectui#9868, contract review finding 1).
1212
*
13-
* Since objectui#9868 `NavigationSyncEffect` writes every page and dashboard
14-
* created in the console WITHOUT a `label` — absent means "inherit the target's
13+
* Since objectui#9868 the console writes every page and dashboard created in
14+
* it into navigation WITHOUT a `label` (`useNavigationWrites`, objectstack#28680) — absent means "inherit the target's
1515
* current label at render time". The palette read `resolveKeyedI18nLabel(
1616
* item.label, t)`, which answers `undefined` for an absent label, so such an
1717
* entry drew a BLANK row whose cmdk filter value read `page undefined my_page`.
@@ -53,7 +53,7 @@ const APP = {
5353
name: 'crm',
5454
label: 'CRM',
5555
navigation: [
56-
// What NavigationSyncEffect writes for a page / dashboard since objectui#9868.
56+
// What the console's create sites write for a page / dashboard since objectui#9868.
5757
{ id: 'nav_page', type: 'page', pageName: 'my_page' },
5858
{ id: 'nav_dash', type: 'dashboard', dashboardName: 'sales_overview' },
5959
{ id: 'nav_board', type: 'object', objectName: 'customer', viewName: 'board' },

‎ui/packages/app-shell/src/chrome/CommandPalette.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,8 @@ function CommandPaletteBody(props: CommandPaletteProps & { gates: NavMemberGates
178178
const { t, language } = useObjectTranslation();
179179
// A nav entry's text, the way the sidebar and `nav:menu` name it: an entry
180180
// with NO `label` shows its target's current label, else its target's machine
181-
// name (objectui#9868 — `NavigationSyncEffect` writes such entries). A present
181+
// name (objectui#9868 — the console's page and dashboard create sites write
182+
// such entries through `useNavigationWrites`). A present
182183
// label renders as authored — an inline locale map in the viewer's `language`,
183184
// the locale the sidebar passes (objectui#11299).
184185
const targetLabel = useNavTargetLabel();

‎ui/packages/app-shell/src/console/AppContent.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import { ObjectNotFoundState } from '../views/ObjectView.js';
5757
import { KeyboardShortcutsDialog } from '../chrome/KeyboardShortcutsDialog.js';
5858
import { OnboardingWalkthrough } from '../chrome/OnboardingWalkthrough.js';
5959
import { RouteFader } from '../chrome/RouteFader.js';
60-
import { NavigationSyncEffect } from '../hooks/useNavigationSync.js';
6160
import { AppNoAccessEmptyState } from './AppNoAccessEmptyState.js';
6261
import { appNamesOnlyUnreadableObjects, appServesNoNavigation } from './appServesNoNavigation.js';
6362
import { resolveDeclaredLandingRoute, type LandingVisibilityEvaluator } from './appDeclaredLanding.js';
@@ -1452,7 +1451,6 @@ export function AppContent({ extraRoutes, extraRoutesNoApp }: AppContentProps =
14521451
currentUserId={user?.id ?? null}
14531452
currentOrgId={activeOrganization?.id ?? null}
14541453
>
1455-
<NavigationSyncEffect />
14561454
<ConsoleLayout
14571455
activeAppName={activeApp.name}
14581456
activeApp={activeApp}

‎ui/packages/app-shell/src/console/__tests__/AppContent.routeChunkPreload-12980.test.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ vi.mock('../../layout/ConsoleLayout', () => ({
7070
vi.mock('../../chrome/CommandPalette', () => ({ CommandPalette: () => null }));
7171
vi.mock('../../chrome/KeyboardShortcutsDialog', () => ({ KeyboardShortcutsDialog: () => null }));
7272
vi.mock('../../chrome/OnboardingWalkthrough', () => ({ OnboardingWalkthrough: () => null }));
73-
vi.mock('../../hooks/useNavigationSync', () => ({ NavigationSyncEffect: () => null }));
7473
vi.mock('@object-ui/i18n', async (importOriginal) => ({
7574
...(await importOriginal<Record<string, unknown>>()),
7675
useObjectTranslation: () => ({

‎ui/packages/app-shell/src/hooks/__tests__/chromeReads.afterFirstPaint-12980.test.tsx‎

Lines changed: 3 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22
* ObjectUI
33
* Copyright (c) 2024-present ObjectStack Inc.
44
*
5-
* objectui#12980, part 3 — two shell readers that mount UNDER the boot splash
6-
* wait for the page's first paint, not the splash's:
5+
* objectui#12980, part 3 — shell readers that mount UNDER the boot splash
6+
* wait for the page's first paint, not the splash's (objectstack#28680 retired a
7+
* third, `NavigationSyncEffect`):
78
*
8-
* - `NavigationSyncEffect` reads the `pages` / `dashboards` lists (lazy
9-
* getters: reading one starts its `/meta/:type` read) only once the splash
10-
* is gone and a frame has painted;
119
* - the bell's Activity feed, until the bell asks for it, does not even ask
1210
* whether `sys_activity` exists: inside an app that answer is its own
1311
* by-name read through the app's object scope, which used to go out in
@@ -39,7 +37,6 @@ vi.mock('@object-ui/auth', async (importOriginal) => ({
3937
}));
4038
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
4139

42-
import { NavigationSyncEffect } from '../useNavigationSync';
4340
import { useSignedInUserLocale } from '../useUserLocale';
4441
import { __resetFirstPaint, useSharedActivityFeed } from '../sharedUserFeeds';
4542
import { ExpressionProvider } from '../../providers/ExpressionProvider';
@@ -73,51 +70,6 @@ afterEach(() => {
7370
vi.restoreAllMocks();
7471
});
7572

76-
describe('NavigationSyncEffect reads the page and dashboard lists after the first paint (objectui#12980)', () => {
77-
function metadataRecording(read: Set<string>): MetadataContextValue {
78-
return {
79-
apps: [{ name: 'crm', label: 'CRM', navigation: [] }],
80-
get pages() {
81-
read.add('page');
82-
return [];
83-
},
84-
get dashboards() {
85-
read.add('dashboard');
86-
return [];
87-
},
88-
objects: [],
89-
reports: [],
90-
loading: false,
91-
error: null,
92-
refresh: async () => {},
93-
invalidate: () => {},
94-
ensureType: async () => [],
95-
getItem: async () => null,
96-
getItemsByType: () => [],
97-
getTypeStatus: () => 'ready',
98-
} as unknown as MetadataContextValue;
99-
}
100-
101-
it('reads neither list while the boot splash is up, and both once it is gone', async () => {
102-
const read = new Set<string>();
103-
const adapter = { getClient: () => ({ meta: { saveItem: vi.fn() } }) } as unknown as React.ComponentProps<
104-
typeof AdapterCtx.Provider
105-
>['value'];
106-
render(
107-
<AdapterCtx.Provider value={adapter}>
108-
<MetadataCtx.Provider value={metadataRecording(read)}>
109-
<NavigationSyncEffect />
110-
</MetadataCtx.Provider>
111-
</AdapterCtx.Provider>,
112-
);
113-
await someFrames();
114-
expect([...read]).toEqual([]);
115-
116-
dropSplash();
117-
await waitFor(() => expect([...read].sort()).toEqual(['dashboard', 'page']));
118-
});
119-
});
120-
12173
describe('useSignedInUserLocale reads the user row after the first paint (objectui#12980)', () => {
12274
function Probe() {
12375
useSignedInUserLocale();

‎ui/packages/app-shell/src/hooks/__tests__/useNavigationSync.labelInherits-9868.test.tsx‎

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,18 @@
1616
* means "resolve at render time", so an unnamed entry is written with NO
1717
* `label` key; an author label is still written, verbatim.
1818
*
19-
* Asserted on the real writer — `NavigationSyncEffect`, the one caller in the
20-
* console, which never passes a label — and, for the author-label control, on
21-
* `useNavigationSync().syncPageCreated` / `syncDashboardCreated` with one.
19+
* Asserted on the real writer — `useNavigationWrites().created`, the door the
20+
* console's create sites call (objectstack#28680; it replaced
21+
* `NavigationSyncEffect`), which never passes a label — and, for the
22+
* author-label control, on `useNavigationSync().syncPageCreated` /
23+
* `syncDashboardCreated` with one.
2224
*/
2325

2426
import { describe, it, expect, vi, beforeEach } from 'vitest';
2527
import React, { useEffect } from 'react';
2628
import { render, waitFor } from '@testing-library/react';
2729
import { MetadataCtx, AdapterCtx, type MetadataContextValue } from '@object-ui/react';
28-
import { NavigationSyncEffect, useNavigationSync } from '../useNavigationSync';
29-
import { __markFirstPainted } from '../sharedUserFeeds';
30+
import { useNavigationSync, useNavigationWrites } from '../useNavigationSync';
3031

3132
vi.mock('sonner', () => ({
3233
toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() },
@@ -64,29 +65,32 @@ const adapterFor = (saveItem: ReturnType<typeof vi.fn>) =>
6465
const writtenEntries = (saveItem: ReturnType<typeof vi.fn>) =>
6566
saveItem.mock.calls.flatMap(([, , schema]) => schema.navigation as Record<string, unknown>[]);
6667

67-
const flush = () => new Promise((r) => setTimeout(r, 0));
68-
6968
beforeEach(() => {
7069
vi.clearAllMocks();
71-
// objectui#12980 — the effect reads nothing before the console's first
72-
// paint; these cases are about the diff, so they start past it.
73-
__markFirstPainted();
7470
});
7571

76-
describe('objectui#9868 — NavigationSyncEffect writes unnamed entries with NO label', () => {
72+
describe('objectui#9868 — the create sites write unnamed entries with NO label', () => {
7773
it('a new page and a new dashboard land in navigation without a `label` key', async () => {
7874
const saveItem = vi.fn().mockResolvedValue({});
79-
const tree = (world: World) => (
75+
function Site() {
76+
const { created } = useNavigationWrites();
77+
useEffect(() => {
78+
void (async () => {
79+
await created('page', 'my_page', { name: 'my_page' });
80+
await created('dashboard', 'my_dash', { name: 'my_dash', label: 'My Dashboard' });
81+
})();
82+
// Once, on mount: the two creates are the case.
83+
// eslint-disable-next-line react-hooks/exhaustive-deps
84+
}, []);
85+
return null;
86+
}
87+
render(
8088
<AdapterCtx.Provider value={adapterFor(saveItem)}>
81-
<MetadataCtx.Provider value={metaValue(world)}>
82-
<NavigationSyncEffect />
89+
<MetadataCtx.Provider value={metaValue({ pages: [], dashboards: [] })}>
90+
<Site />
8391
</MetadataCtx.Provider>
84-
</AdapterCtx.Provider>
92+
</AdapterCtx.Provider>,
8593
);
86-
const { rerender } = render(tree({ pages: [], dashboards: [] }));
87-
await flush();
88-
89-
rerender(tree({ pages: [{ name: 'my_page' }], dashboards: [{ name: 'my_dash', label: 'My Dashboard' }] }));
9094
await waitFor(() => expect(saveItem).toHaveBeenCalledTimes(2));
9195

9296
const [page, dash] = [

‎ui/packages/app-shell/src/hooks/__tests__/useNavigationSync.standardEntryLocalizes-11201.test.tsx‎

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
* would be relying on the renderer's translate-if-equal-to-name convention,
1818
* which B retires. This pins the other half, end to end on the real code:
1919
*
20-
* 1. the writer — `NavigationSyncEffect`, the console's one caller of the
21-
* sync — stores the new dashboard's entry with NO `label` key;
20+
* 1. the writer — `useNavigationWrites().created`, the door the console's
21+
* create sites call (objectstack#28680) — stores the new dashboard's entry
22+
* with NO `label` key;
2223
* 2. that STORED entry, handed to the sidebar's `NavigationRenderer` wired as
2324
* `UnifiedSidebar` wires it (`useNavTargetLabel` plus the active
2425
* `language` as `locale`, objectui#11299), under a real `I18nProvider`
@@ -31,7 +32,7 @@
3132

3233
import '@testing-library/jest-dom/vitest';
3334
import { describe, it, expect, vi, beforeAll } from 'vitest';
34-
import React from 'react';
35+
import React, { useEffect } from 'react';
3536
import { render, screen, waitFor, within, cleanup } from '@testing-library/react';
3637
import { MemoryRouter } from 'react-router-dom';
3738
import type { TranslationData } from '@objectstack/spec/system';
@@ -49,8 +50,7 @@ import {
4950
transformSpecTranslations,
5051
useObjectTranslation,
5152
} from '@object-ui/i18n';
52-
import { NavigationSyncEffect } from '../useNavigationSync';
53-
import { __markFirstPainted } from '../sharedUserFeeds';
53+
import { useNavigationWrites } from '../useNavigationSync';
5454
import { useNavTargetLabel } from '../useNavTargetLabel';
5555

5656
vi.mock('sonner', () => ({
@@ -89,27 +89,28 @@ function metaValue(dashboards: unknown[]): MetadataContextValue {
8989
};
9090
}
9191

92-
const flush = () => new Promise((r) => setTimeout(r, 0));
93-
9492
/** Drive the real writer through a dashboard creation; return the entry it stored. */
9593
async function syncedDashboardEntry(): Promise<Record<string, unknown>> {
9694
const saveItem = vi.fn().mockResolvedValue({});
9795
const adapter = { getClient: () => ({ meta: { saveItem } }) } as unknown as React.ComponentProps<
9896
typeof AdapterCtx.Provider
9997
>['value'];
100-
const tree = (dashboards: unknown[]) => (
98+
function Site() {
99+
const { created } = useNavigationWrites();
100+
useEffect(() => {
101+
void created('dashboard', DASHBOARD.name, DASHBOARD);
102+
// Once, on mount: the create is the case.
103+
// eslint-disable-next-line react-hooks/exhaustive-deps
104+
}, []);
105+
return null;
106+
}
107+
const { unmount } = render(
101108
<AdapterCtx.Provider value={adapter}>
102-
<MetadataCtx.Provider value={metaValue(dashboards)}>
103-
<NavigationSyncEffect />
109+
<MetadataCtx.Provider value={metaValue([DASHBOARD])}>
110+
<Site />
104111
</MetadataCtx.Provider>
105-
</AdapterCtx.Provider>
112+
</AdapterCtx.Provider>,
106113
);
107-
// objectui#12980 — the effect reads nothing before the console's first
108-
// paint; this case is about the stored entry, so it starts past it.
109-
__markFirstPainted();
110-
const { rerender, unmount } = render(tree([]));
111-
await flush();
112-
rerender(tree([DASHBOARD]));
113114
await waitFor(() => expect(saveItem).toHaveBeenCalledTimes(1));
114115
unmount();
115116
const [type, name, schema] = saveItem.mock.calls[0];

0 commit comments

Comments
 (0)