Persist device row moves
This commit is contained in:
@@ -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;
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
+37
-24
@@ -33,6 +33,9 @@ import type {
|
||||
import type {
|
||||
CircuitSnapshot,
|
||||
} from "../../domain/models/circuit-structure-project-command.model";
|
||||
import type {
|
||||
CircuitDeviceRowMoveAssignment,
|
||||
} from "../../domain/models/circuit-device-row-move-project-command.model";
|
||||
|
||||
async function request<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
@@ -212,12 +215,6 @@ export function deleteCircuitCommand(
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteCircuitById(circuitId: string) {
|
||||
return request<void>(`/api/circuits/${circuitId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
export function getNextCircuitIdentifier(sectionId: string) {
|
||||
return request<{ sectionId: string; nextIdentifier: string }>(
|
||||
`/api/circuit-sections/${sectionId}/next-identifier`
|
||||
@@ -275,28 +272,44 @@ export function deleteCircuitDeviceRowCommand(
|
||||
);
|
||||
}
|
||||
|
||||
export function moveCircuitDeviceRowById(
|
||||
rowId: string,
|
||||
input: { targetCircuitId?: string; targetSectionId?: string; createNewCircuit?: boolean }
|
||||
export function moveCircuitDeviceRowsCommand(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
moves: CircuitDeviceRowMoveAssignment[],
|
||||
description: string
|
||||
) {
|
||||
return request(`/api/circuit-device-rows/${rowId}/move`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return executeProjectCommand(
|
||||
projectId,
|
||||
expectedRevision,
|
||||
{
|
||||
schemaVersion: 1,
|
||||
type: "circuit-device-row.move",
|
||||
payload: { moves },
|
||||
},
|
||||
description
|
||||
);
|
||||
}
|
||||
|
||||
export function moveCircuitDeviceRowsBulk(input: {
|
||||
rowIds: string[];
|
||||
targetCircuitId?: string;
|
||||
targetSectionId?: string;
|
||||
createNewCircuit?: boolean;
|
||||
}) {
|
||||
return request<{ movedRowIds: string[]; targetCircuitId: string; createdCircuitId?: string }>(
|
||||
"/api/circuit-device-rows/move-bulk",
|
||||
export function moveCircuitDeviceRowsToNewCircuitCommand(
|
||||
projectId: string,
|
||||
expectedRevision: number,
|
||||
targetCircuit: CircuitSnapshot,
|
||||
moves: CircuitDeviceRowMoveAssignment[],
|
||||
description: string
|
||||
) {
|
||||
return executeProjectCommand(
|
||||
projectId,
|
||||
expectedRevision,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
}
|
||||
schemaVersion: 1,
|
||||
type: "circuit-device-row.move-with-new-circuit",
|
||||
payload: {
|
||||
targetCircuitAction: "create",
|
||||
targetCircuit,
|
||||
moves,
|
||||
},
|
||||
},
|
||||
description
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
} from "../types.js";
|
||||
import type {
|
||||
CircuitDeviceRowMoveAssignment,
|
||||
} from "../../domain/models/circuit-device-row-move-project-command.model.js";
|
||||
|
||||
export function buildCircuitDeviceRowMoveAssignments(input: {
|
||||
circuits: CircuitTreeCircuitDto[];
|
||||
rowIds: string[];
|
||||
targetCircuitId: string;
|
||||
targetDeviceRows: CircuitTreeDeviceRowDto[];
|
||||
}): CircuitDeviceRowMoveAssignment[] {
|
||||
const { circuits, rowIds, targetCircuitId, targetDeviceRows } =
|
||||
input;
|
||||
if (new Set(rowIds).size !== rowIds.length) {
|
||||
throw new Error("Gerätezeilen dürfen nicht mehrfach ausgewählt sein.");
|
||||
}
|
||||
|
||||
const sourceByRowId = new Map<
|
||||
string,
|
||||
{ circuitId: string; sortOrder: number }
|
||||
>();
|
||||
for (const circuit of circuits) {
|
||||
for (const row of circuit.deviceRows) {
|
||||
sourceByRowId.set(row.id, {
|
||||
circuitId: circuit.id,
|
||||
sortOrder: row.sortOrder,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const movedRowIds = new Set(rowIds);
|
||||
const retainedTargetRows = targetDeviceRows.filter(
|
||||
(row) => !movedRowIds.has(row.id)
|
||||
);
|
||||
const lastTargetSortOrder = retainedTargetRows.reduce(
|
||||
(highest, row) => Math.max(highest, row.sortOrder),
|
||||
0
|
||||
);
|
||||
|
||||
return rowIds.flatMap((rowId, index) => {
|
||||
const source = sourceByRowId.get(rowId);
|
||||
if (!source) {
|
||||
throw new Error("Eine ausgewählte Gerätezeile wurde nicht gefunden.");
|
||||
}
|
||||
const targetSortOrder =
|
||||
lastTargetSortOrder + (index + 1) * 10;
|
||||
if (
|
||||
source.circuitId === targetCircuitId &&
|
||||
source.sortOrder === targetSortOrder
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
rowId,
|
||||
expectedCircuitId: source.circuitId,
|
||||
expectedSortOrder: source.sortOrder,
|
||||
targetCircuitId,
|
||||
targetSortOrder,
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user