Persist device row moves

This commit is contained in:
2026-07-25 21:09:02 +02:00
parent 2eba4ea75e
commit 08a2775a88
25 changed files with 391 additions and 1678 deletions
+99 -45
View File
@@ -27,6 +27,9 @@ import {
buildCircuitDeviceRowInsertSnapshot,
buildCircuitInsertSnapshot,
} from "../utils/circuit-structure-command";
import {
buildCircuitDeviceRowMoveAssignments,
} from "../utils/circuit-device-row-move-command";
import type {
CellKey,
CellKind,
@@ -40,7 +43,6 @@ import {
} from "../utils/circuit-grid-projection";
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
import {
deleteCircuitById,
deleteCircuitCommand,
deleteCircuitDeviceRowCommand,
getCircuitTree,
@@ -48,8 +50,8 @@ import {
insertCircuitCommand,
insertCircuitDeviceRowCommand,
listProjectDevices,
moveCircuitDeviceRowsBulk,
moveCircuitDeviceRowById,
moveCircuitDeviceRowsCommand,
moveCircuitDeviceRowsToNewCircuitCommand,
reorderSectionCircuits,
renumberCircuitSection,
redoProjectCommand,
@@ -2230,8 +2232,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
});
}
// Handles device-row drag intent (single or bulk) and preserves enough source grouping
// for deterministic undo back to original circuits.
// Handles single and bulk device-row drag through persistent project history.
// Exact source and target positions make undo deterministic.
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
event.preventDefault();
event.stopPropagation();
@@ -2269,65 +2271,117 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return;
}
// Device-row drag supports bulk selection, but target intent is always explicit:
// move into existing circuit or create new circuit in a target section.
const circuits = (data?.sections ?? []).flatMap(
(section) => section.circuits
);
const label =
rowIds.length > 1
? `${rowIds.length} Gerätezeilen verschieben`
: "Gerätezeile verschieben";
if (intent.kind === "move-to-circuit") {
const groupedBySource = new Map<string, string[]>();
for (const rowId of rowIds) {
const source = sourceByRowId.get(rowId)!;
if (!groupedBySource.has(source)) {
groupedBySource.set(source, []);
}
groupedBySource.get(source)!.push(rowId);
const targetCircuit = circuits.find(
(circuit) => circuit.id === intent.circuitId
);
if (!targetCircuit) {
setError("Der Zielstromkreis ist ungültig.");
return;
}
const moves = buildCircuitDeviceRowMoveAssignments({
circuits,
rowIds,
targetCircuitId: targetCircuit.id,
targetDeviceRows: targetCircuit.deviceRows,
});
if (moves.length === 0) {
return;
}
await runCommand({
label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen verschieben` : "Gerätezeile verschieben",
label,
redo: async () => {
await moveCircuitDeviceRowsBulk({ rowIds, targetCircuitId: intent.circuitId });
const result = await moveCircuitDeviceRowsCommand(
projectId,
getExpectedProjectRevision(),
moves,
label
);
applyProjectCommandResult(result);
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
undo: async () => {
for (const [source, ids] of groupedBySource) {
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
}
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
undo: async () => null,
persistent: {
undoIntent: () => {
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
redoIntent: () => {
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
},
});
return;
}
let createdCircuitId: string | null = null;
const groupedBySource = new Map<string, string[]>();
for (const rowId of rowIds) {
const source = sourceByRowId.get(rowId)!;
if (!groupedBySource.has(source)) {
groupedBySource.set(source, []);
}
groupedBySource.get(source)!.push(rowId);
const targetSection = data?.sections.find(
(section) => section.id === intent.sectionId
);
if (!targetSection) {
setError("Der Zielbereich ist ungültig.");
return;
}
const newCircuitLabel =
rowIds.length > 1
? `${rowIds.length} Gerätezeilen in neuen Stromkreis verschieben`
: "Gerätezeile in neuen Stromkreis verschieben";
await runCommand({
label: rowIds.length > 1 ? `${rowIds.length} Gerätezeilen in neuen Stromkreis verschieben` : "Gerätezeile in neuen Stromkreis verschieben",
label: newCircuitLabel,
redo: async () => {
const moved = await moveCircuitDeviceRowsBulk({
rowIds,
targetSectionId: intent.sectionId,
createNewCircuit: true,
const next = await getNextCircuitIdentifier(intent.sectionId);
const sortOrder =
targetSection.circuits.length > 0
? Math.max(
...targetSection.circuits.map(
(circuit) => circuit.sortOrder
)
) + 10
: 10;
const targetCircuit = createCircuitSnapshot({
sectionId: intent.sectionId,
equipmentIdentifier: next.nextIdentifier,
displayName: "Neuer Stromkreis",
sortOrder,
isReserve: true,
});
createdCircuitId = moved.createdCircuitId ?? null;
const moves = buildCircuitDeviceRowMoveAssignments({
circuits,
rowIds,
targetCircuitId: targetCircuit.id,
targetDeviceRows: [],
});
const result =
await moveCircuitDeviceRowsToNewCircuitCommand(
projectId,
getExpectedProjectRevision(),
targetCircuit,
moves,
newCircuitLabel
);
applyProjectCommandResult(result);
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
undo: async () => {
for (const [source, ids] of groupedBySource) {
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
}
if (createdCircuitId) {
await deleteCircuitById(createdCircuitId);
}
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
undo: async () => null,
persistent: {
undoIntent: () => {
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
redoIntent: () => {
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
return null;
},
},
});
}