Drag a whole selection and join existing groups in the hierarchy (#241)
game-maker / gate (push) Successful in 3m26s
game-maker / suite (push) Failing after 9m12s

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:
adminandClaude Opus 5.5 committed 2026-09-27 11:19:12 +02:00
1 parent 893b81f798
commit 2eeabf7c23
10 files changed
+327 -64

No files matched your search

+16 -3
View File
@@ -79,15 +79,22 @@ export class HierarchyPanel extends Panel {
this.refresh();
}
/** A move that leaves the order as it is does nothing, so a drag back to its start costs no undo step. */
/**
* A row dragged out of the multi-selection takes the other selected objects along. A move that
* leaves the order as it is does nothing, so a drag back to its start costs no undo step.
*/
private drop(draggedId: string, spot: DropSpot): void {
const holder = this.holder();
if (!holder) return;
const { commands, unfold } = hierarchyDrop(holder, draggedId, spot, (rowId) => this.expanded.has(rowId));
const s = this.state.selection;
const selected = s?.kind === 'object' && s.holderId === holder.id ? s.objectIds : [];
const isOpen = (rowId: string) => this.expanded.has(rowId);
const { commands, unfold } = hierarchyDrop(holder, draggedId, spot, isOpen, selected);
if (commands.length === 0) return;
// Unfold where the row went, otherwise it would seem to vanish on drop.
for (const rowId of unfold) this.expanded.add(rowId);
this.state.execute(oneStep('Move object', commands));
const many = selected.length > 1 && selected.includes(draggedId);
this.state.execute(oneStep(many ? 'Move objects' : 'Move object', commands));
}
/** Ctrl-click on a group row: all members join the selection, or leave it when all were in. */
@@ -185,6 +192,11 @@ export class HierarchyPanel extends Panel {
return this.list.iconGlobal(id);
}
/** Global centre of a row's hint – a group's member count (test hook). */
hintPosition(id: string): { x: number; y: number } | null {
return this.list.hintGlobal(id);
}
expandAll(): void {
const holder = this.holder();
for (const object of holder?.objects ?? []) this.expanded.add(object.id);
@@ -263,6 +275,7 @@ export class HierarchyPanel extends Panel {
label: row.group.name || 'Group',
hint: String(row.memberIds.length),
icon: GROUP_ICON_SVG,
iconAtEnd: true,
depth: 0,
expanded: this.expanded.has(groupRowId(row.group.id)),
overridden: row.memberIds.some((id) => marks.has(id)),