Make circuit reordering atomic
This commit is contained in:
@@ -81,6 +81,7 @@ Intent is separated by drag source type:
|
|||||||
- circuit drag (BMK handle):
|
- circuit drag (BMK handle):
|
||||||
- reorder circuits inside same section only
|
- reorder circuits inside same section only
|
||||||
- cross-section reorder is rejected
|
- cross-section reorder is rejected
|
||||||
|
- the complete section order is validated and stored in one SQLite transaction
|
||||||
- bulk device row move:
|
- bulk device row move:
|
||||||
- supported via multi-selection + drag
|
- supported via multi-selection + drag
|
||||||
- multi-circuit move:
|
- multi-circuit move:
|
||||||
@@ -103,6 +104,7 @@ Cross-section device moves show confirmation-required feedback before drop. The
|
|||||||
|
|
||||||
- disabled while filters are active
|
- disabled while filters are active
|
||||||
- undoable via command history
|
- undoable via command history
|
||||||
|
- each complete section order is stored atomically
|
||||||
- until applied, sort remains view-only
|
- until applied, sort remains view-only
|
||||||
|
|
||||||
## Column Configuration
|
## Column Configuration
|
||||||
|
|||||||
@@ -137,6 +137,46 @@ export class CircuitRepository {
|
|||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateSortOrdersSafely(sectionId: string, orderedCircuitIds: string[]) {
|
||||||
|
if (orderedCircuitIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (new Set(orderedCircuitIds).size !== orderedCircuitIds.length) {
|
||||||
|
throw new Error("Stromkreise dürfen in der Reihenfolge nicht mehrfach vorkommen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
db.transaction((tx) => {
|
||||||
|
const sectionCircuits = tx
|
||||||
|
.select({ id: circuits.id })
|
||||||
|
.from(circuits)
|
||||||
|
.where(eq(circuits.sectionId, sectionId))
|
||||||
|
.all();
|
||||||
|
const sectionCircuitIds = new Set(sectionCircuits.map((circuit) => circuit.id));
|
||||||
|
if (
|
||||||
|
sectionCircuits.length !== orderedCircuitIds.length ||
|
||||||
|
orderedCircuitIds.some((circuitId) => !sectionCircuitIds.has(circuitId))
|
||||||
|
) {
|
||||||
|
throw new Error("Die Reihenfolge muss alle Stromkreise des Bereichs enthalten.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < orderedCircuitIds.length; index += 1) {
|
||||||
|
const result = tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({ sortOrder: (index + 1) * 10 })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuits.sectionId, sectionId),
|
||||||
|
eq(circuits.id, orderedCircuitIds[index])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (result.changes !== 1) {
|
||||||
|
throw new Error("Ein Stromkreis wurde vor Abschluss der Sortierung verändert.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async updateEquipmentIdentifiersSafely(
|
async updateEquipmentIdentifiersSafely(
|
||||||
circuitListId: string,
|
circuitListId: string,
|
||||||
updates: Array<{ id: string; equipmentIdentifier: string }>,
|
updates: Array<{ id: string; equipmentIdentifier: string }>,
|
||||||
|
|||||||
@@ -476,32 +476,7 @@ export class CircuitWriteService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let index = 0; index < input.orderedCircuitIds.length; index += 1) {
|
this.circuitRepository.updateSortOrdersSafely(sectionId, input.orderedCircuitIds);
|
||||||
const circuitId = input.orderedCircuitIds[index];
|
|
||||||
const circuit = sectionCircuits.find((entry) => entry.id === circuitId);
|
|
||||||
if (!circuit) {
|
|
||||||
throw new Error("Invalid circuit id.");
|
|
||||||
}
|
|
||||||
await this.circuitRepository.update(circuit.id, {
|
|
||||||
sectionId: circuit.sectionId,
|
|
||||||
equipmentIdentifier: circuit.equipmentIdentifier,
|
|
||||||
displayName: circuit.displayName ?? undefined,
|
|
||||||
sortOrder: (index + 1) * 10,
|
|
||||||
protectionType: circuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: circuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: circuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: circuit.voltage ?? undefined,
|
|
||||||
controlRequirement: circuit.controlRequirement ?? undefined,
|
|
||||||
status: circuit.status ?? undefined,
|
|
||||||
isReserve: Boolean(circuit.isReserve),
|
|
||||||
remark: circuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return this.circuitRepository.listBySection(sectionId);
|
return this.circuitRepository.listBySection(sectionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -580,7 +580,7 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
||||||
const updates: Array<{ id: string; sortOrder: number; equipmentIdentifier: string }> = [];
|
let safeReorder: { sectionId: string; circuitIds: string[] } | undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
@@ -595,18 +595,17 @@ describe("circuit write service rules", () => {
|
|||||||
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-2F5", sortOrder: 30, isReserve: 1 },
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-2F5", sortOrder: 30, isReserve: 1 },
|
||||||
] as never[];
|
] as never[];
|
||||||
},
|
},
|
||||||
async update(id: string, payload: { sortOrder: number; equipmentIdentifier: string }) {
|
updateSortOrdersSafely(sectionId: string, circuitIds: string[]) {
|
||||||
updates.push({ id, sortOrder: payload.sortOrder, equipmentIdentifier: payload.equipmentIdentifier });
|
safeReorder = { sectionId, circuitIds };
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
||||||
assert.deepEqual(updates, [
|
assert.deepEqual(safeReorder, {
|
||||||
{ id: "c3", sortOrder: 10, equipmentIdentifier: "-2F5" },
|
sectionId: "s1",
|
||||||
{ id: "c1", sortOrder: 20, equipmentIdentifier: "-2F7" },
|
circuitIds: ["c3", "c1", "c2"],
|
||||||
{ id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" },
|
});
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
||||||
@@ -698,6 +697,61 @@ describe("circuit write service rules", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("safe circuit reorder uses one synchronous transaction callback", () => {
|
||||||
|
const repository = new CircuitRepository();
|
||||||
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||||
|
|
||||||
|
let callbackReturnedPromise = false;
|
||||||
|
const sortOrders: number[] = [];
|
||||||
|
(db as unknown as { transaction: (cb: (tx: unknown) => unknown) => void }).transaction = (cb) => {
|
||||||
|
const fakeTx = {
|
||||||
|
select() {
|
||||||
|
return {
|
||||||
|
from() {
|
||||||
|
return {
|
||||||
|
where() {
|
||||||
|
return {
|
||||||
|
all() {
|
||||||
|
return [{ id: "c1" }, { id: "c2" }, { id: "c3" }];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
return {
|
||||||
|
set(values: { sortOrder: number }) {
|
||||||
|
sortOrders.push(values.sortOrder);
|
||||||
|
return {
|
||||||
|
where() {
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return { changes: 1 };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const callbackResult = cb(fakeTx);
|
||||||
|
callbackReturnedPromise = Boolean(
|
||||||
|
callbackResult && typeof (callbackResult as Promise<unknown>).then === "function"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
repository.updateSortOrdersSafely("s1", ["c3", "c1", "c2"]);
|
||||||
|
assert.equal(callbackReturnedPromise, false);
|
||||||
|
assert.deepEqual(sortOrders, [10, 20, 30]);
|
||||||
|
} finally {
|
||||||
|
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("bulk device row move uses one synchronous transaction callback", () => {
|
it("bulk device row move uses one synchronous transaction callback", () => {
|
||||||
const repository = new CircuitDeviceRowRepository();
|
const repository = new CircuitDeviceRowRepository();
|
||||||
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||||
|
|||||||
Reference in New Issue
Block a user