Drag a whole selection and join existing groups in the hierarchy (#241)
Three follow-ups to the hierarchy groups. Dragging a row that belonged to a multi-selection moved only that one row. The group dialog could only create a new group. The group row drew its folder in front of the label, unlike every other row in the hierarchy. - Dragging a selected row moves every selected object. This holds for reordering, dropping into a group or between its members, dragging out of a group and dropping onto an object as new parent. - The moved objects land side by side in hierarchy order, whichever row was grabbed. The whole move is one undo step. - A selected child of a selected object travels with its parent, not on its own. Dropping the selection onto its own rows does nothing. - Dragging a row that is not selected still moves only that row. - hierarchyDrop.moveObjects replaces the single-object rule. One object is the case with one element, so the old drop tests hold unchanged. - The group dialog has a "Group" dropdown: New group or an existing group. Picking a group greys out the name and turns the button into "Add". A group that holds all chosen objects already is not offered. - Rename keeps using the dialog without the dropdown. - ListRow draws an end icon at the very right and the hint left of it. Only the hierarchy uses iconAtEnd, so other lists do not change. - New test hook __gm.hierarchyHint() gives the centre of a row's hint. Not clicked in the browser: dragging a selection in the container tab and onto an object as parent. Only the unit tests cover these. Tests: - ./check.sh: 178 files, 2564 tests, CHECK EXIT 0 - e2e 1 + 52: ALL PASSED, 27 checks - 7 new unit tests in hierarchy-drop.test.ts, 4 new e2e checks in 52i and 52j with real clicks and a real drag - 4 counter-probes bite: drop rule ignores the selection (5 unit tests fail), icon column not kept free, dialog ignores the picked group, panel ignores the selection (only o2 lands in the group) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
893b81f798
commit
2eeabf7c23
10 files changed
+327
-64
No files matched your search
@@ -169,4 +169,76 @@ export async function hierarchyGroups(t) {
|
||||
JSON.stringify({ down, primaryAfter, up, rows: await rowIds() }),
|
||||
);
|
||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||
|
||||
// 52i. A group "Pile" of o5 alone; its folder stands in the icon column of the objects, the
|
||||
// count left of it. Then o1 and o2 join it through the dialog's group dropdown.
|
||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o5', objectIds: ['o5'] });
|
||||
const lone = await rowAt('o5');
|
||||
await click(lone.x, lone.y, 'right');
|
||||
await clickText('Group…');
|
||||
await typeText('Pile');
|
||||
await press('Enter');
|
||||
const pile = (await scene()).groups?.[0];
|
||||
const pileRow = `group:${pile?.id}`;
|
||||
const folder = await json(`__gm.hierarchyIcon(${JSON.stringify(pileRow)})`);
|
||||
const count = await json(`__gm.hierarchyHint(${JSON.stringify(pileRow)})`);
|
||||
const objectIcon = await json('__gm.hierarchyIcon("o1")');
|
||||
check(
|
||||
"a group's folder stands in the objects' icon column, its count left of it",
|
||||
folder !== null &&
|
||||
objectIcon !== null &&
|
||||
count !== null &&
|
||||
Math.abs(folder.x - objectIcon.x) < 1 &&
|
||||
count.x < folder.x - 8,
|
||||
JSON.stringify({ folder, objectIcon, count }),
|
||||
);
|
||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2'] });
|
||||
const joiner = await rowAt('o1');
|
||||
await click(joiner.x, joiner.y, 'right');
|
||||
await clickText('Group 2 objects…');
|
||||
await clickText('New group');
|
||||
const choices = (await json('__gm.menuRows()')) ?? [];
|
||||
// The last "Pile" drawn is the menu's, above the hierarchy row of the same name.
|
||||
const pick = await json('__gm.locate("Pile", true)');
|
||||
await click(pick.x, pick.y);
|
||||
await shot('52-join-group');
|
||||
await clickText('Add');
|
||||
const joined = await scene();
|
||||
check(
|
||||
'the Group dialog puts the objects into an existing group picked from its dropdown',
|
||||
same(choices, ['New group', 'Pile']) &&
|
||||
joined.groups?.length === 1 &&
|
||||
same(
|
||||
joined.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
||||
['o1', 'o2', 'o5'],
|
||||
),
|
||||
JSON.stringify({ choices, groups: joined.groups, objects: joined.objects.map((o) => [o.id, o.groupId]) }),
|
||||
);
|
||||
await undo();
|
||||
|
||||
// 52j. Dragging one row of a multi-selection takes the others along – here onto the group row.
|
||||
await select({ kind: 'object', holderId: 'scene1', objectId: 'o1', objectIds: ['o1', 'o2'] });
|
||||
const grab = await rowAt('o2');
|
||||
const into = await rowAt(pileRow);
|
||||
await drag(grab.x, grab.y, into.x, into.y);
|
||||
const dragged = await scene();
|
||||
const inPile = dragged.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id);
|
||||
await undo();
|
||||
const undone = await scene();
|
||||
check(
|
||||
'dragging a selected row moves the whole selection, in one undo step',
|
||||
same(inPile, ['o5', 'o1', 'o2']) &&
|
||||
same(
|
||||
undone.objects.filter((o) => o.groupId === pile?.id).map((o) => o.id),
|
||||
['o5'],
|
||||
),
|
||||
JSON.stringify({ inPile, undone: undone.objects.map((o) => [o.id, o.groupId]) }),
|
||||
);
|
||||
await undo();
|
||||
check(
|
||||
'undo takes the group of o5 back too',
|
||||
(await scene()).groups === undefined,
|
||||
JSON.stringify((await scene()).groups),
|
||||
);
|
||||
await evaluate('__gm.state.select(null); __gm.hierarchyCollapseAll()');
|
||||
}
|
||||
Reference in new issue
Block a user