Persist circuit reorders

This commit is contained in:
2026-07-25 21:22:10 +02:00
parent 08a2775a88
commit abcb468807
26 changed files with 914 additions and 372 deletions
+80 -32
View File
@@ -30,6 +30,9 @@ import {
import {
buildCircuitDeviceRowMoveAssignments,
} from "../utils/circuit-device-row-move-command";
import {
buildCircuitSectionReorderAssignments,
} from "../utils/circuit-section-reorder-command";
import type {
CellKey,
CellKind,
@@ -52,7 +55,8 @@ import {
listProjectDevices,
moveCircuitDeviceRowsCommand,
moveCircuitDeviceRowsToNewCircuitCommand,
reorderSectionCircuits,
reorderCircuitSectionCommand,
reorderCircuitSectionsCommand,
renumberCircuitSection,
redoProjectCommand,
updateSectionEquipmentIdentifiers,
@@ -991,22 +995,42 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return;
}
// Sorting is view-only until explicitly applied; this avoids accidental persisted reorders.
const reorderedSections = changedSections.map((changedSection) => {
const currentSection = data.sections.find(
(section) => section.id === changedSection.sectionId
);
if (!currentSection) {
throw new Error("Ein sortierter Bereich wurde nicht gefunden.");
}
return {
sectionId: currentSection.id,
assignments: buildCircuitSectionReorderAssignments(
currentSection.circuits,
changedSection.order
),
};
});
// Sorting is view-only until explicitly applied. All affected sections are
// then committed as one atomic project-history entry.
await runCommand({
label: "Sortierte Reihenfolge übernehmen",
redo: async () => {
for (const section of changedSections) {
await reorderSectionCircuits(section.sectionId, section.order);
}
const result = await reorderCircuitSectionsCommand(
projectId,
getExpectedProjectRevision(),
reorderedSections,
"Sortierte Reihenfolge übernehmen"
);
applyProjectCommandResult(result);
setSortState(null);
setOpenFilterColumn(null);
return null;
},
undo: async () => {
for (const section of beforeBySection) {
await reorderSectionCircuits(section.sectionId, section.order);
}
return null;
undo: async () => null,
persistent: {
undoIntent: () => null,
redoIntent: () => null,
},
});
}
@@ -2112,7 +2136,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
// Applies same-section circuit reorder as explicit id ordering.
// No implicit renumbering is performed here.
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) {
function resolveCircuitReorderOrder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) {
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
if (!section) {
throw new Error("Der Bereich ist ungültig.");
@@ -2135,7 +2159,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
const insertIndex = intent.kind === "after-circuit" ? targetIndex + 1 : targetIndex;
nextIds.splice(insertIndex, 0, ...block);
}
await reorderSectionCircuits(section.id, nextIds);
return nextIds;
}
// Handles project-device drag intent. This path creates linked rows/circuits and
@@ -2414,31 +2438,55 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
setError("Stromkreise können derzeit nicht bereichsübergreifend verschoben werden.");
return;
}
const beforeOrder = section.circuits.map((circuit) => circuit.id);
const primaryCircuitId = sourceCircuitIds[0];
const targetOrder = resolveCircuitReorderOrder(
intent,
sourceCircuitIds
);
const assignments = buildCircuitSectionReorderAssignments(
section.circuits,
targetOrder
);
if (assignments.length === 0) {
return;
}
const label =
sourceCircuitIds.length > 1
? `${sourceCircuitIds.length} Stromkreise neu anordnen`
: "Stromkreise neu anordnen";
const selectionIntent: SelectionIntent = {
rowKey: `circuitSummary:${primaryCircuitId}`,
cellKey: "equipmentIdentifier",
rowType: "circuitSummary",
sectionId: intent.sectionId,
circuitId: primaryCircuitId,
};
await runCommand({
label: sourceCircuitIds.length > 1 ? `${sourceCircuitIds.length} Stromkreise neu anordnen` : "Stromkreise neu anordnen",
label,
redo: async () => {
await applyCircuitReorder(intent, sourceCircuitIds);
const result = await reorderCircuitSectionCommand(
projectId,
getExpectedProjectRevision(),
intent.sectionId,
assignments,
label
);
applyProjectCommandResult(result);
pendingSelectedCircuitIdsAfterReload.current = sourceCircuitIds;
return {
rowKey: `circuitSummary:${primaryCircuitId}`,
cellKey: "equipmentIdentifier",
rowType: "circuitSummary",
sectionId: intent.sectionId,
circuitId: primaryCircuitId,
};
return selectionIntent;
},
undo: async () => {
await reorderSectionCircuits(intent.sectionId, beforeOrder);
pendingSelectedCircuitIdsAfterReload.current = sourceCircuitIds;
return {
rowKey: `circuitSummary:${primaryCircuitId}`,
cellKey: "equipmentIdentifier",
rowType: "circuitSummary",
sectionId: intent.sectionId,
circuitId: primaryCircuitId,
};
undo: async () => null,
persistent: {
undoIntent: () => {
pendingSelectedCircuitIdsAfterReload.current =
sourceCircuitIds;
return selectionIntent;
},
redoIntent: () => {
pendingSelectedCircuitIdsAfterReload.current =
sourceCircuitIds;
return selectionIntent;
},
},
});
}
+41 -5
View File
@@ -36,6 +36,12 @@ import type {
import type {
CircuitDeviceRowMoveAssignment,
} from "../../domain/models/circuit-device-row-move-project-command.model";
import type {
CircuitSectionReorderAssignment,
} from "../../domain/models/circuit-section-reorder-project-command.model";
import type {
CircuitSectionsReorderSection,
} from "../../domain/models/circuit-sections-reorder-project-command.model";
async function request<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, {
@@ -319,11 +325,41 @@ export function renumberCircuitSection(sectionId: string) {
});
}
export function reorderSectionCircuits(sectionId: string, orderedCircuitIds: string[]) {
return request(`/api/circuit-sections/${sectionId}/circuits/reorder`, {
method: "PATCH",
body: JSON.stringify({ orderedCircuitIds }),
});
export function reorderCircuitSectionCommand(
projectId: string,
expectedRevision: number,
sectionId: string,
assignments: CircuitSectionReorderAssignment[],
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit.reorder-section",
payload: { sectionId, assignments },
},
description
);
}
export function reorderCircuitSectionsCommand(
projectId: string,
expectedRevision: number,
sections: CircuitSectionsReorderSection[],
description: string
) {
return executeProjectCommand(
projectId,
expectedRevision,
{
schemaVersion: 1,
type: "circuit.reorder-sections",
payload: { sections },
},
description
);
}
export function updateSectionEquipmentIdentifiers(
@@ -0,0 +1,44 @@
import type {
CircuitSectionReorderAssignment,
} from "../../domain/models/circuit-section-reorder-project-command.model.js";
import type { CircuitTreeCircuitDto } from "../types.js";
export function buildCircuitSectionReorderAssignments(
circuits: CircuitTreeCircuitDto[],
orderedCircuitIds: string[]
): CircuitSectionReorderAssignment[] {
if (
circuits.length !== orderedCircuitIds.length ||
new Set(orderedCircuitIds).size !== orderedCircuitIds.length
) {
throw new Error(
"Die Reihenfolge muss jeden Stromkreis des Bereichs genau einmal enthalten."
);
}
const circuitsById = new Map(
circuits.map((circuit) => [circuit.id, circuit])
);
const assignments = orderedCircuitIds.map((circuitId, index) => {
const circuit = circuitsById.get(circuitId);
if (!circuit) {
throw new Error(
"Die Reihenfolge enthält einen ungültigen Stromkreis."
);
}
return {
circuitId,
expectedSortOrder: circuit.sortOrder,
targetSortOrder: (index + 1) * 10,
};
});
if (
assignments.every(
(assignment) =>
assignment.expectedSortOrder === assignment.targetSortOrder
)
) {
return [];
}
return assignments;
}