Skip to content

Commit fbc1656

Browse files
fix(app-shell): show Studio nav entries in the menu's order, and let a canvas drag write order (#27039)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ882oEE1FPo1aN5yzd2Gp32G)_ Fixes #26979 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:Studio「界面」左栏导航树与导航编辑画布,改为按 `order` 排列,与运行时菜单一致;画布拖拽排序时必要时写入被拖条目的 `order`。 - 为什么改:运行时菜单按 `order` 排序,Studio 却按数组顺序显示,设置了 `order` 的条目在设计器和发布后的菜单里位置不同,拖拽也可能看不出效果。 - 风险与回滚:只动 app-shell(私有包)三处源码加测试;没有设置 `order` 的应用显示与拖拽结果不变;回滚即还原本 PR。 - 先看:`AppNavCanvas.tsx` 的 `moveItem` 与 `inMenuOrder`;`StudioDesignSurface.tsx` 的 `NavTree`;新测试块 `railNavOrder-26979`。 - 两分钟验证:`pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/StudioDesignSurface.railLabels.test.tsx -t railNavOrder`,预期 4 个测试全部通过。 - 需要你做的:无,席位落地。 ## What changes - The Studio Interfaces rail (`NavTree`) and the nav canvas (`AppNavCanvas`) list every level through one new helper, `inMenuOrder` (exported from `AppNavCanvas.tsx`): `order` ascending, an absent `order` as `0`, ties in authored order. That is the running menu's rule, `byNavOrder` in `@object-ui/layout`, which the sidebar applies at the top level and inside each group. Selection ids, renames and removals still address the authored array index. - A canvas drop rewrites the root array in the new shown order and writes the moved entry's `order` only when its current weight no longer falls between its new neighbours'; it then takes its neighbour's weight and the stable sort keeps it where it was dropped. At most one `order` write per drop, and none for an app whose entries carry no `order`. - `NavNode` (`navSurface.ts`) and the canvas's `RawNav` declare the spec's `order?: number`, type only. ### Why this shape (the card's two options, measured) - The menu sorts by `order` at every level, stably. A drag that only rewrote `order` (shape b) cannot fix entries whose `order` was written by the API or the inspector before any drag: the rail and canvas would still list array order. So both lists sort (shape a), and then a drag has to write `order` or it does nothing visible. Normalising the array to the shown order lets one neighbour-weight write place the entry, which is the fewest `order` writes that keeps the builder equal to the menu. - `byNavOrder` is not imported: `navOrder.ts` is internal to `@object-ui/layout` and publishing it would widen that package's entry point. Per the seat's claim amendment on the card, Studio states the same rule locally and a test renders `NavigationRenderer` beside the rail and the canvas over the same entries, so the two rules cannot drift without a red test. - `AppNavCanvas` is shared with Setup's app editor (`AppPreview.tsx`), which mounts the same component: the sorted listing and the drop behaviour are the same there. ## Tests `StudioDesignSurface.railNavOrder-26979` in `StudioDesignSurface.railLabels.test.tsx` renders the real `InterfacesPillar` rail, `AppNavCanvas` and `NavigationRenderer`: - entries with `order` [3, 1, 2] plus a group whose children carry [2, 1] read in one order on all three; - a drag in the canvas leaves all three agreeing, and only the moved entry changes (it gains `order`; every other entry is the same object); - control: entries without `order` keep array order, and a drag among them writes no `order`. Ablations (each self-restoring through `scripts/ablation-replace.mjs`, blob equal to HEAD after restore): | mutation | result | |---|---| | `inMenuOrder` stops sorting | 2 of 4 red (agreement, drag) | | the drop's `order` write removed | 1 of 4 red (drag) | | `NavTree` lists array order | 2 of 4 red (agreement, drag) | The first attempt at the sort ablation was refused by the tool (its replacement text already occurred in the file), so it did not run; it was rerun with a distinct replacement. ## Acceptance notes - Not changed, outside the file surface: on load the Interfaces pillar opens the first resolvable leaf of the authored array (`firstLeaf` in the load effect), which can now differ from the rail's first row when `order` reorders the top. Observation only. - No changeset: the diff touches only `@object-ui/app-shell`, which is `private: true`. ## Verification All local readings on head `42f2d1b4` unless noted. Exit codes captured before any pipe. <details><summary>Derived gate families (24): all exit 0, reconciled with dispatch-gates --ran</summary> `node scripts/pm/dispatch-gates.mjs --commands` derived 24 families for this diff; each ran on `42f2d1b4` and exited 0. `--ran` answers: "24 derived famil(ies) accounted for — 24 run, 0 NOT-MEASURED". The set includes `node scripts/check-sdui-manifest.mjs` (and its `--self-test`), `node scripts/check-comment-mask-corpus.mjs`, `pnpm --dir ui exec vitest run scripts/`, `pnpm check:cross-package-test-inputs`, `pnpm check:test-source-alias`, `pnpm check:type-check-coverage`, `pnpm check:type-check-debt` and `pnpm check:nul-bytes`. </details> <details><summary>Package tests and extra families (on 54fb22e; the later commit adds one comment and one lint directive)</summary> - `pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/`: Test Files 243 passed (243), Tests 1761 passed (1761). - `pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/previews/ packages/app-shell/src/views/metadata-admin/studio-locale.i18n.test.tsx`: Test Files 148 passed (148), Tests 1678 passed (1678). Covers Setup's `AppPreview` mount of the canvas. - `pnpm --filter @object-ui/app-shell type-check`: exit 0. - `pnpm --dir ui run check:spec-symbols`: exit 0. `pnpm --dir ui run check:new-line-citations`: 0 new citations, exit 0. - eslint on the four changed files: 0 errors; on `42f2d1b4` the new export adds no warning (it carries the same directive as `navPayloadOf`). - On `42f2d1b4`: `StudioDesignSurface.railLabels.test.tsx` whole file, Tests 60 passed (60). </details> Not run locally, CI-owned: the repo-wide `pnpm lint`, `test:repo`, and the full UI suite. ## 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. - [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. - [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. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbWATiRKGB7bP2VvNAG --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbWATiRKGB7bP2VvNAG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 693a729 commit fbc1656

4 files changed

Lines changed: 276 additions & 12 deletions

File tree

‎ui/packages/app-shell/src/views/metadata-admin/previews/AppNavCanvas.tsx‎

Lines changed: 68 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
* fits the Studio's 240 px rail (objectui#12519). Drag-drop reorders within
99
* the root list.
1010
*
11+
* Each level is listed in the order the running menu draws it (objectstack#26979):
12+
* by `order`, ties in authored order (`inMenuOrder`). A drop therefore has to
13+
* move the entry in that order, which `order` can override: see `moveItem`.
14+
*
1115
* Kind and target are read from the spec's discriminated union — `type`
1216
* plus that branch's own target key — never inferred from off-spec keys
1317
* (objectui#3275).
@@ -41,7 +45,7 @@ import type { I18nLabel } from '@objectstack/spec/ui';
4145
import { Badge, cn } from '@object-ui/components';
4246
import type { NavTargetLabelResolver } from '@object-ui/layout';
4347
import { useNavTargetLabel } from '../../../hooks/useNavTargetLabel.js';
44-
import { appendArray, moveArray, spliceArray } from '../inspectors/_shared.js';
48+
import { appendArray, spliceArray } from '../inspectors/_shared.js';
4549
import { t, tFormat, useMetadataLocale } from '../i18n.js';
4650
import { navEntryLabelText, navItemLabelText, renamedLabel } from './navItemLabel.js';
4751

@@ -60,6 +64,8 @@ interface RawNav {
6064
book?: string;
6165
doc?: string;
6266
actionDef?: { actionName?: string };
67+
/** The spec's sort weight within a level (lower = first); read through `inMenuOrder` only. */
68+
order?: number;
6369
children?: RawNav[];
6470
[k: string]: unknown;
6571
}
@@ -254,6 +260,31 @@ export function navPayloadOf(entries: readonly unknown[]): Array<Record<string,
254260
return sent;
255261
}
256262

263+
/**
264+
* A level's entries in the order the running menu draws them, each with its
265+
* index in the authored array (objectstack#26979). The Studio's one reader of
266+
* that order: this canvas and the Interfaces rail (`NavTree`) both list a
267+
* level through it.
268+
*
269+
* The rule it states is the menu's, `byNavOrder` in `@object-ui/layout`
270+
* (`navOrder.ts`), which every sidebar level and the mobile tab bar sort by:
271+
* `order` ascending, an absent `order` as `0`, ties in authored order
272+
* (`Array.prototype.sort` is stable). Stated here, not imported: that module
273+
* is internal to its package, and publishing it would widen
274+
* `@object-ui/layout`'s entry point. What holds the two rules together is a
275+
* test that renders `NavigationRenderer` beside the rail and this canvas over
276+
* the same entries: the `StudioDesignSurface.railNavOrder-26979` block of
277+
* `StudioDesignSurface.railLabels.test.tsx`. ⛔ No third copy: sort a Studio
278+
* nav level through this.
279+
*
280+
* Exported for `NavTree`, as `navPayloadOf` is for the nav save: a pure
281+
* function, not a component, that never reaches the package entry.
282+
*/
283+
// eslint-disable-next-line react-refresh/only-export-components -- see above
284+
export function inMenuOrder<T extends { order?: number }>(list: readonly T[]): Array<readonly [T, number]> {
285+
return list.map((it, i) => [it, i] as const).sort(([a], [b]) => (a.order ?? 0) - (b.order ?? 0));
286+
}
287+
257288
export interface AppNavCanvasProps {
258289
draft: Record<string, unknown>;
259290
rootKey: string;
@@ -276,7 +307,12 @@ export function AppNavCanvas({
276307
return Array.isArray(v) ? (v as RawNav[]) : [];
277308
}, [draft, rootKey]);
278309

310+
// The root level as the menu lists it, each entry with its array index:
311+
// selection ids, renames and removals address the authored array.
312+
const shown = React.useMemo(() => inMenuOrder(items), [items]);
313+
279314
const selectedId = selection && selection.kind === 'nav' ? selection.id : null;
315+
/** The ARRAY index of the entry being dragged. */
280316
const [dragIndex, setDragIndex] = React.useState<number | null>(null);
281317

282318
const setItems = React.useCallback(
@@ -341,21 +377,44 @@ export function AppNavCanvas({
341377
[onPatch, items, setItems, rootKey, selectedId, onSelectionChange, locale],
342378
);
343379

380+
/**
381+
* Drop the entry at array index `from` before the entry SHOWN at position
382+
* `before` (`items.length` = at the end), as the menu will then draw it
383+
* (objectstack#26979).
384+
*
385+
* Reordering the array alone does nothing visible once `order` is set,
386+
* since the menu sorts by it. So the drop writes the array in the new
387+
* shown order, and writes the moved entry's `order` only when its current
388+
* weight no longer falls between its new neighbours': then it takes its
389+
* neighbour's weight, and the stable sort keeps it on the array side it was
390+
* dropped on. At most one `order` write per drop, and none for an app whose
391+
* entries carry no `order`.
392+
*/
344393
const moveItem = React.useCallback(
345394
(from: number, before: number) => {
346395
if (!onPatch) return;
396+
const seq = shown.map(([, i]) => i);
397+
const at = seq.indexOf(from);
398+
if (at < 0) return;
347399
let to = before;
348-
if (from < before) to = before - 1;
349-
if (to === from) return;
350-
const next = moveArray(items, from, to);
400+
if (at < before) to = before - 1;
401+
if (to === at) return;
402+
seq.splice(at, 1);
403+
seq.splice(to, 0, from);
404+
const next = seq.map((i) => items[i]);
405+
const moved = next[to];
406+
const prev = to > 0 ? (next[to - 1].order ?? 0) : -Infinity;
407+
const after = to < next.length - 1 ? (next[to + 1].order ?? 0) : Infinity;
408+
const own = moved.order ?? 0;
409+
if (own < prev || own > after) next[to] = { ...moved, order: to > 0 ? prev : after };
351410
setItems(next);
352411
onSelectionChange?.({
353412
kind: 'nav',
354413
id: `${rootKey}[${to}]`,
355414
label: navLabel(next[to] ?? {}, to, locale, targetLabel),
356415
});
357416
},
358-
[onPatch, items, setItems, rootKey, onSelectionChange, locale, targetLabel],
417+
[onPatch, items, shown, setItems, rootKey, onSelectionChange, locale, targetLabel],
359418
);
360419

361420
return (
@@ -394,7 +453,7 @@ export function AppNavCanvas({
394453
if (!e.dataTransfer.types.includes(DND_MIME)) return;
395454
e.preventDefault();
396455
if (dragIndex == null) return;
397-
moveItem(dragIndex, items.length);
456+
moveItem(dragIndex, shown.length);
398457
setDragIndex(null);
399458
}}
400459
>
@@ -405,7 +464,7 @@ export function AppNavCanvas({
405464
: t('engine.appNav.emptyReadonly', locale)}
406465
</div>
407466
) : (
408-
items.map((it, i) => (
467+
shown.map(([it, i], at) => (
409468
<NavCardTree
410469
key={i}
411470
item={it}
@@ -423,7 +482,7 @@ export function AppNavCanvas({
423482
onDragEnd={() => setDragIndex(null)}
424483
onDropBefore={() => {
425484
if (dragIndex == null) return;
426-
moveItem(dragIndex, i);
485+
moveItem(dragIndex, at);
427486
setDragIndex(null);
428487
}}
429488
/>
@@ -484,7 +543,7 @@ function NavCardTree({
484543
onDropBefore={onDropBefore}
485544
/>
486545
{Array.isArray(item.children) &&
487-
item.children.map((c, j) => {
546+
inMenuOrder(item.children).map(([c, j]) => {
488547
const childPath = `${path}.children[${j}]`;
489548
return (
490549
<NavCardTree

‎ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.railLabels.test.tsx‎

Lines changed: 199 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
22

33
/**
4-
* The Interfaces rail and its labels: ten pins that render the real
4+
* The Interfaces rail and its labels: the pins that render the real
55
* `InterfacesPillar` over one mocked metadata harness, folded into one file
66
* (objectstack#25911).
77
*
@@ -64,6 +64,7 @@ vi.mock('@object-ui/react', async (importOriginal) => {
6464
});
6565

6666
import { InterfacesPillar, StudioNavItemInspector } from './StudioDesignSurface';
67+
import { AppNavCanvas } from '../metadata-admin/previews/AppNavCanvas';
6768
import { t } from '../metadata-admin/i18n';
6869
import { listStudioCanvasPreviewTypes } from './studio-canvas-preview';
6970

@@ -1417,6 +1418,203 @@ describe('StudioDesignSurface.zhPillarNames-11801', () => {
14171418
});
14181419
});
14191420

1421+
// ── StudioDesignSurface.railNavOrder-26979 ───────────────────────────────────
1422+
1423+
/**
1424+
* objectstack#26979 — the Studio shows an app's nav entries in the order the
1425+
* running menu draws them.
1426+
*
1427+
* The menu sorts every level by `order` (`byNavOrder` in
1428+
* `packages/layout/src/navOrder.ts`: lower first, an absent `order` as `0`,
1429+
* ties in authored order), at the top level and inside each group. The
1430+
* Interfaces rail (`NavTree`) and the nav canvas (`AppNavCanvas`, which
1431+
* Setup's app editor mounts too) listed the authored array instead, so an
1432+
* entry given `order` stood in one place in Studio and another in the
1433+
* published console, and a canvas drag, which only reordered the array, could
1434+
* do nothing visible. Both now list a level through `inMenuOrder`, which
1435+
* states the menu's rule; `byNavOrder` is internal to its package, so these
1436+
* pins are what hold the two rules together: each renders the menu
1437+
* (`NavigationRenderer`) beside the rail and the canvas over the same entries.
1438+
*
1439+
* - entries with `order` [3, 1, 2] (and a group whose children carry
1440+
* [2, 1]) read in one order on all three surfaces;
1441+
* - a drag in the canvas leaves all three agreeing, writing `order` on the
1442+
* moved entry only;
1443+
* - CONTROL: entries with no `order` keep their array order, and a drag
1444+
* among them writes no `order` at all.
1445+
*/
1446+
describe('StudioDesignSurface.railNavOrder-26979', () => {
1447+
/** The navigation the mocked client serves the pillar; each test sets it. */
1448+
let served: Array<Record<string, unknown>> = [];
1449+
1450+
beforeEach(() => {
1451+
studio.client = mockClient;
1452+
});
1453+
1454+
const PKG = 'com.acme.app';
1455+
1456+
const page = (label: string, order?: number) => ({
1457+
id: `nav_${label.toLowerCase()}`,
1458+
type: 'page',
1459+
label,
1460+
pageName: label.toLowerCase(),
1461+
...(order === undefined ? {} : { order }),
1462+
});
1463+
1464+
/** Authored out of menu order: the menu reads Bravo, Charlie, Alpha, Folder (Yankee, Xray). */
1465+
const ORDERED = [
1466+
page('Alpha', 3),
1467+
page('Bravo', 1),
1468+
page('Charlie', 2),
1469+
{ id: 'nav_folder', type: 'group', label: 'Folder', order: 4, expanded: true, children: [page('Xray', 2), page('Yankee', 1)] },
1470+
];
1471+
1472+
/** No `order` anywhere: the menu reads the array as authored. */
1473+
const UNORDERED = [page('Alpha'), page('Bravo'), page('Charlie')];
1474+
1475+
const LABELS = ['Alpha', 'Bravo', 'Charlie', 'Folder', 'Xray', 'Yankee'];
1476+
1477+
const mockClient = {
1478+
list: vi.fn(async (type: string) => (type === 'app' ? [{ name: 'acme_app', label: 'Acme' }] : [])),
1479+
listDrafts: vi.fn(async () => []),
1480+
layered: vi.fn(async (type: string, name: string) => {
1481+
if (type === 'app') return { effective: { name: 'acme_app', label: 'Acme', navigation: served } };
1482+
if (type === 'page') return { effective: { name, label: name, type: 'app', regions: [{ name: 'main', components: [] }] } };
1483+
return { effective: { name } };
1484+
}),
1485+
getDraft: vi.fn(async () => null),
1486+
save: vi.fn(async () => ({})),
1487+
get: vi.fn(async () => undefined),
1488+
};
1489+
1490+
afterEach(cleanup);
1491+
1492+
/** The labels `el` shows, in the order it shows them; each must show once. */
1493+
function orderIn(el: HTMLElement): string[] {
1494+
const text = el.textContent ?? '';
1495+
const shown = LABELS.filter((l) => text.includes(l));
1496+
for (const l of shown) expect(text.indexOf(l)).toBe(text.lastIndexOf(l));
1497+
return shown.sort((a, b) => text.indexOf(a) - text.indexOf(b));
1498+
}
1499+
1500+
/** The running app's sidebar over `nav`. */
1501+
function menuOrder(nav: unknown[]): string[] {
1502+
const { container, unmount } = render(
1503+
<MemoryRouter initialEntries={['/apps/acme_app']}>
1504+
<SidebarProvider defaultOpen>
1505+
<NavigationRenderer items={nav as NavigationItem[]} basePath="/apps/acme_app" />
1506+
</SidebarProvider>
1507+
</MemoryRouter>,
1508+
);
1509+
const order = orderIn(container);
1510+
unmount();
1511+
return order;
1512+
}
1513+
1514+
/** The Studio Interfaces rail over `nav`, once its rows have rendered. */
1515+
async function railOrder(nav: Array<Record<string, unknown>>, first: string): Promise<string[]> {
1516+
served = nav;
1517+
const { unmount } = render(
1518+
<I18nProvider config={{ defaultLanguage: 'en', detectBrowserLanguage: false }} persistLanguage={false}>
1519+
<MemoryRouter initialEntries={[`/studio/${PKG}/interfaces`]}>
1520+
<InterfacesPillar packageId={PKG} />
1521+
</MemoryRouter>
1522+
</I18nProvider>,
1523+
);
1524+
// objectui#12455 — a page row is named by its label and its kind tag.
1525+
const row = await screen.findByRole('button', { name: `${first} Page` }, { timeout: 8000 });
1526+
const rail = row.closest('nav');
1527+
expect(rail).not.toBeNull();
1528+
const order = orderIn(rail as HTMLElement);
1529+
unmount();
1530+
return order;
1531+
}
1532+
1533+
/** The nav canvas over `nav`, holding its patches as the host's draft does. */
1534+
function renderCanvas(nav: unknown[]) {
1535+
const patches: unknown[][] = [];
1536+
function Host() {
1537+
const [draft, setDraft] = React.useState<Record<string, unknown>>({ navigation: nav });
1538+
return (
1539+
<div data-testid="nav-canvas">
1540+
<AppNavCanvas
1541+
draft={draft}
1542+
rootKey="navigation"
1543+
selection={null}
1544+
onPatch={(patch) => {
1545+
patches.push(patch.navigation as unknown[]);
1546+
setDraft((d) => ({ ...d, ...patch }));
1547+
}}
1548+
/>
1549+
</div>
1550+
);
1551+
}
1552+
render(<Host />);
1553+
return { canvas: screen.getByTestId('nav-canvas'), patches };
1554+
}
1555+
1556+
/** The canvas card for `label`: the row button whose text starts with it. */
1557+
const card = (label: string) =>
1558+
screen.getAllByRole('button').find((b) => b.getAttribute('aria-pressed') !== null && b.textContent?.startsWith(label))!;
1559+
1560+
/** Drag the card `label` and drop it before the card `before`, as a pointer does. */
1561+
function dragBefore(label: string, before: string) {
1562+
// The canvas's own drag type (`DND_MIME` in `AppNavCanvas.tsx`); a drop of any other type is ignored.
1563+
const dataTransfer = { types: ['text/x-objectui-nav'], setData: vi.fn(), getData: vi.fn(), effectAllowed: '', dropEffect: '' };
1564+
fireEvent.dragStart(card(label), { dataTransfer });
1565+
fireEvent.dragOver(card(before), { dataTransfer });
1566+
fireEvent.drop(card(before), { dataTransfer });
1567+
}
1568+
1569+
it('the fixtures are what the spec accepts', () => {
1570+
for (const item of [...ORDERED, ...UNORDERED]) expect(NavigationItemSchema.safeParse(item).success).toBe(true);
1571+
});
1572+
1573+
it('entries with order [3, 1, 2] read in the menu order on the rail, the canvas and the menu', async () => {
1574+
const menu = menuOrder(ORDERED);
1575+
// Measured on the menu first: it sorts the top level and the group's children.
1576+
expect(menu).toEqual(['Bravo', 'Charlie', 'Alpha', 'Folder', 'Yankee', 'Xray']);
1577+
expect(await railOrder(ORDERED, 'Bravo')).toEqual(menu);
1578+
const { canvas } = renderCanvas(ORDERED);
1579+
expect(orderIn(canvas)).toEqual(menu);
1580+
});
1581+
1582+
it('a drag in the canvas leaves all three agreeing, and writes order on the moved entry only', async () => {
1583+
const { canvas, patches } = renderCanvas(ORDERED);
1584+
// Alpha (order 3, shown third) to the top, before Bravo (order 1).
1585+
dragBefore('Alpha', 'Bravo');
1586+
expect(patches).toHaveLength(1);
1587+
const next = patches[0] as Array<Record<string, unknown>>;
1588+
expect(orderIn(canvas)).toEqual(['Alpha', 'Bravo', 'Charlie', 'Folder', 'Yankee', 'Xray']);
1589+
// One `order` write: Alpha takes the weight of the entry it now precedes.
1590+
// Every other entry is the object it was.
1591+
const before = new Map(ORDERED.map((it) => [it.id, it]));
1592+
for (const it of next) {
1593+
if (it.id === 'nav_alpha') expect(it).toEqual({ ...before.get('nav_alpha'), order: 1 });
1594+
else expect(it).toBe(before.get(it.id as string));
1595+
}
1596+
cleanup();
1597+
const menu = menuOrder(next);
1598+
expect(menu).toEqual(['Alpha', 'Bravo', 'Charlie', 'Folder', 'Yankee', 'Xray']);
1599+
expect(await railOrder(next, 'Alpha')).toEqual(menu);
1600+
});
1601+
1602+
it('CONTROL: entries with no order keep their array order, and a drag among them writes no order', async () => {
1603+
const menu = menuOrder(UNORDERED);
1604+
expect(menu).toEqual(['Alpha', 'Bravo', 'Charlie']);
1605+
expect(await railOrder(UNORDERED, 'Alpha')).toEqual(menu);
1606+
const { canvas, patches } = renderCanvas(UNORDERED);
1607+
expect(orderIn(canvas)).toEqual(menu);
1608+
dragBefore('Charlie', 'Alpha');
1609+
const next = patches[0] as Array<Record<string, unknown>>;
1610+
expect(next.map((it) => it.id)).toEqual(['nav_charlie', 'nav_alpha', 'nav_bravo']);
1611+
for (const it of next) expect(it).not.toHaveProperty('order');
1612+
expect(orderIn(canvas)).toEqual(['Charlie', 'Alpha', 'Bravo']);
1613+
cleanup();
1614+
expect(menuOrder(next)).toEqual(['Charlie', 'Alpha', 'Bravo']);
1615+
});
1616+
});
1617+
14201618
// ── StudioDesignSurface.navEntryIdentity-11774 ───────────────────────────────
14211619

14221620
/**

0 commit comments

Comments
 (0)