Skip to content

Commit ecf9048

Browse files
feat(dashboard): dock tab-drag — draggable headers + within-container reorder commit (#14850) (#14851)
The dock's DockZoneModel ops were unit-green, but no gesture reached them: the first whitebox-e2e proved the rendered tab headers were not draggable at all and a native drag left the committed dockZone.v1 document unchanged. DockLayoutAdapter.projectTabsNode now projects dragResortable:true on each tabs node and commits the tab.Container moveTo event through applyDockZoneOperation + onDockZoneDocumentChange — reusing the EXISTING tab-header SortZone (ADR 0029 'no parallel drag system'). No new drag machinery, no framework change: the reference tab.Container example already proves the primitive; the dock only had to project it. Ships its whitebox-e2e in the same PR (gesture-proof guardrail): dragging 'Strategy' past 'Swarm' reorders the App-Worker model ['strategy','swarm'] -> ['swarm','strategy'].
1 parent ca8eee7 commit ecf9048

2 files changed

Lines changed: 93 additions & 2 deletions

File tree

src/dashboard/DockLayoutAdapter.mjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,8 +501,24 @@ class DockLayoutAdapter extends Base {
501501
cls : ['neo-dashboard-dock-tabs'],
502502
dockNodeId : nodeId,
503503
dockNodeType: 'tabs',
504-
items : items.map(itemId => this.projectItem(itemId, context)),
505-
ntype : 'tab-container'
504+
// Reuse the EXISTING tab-header SortZone for the gesture — no parallel drag system:
505+
// `dragResortable` makes the headers draggable, and the `moveTo` the container fires on drop
506+
// commits the reorder into the COMMITTED dock model through the landed operation seam. The
507+
// tab.Container drags; the model owns the result. (Cross-zone drag rides the dashboard SortZone
508+
// in a follow-up slice.)
509+
dragResortable: true,
510+
items : items.map(itemId => this.projectItem(itemId, context)),
511+
listeners : {
512+
moveTo: data => {
513+
let itemId = items[data.fromIndex],
514+
result = context.applyDockZoneOperation?.({operation: 'addTab', itemId, tabsNodeId: nodeId, index: data.toIndex});
515+
516+
if (result && !result.errors?.length && result.document) {
517+
context.onDockZoneDocumentChange?.(result.document, {operation: 'addTab', itemId, tabsNodeId: nodeId, index: data.toIndex}, null)
518+
}
519+
}
520+
},
521+
ntype: 'tab-container'
506522
}
507523
}
508524
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { test, expect } from '../../fixtures.mjs';
2+
3+
/**
4+
* Whitebox-e2e: the FIRST end-to-end proof that the dock ENGINE reorders a pane on a real drag gesture —
5+
* not the JSON model in isolation (that is unit-tested), but a rendered tab header dragged in the live
6+
* example, asserted against the committed dockZone.v1 document in the App Worker.
7+
*
8+
* Scope (this slice): WITHIN-container reorder. Dragging the "Strategy" tab header past its sibling
9+
* "Swarm" inside `main-tabs` reorders the committed model from ['strategy','swarm'] to
10+
* ['swarm','strategy']. The gesture rides the EXISTING tab-header SortZone (no parallel drag system);
11+
* the DockLayoutAdapter projects `dragResortable: true` and its `moveTo` listener commits the result
12+
* through applyDockZoneOperation + onDockZoneDocumentChange. Cross-zone drag (an item leaving its tabs
13+
* node) rides the dashboard SortZone in a follow-up slice.
14+
*
15+
* Paradigm (whitebox-e2e protocol): Playwright drives the native mouse gesture; the Neural Link fixture
16+
* reads the holder's committed document (dockModel) before and after. Two product-level truths are
17+
* asserted: (1) the tab headers are actually draggable in the DOM, (2) the drag mutates worker truth.
18+
*
19+
* Run: npx playwright test DockDragDropNL -c test/playwright/playwright.config.e2e.mjs --workers=1
20+
*/
21+
test.describe('Dock drag-and-drop journey (Neural Link)', () => {
22+
test.setTimeout(90000);
23+
test.use({ viewport: { width: 1600, height: 900 } });
24+
25+
test('dragging a tab header past its sibling reorders the item in the committed dock model', async ({ page, neuralLink }) => {
26+
await page.goto('/examples/dashboard/dock/');
27+
page.on('pageerror', err => console.error('BROWSER JS ERROR:', err));
28+
29+
await page.waitForTimeout(2500); // settle worker boot + first render
30+
31+
const app = await neuralLink.connectToApp('Neo.examples.dashboard.dock');
32+
33+
// resolve the dock holder + a reader over its committed document (worker truth)
34+
const holders = await app.findInstances({ className: 'Neo.examples.dashboard.dock.MainContainer' }, ['id']);
35+
const holderId = Array.isArray(holders) ? holders[0]?.id : holders?.id;
36+
expect(holderId, 'the dock MainContainer must exist in the App Worker').toBeTruthy();
37+
38+
const readModel = async () => (await app.getComponent(holderId, ['dockModel'])).dockModel;
39+
40+
const before = await readModel();
41+
expect(before?.nodes?.['main-tabs']?.items, 'the example must seed main-tabs as [strategy, swarm]')
42+
.toEqual(['strategy', 'swarm']);
43+
44+
// product truth #1: the dock projects draggable tab headers (not just a static model). Both panes of
45+
// main-tabs plus the two single-tab side zones = four draggable tab headers.
46+
const draggableTabHeaders = await page.evaluate(() =>
47+
document.querySelectorAll('.neo-tab-header-button.neo-draggable').length);
48+
expect(draggableTabHeaders, 'the dock tab headers must be draggable (dragResortable projected)')
49+
.toBeGreaterThanOrEqual(2);
50+
51+
// both headers live in main-tabs' toolbar
52+
const strategyTab = page.locator('.neo-tab-header-button', { hasText: 'Strategy' }).first();
53+
const swarmTab = page.locator('.neo-tab-header-button', { hasText: 'Swarm' }).first();
54+
await expect(strategyTab, 'the Strategy tab header must render').toBeVisible({ timeout: 10000 });
55+
await expect(swarmTab, 'the Swarm tab header must render').toBeVisible({ timeout: 10000 });
56+
57+
const strategyBox = await strategyTab.boundingBox();
58+
const swarmBox = await swarmTab.boundingBox();
59+
60+
// native drag: Strategy header -> just past Swarm's right edge (the {steps} cadence arms Neo's drag sensor)
61+
await page.mouse.move(strategyBox.x + strategyBox.width / 2, strategyBox.y + strategyBox.height / 2);
62+
await page.mouse.down();
63+
await page.mouse.move(swarmBox.x + swarmBox.width / 2, swarmBox.y + swarmBox.height / 2, { steps: 30 });
64+
await page.mouse.move(swarmBox.x + swarmBox.width + 12, swarmBox.y + swarmBox.height / 2, { steps: 30 });
65+
await page.mouse.up();
66+
await page.waitForTimeout(1000);
67+
68+
const after = await readModel();
69+
console.log('[dock-dnd] main-tabs:', JSON.stringify(before?.nodes?.['main-tabs']?.items), '->', JSON.stringify(after?.nodes?.['main-tabs']?.items));
70+
71+
// product truth #2: the drag mutated the COMMITTED dock model in the App Worker
72+
expect(after?.nodes?.['main-tabs']?.items, 'the tab-drag must reorder the committed dock model — the whole point of the engine')
73+
.toEqual(['swarm', 'strategy']);
74+
});
75+
});

0 commit comments

Comments
 (0)