Make single row moves atomic
This commit is contained in:
@@ -75,12 +75,12 @@ Intent is separated by drag source type:
|
|||||||
- drop to placeholder -> create new target circuit and move row(s)
|
- drop to placeholder -> create new target circuit and move row(s)
|
||||||
- crossing a section boundary requires explicit confirmation
|
- crossing a section boundary requires explicit confirmation
|
||||||
- phase type, category, linked project device and local row values remain unchanged
|
- phase type, category, linked project device and local row values remain unchanged
|
||||||
|
- target creation, row assignments and reserve-state updates use one SQLite transaction
|
||||||
- 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
|
||||||
- bulk device row move:
|
- bulk device row move:
|
||||||
- supported via multi-selection + drag
|
- supported via multi-selection + drag
|
||||||
- row assignments, optional target creation and reserve-state updates are committed in one SQLite transaction
|
|
||||||
- multi-circuit move:
|
- multi-circuit move:
|
||||||
- supported for same-section selected circuit blocks
|
- supported for same-section selected circuit blocks
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,18 @@ export class CircuitWriteService {
|
|||||||
throw new Error("Invalid target circuit id.");
|
throw new Error("Invalid target circuit id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Placeholder-target move creates a new circuit explicitly; no implicit renumbering of others.
|
let createTargetCircuit:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
|
||||||
|
// Placeholder-target move prepares a new circuit explicitly. Its creation and
|
||||||
|
// the row assignment are committed together without renumbering other circuits.
|
||||||
if (!targetCircuit) {
|
if (!targetCircuit) {
|
||||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||||
throw new Error("Invalid move target.");
|
throw new Error("Invalid move target.");
|
||||||
@@ -326,76 +337,27 @@ export class CircuitWriteService {
|
|||||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
||||||
const nextSortOrder =
|
const nextSortOrder =
|
||||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
const createdId = await this.circuitRepository.create({
|
createTargetCircuit = {
|
||||||
circuitListId: sourceCircuit.circuitListId,
|
circuitListId: sourceCircuit.circuitListId,
|
||||||
sectionId: section.id,
|
sectionId: section.id,
|
||||||
equipmentIdentifier: nextIdentifier,
|
equipmentIdentifier: nextIdentifier,
|
||||||
displayName: "Neuer Stromkreis",
|
displayName: "Neuer Stromkreis",
|
||||||
sortOrder: nextSortOrder,
|
sortOrder: nextSortOrder,
|
||||||
isReserve: false,
|
};
|
||||||
});
|
|
||||||
targetCircuit = await this.circuitRepository.findById(createdId);
|
|
||||||
if (!targetCircuit) {
|
|
||||||
throw new Error("Failed to create target circuit.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
if (targetCircuit && targetCircuit.circuitListId !== sourceCircuit.circuitListId) {
|
||||||
throw new Error("Target circuit does not belong to same circuit list.");
|
throw new Error("Target circuit does not belong to same circuit list.");
|
||||||
}
|
}
|
||||||
if (targetCircuit.id === sourceCircuit.id) {
|
if (targetCircuit?.id === sourceCircuit.id) {
|
||||||
return this.deviceRowRepository.findById(rowId);
|
return this.deviceRowRepository.findById(rowId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id);
|
this.deviceRowRepository.moveRowsTransactional({
|
||||||
await this.deviceRowRepository.moveToCircuit(rowId, targetCircuit.id, (targetCount + 1) * 10);
|
rows: [{ id: row.id, expectedCircuitId: row.circuitId }],
|
||||||
|
targetCircuitId: targetCircuit?.id,
|
||||||
const sourceRemaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
createTargetCircuit,
|
||||||
// Source circuit becomes reserve when all rows are moved away.
|
|
||||||
if (sourceRemaining === 0) {
|
|
||||||
await this.circuitRepository.update(sourceCircuit.id, {
|
|
||||||
sectionId: sourceCircuit.sectionId,
|
|
||||||
equipmentIdentifier: sourceCircuit.equipmentIdentifier,
|
|
||||||
displayName: sourceCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: sourceCircuit.sortOrder,
|
|
||||||
protectionType: sourceCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: sourceCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: sourceCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: sourceCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: sourceCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: sourceCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: sourceCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: sourceCircuit.controlRequirement ?? undefined,
|
|
||||||
status: sourceCircuit.status ?? undefined,
|
|
||||||
isReserve: true,
|
|
||||||
remark: sourceCircuit.remark ?? undefined,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Target circuit is no longer reserve once it receives moved rows.
|
|
||||||
if (Boolean(targetCircuit.isReserve)) {
|
|
||||||
await this.circuitRepository.update(targetCircuit.id, {
|
|
||||||
sectionId: targetCircuit.sectionId,
|
|
||||||
equipmentIdentifier: targetCircuit.equipmentIdentifier,
|
|
||||||
displayName: targetCircuit.displayName ?? undefined,
|
|
||||||
sortOrder: targetCircuit.sortOrder,
|
|
||||||
protectionType: targetCircuit.protectionType ?? undefined,
|
|
||||||
protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined,
|
|
||||||
protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined,
|
|
||||||
cableType: targetCircuit.cableType ?? undefined,
|
|
||||||
cableCrossSection: targetCircuit.cableCrossSection ?? undefined,
|
|
||||||
cableLength: targetCircuit.cableLength ?? undefined,
|
|
||||||
rcdAssignment: targetCircuit.rcdAssignment ?? undefined,
|
|
||||||
terminalDesignation: targetCircuit.terminalDesignation ?? undefined,
|
|
||||||
voltage: targetCircuit.voltage ?? undefined,
|
|
||||||
controlRequirement: targetCircuit.controlRequirement ?? undefined,
|
|
||||||
status: targetCircuit.status ?? undefined,
|
|
||||||
isReserve: false,
|
|
||||||
remark: targetCircuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.deviceRowRepository.findById(rowId);
|
return this.deviceRowRepository.findById(rowId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -299,24 +299,24 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
it("moving a device row to another circuit preserves row and toggles reserve flags", async () => {
|
||||||
const updatedReserve: Array<{ id: string; isReserve: boolean }> = [];
|
let transactionalMove:
|
||||||
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
| {
|
||||||
|
rows: Array<{ id: string; expectedCircuitId: string }>;
|
||||||
|
targetCircuitId?: string;
|
||||||
|
createTargetCircuit?: unknown;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: typeof transactionalMove) {
|
||||||
if (circuitId === "c2") {
|
transactionalMove = input;
|
||||||
return 2;
|
return {
|
||||||
}
|
movedRowIds: input!.rows.map((row) => row.id),
|
||||||
if (circuitId === "c1") {
|
targetCircuitId: input!.targetCircuitId!,
|
||||||
return 0;
|
};
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
async moveToCircuit(rowId: string, targetCircuitId: string, sortOrder: number) {
|
|
||||||
movedCalls.push({ rowId, targetCircuitId, sortOrder });
|
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -340,35 +340,39 @@ describe("circuit write service rules", () => {
|
|||||||
isReserve: 1,
|
isReserve: 1,
|
||||||
} as never;
|
} as never;
|
||||||
},
|
},
|
||||||
async update(id: string, payload: { isReserve: boolean }) {
|
|
||||||
updatedReserve.push({ id, isReserve: payload.isReserve });
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
await service.moveDeviceRow("r1", { targetCircuitId: "c2" });
|
||||||
assert.deepEqual(movedCalls, [{ rowId: "r1", targetCircuitId: "c2", sortOrder: 30 }]);
|
assert.deepEqual(transactionalMove, {
|
||||||
assert.deepEqual(updatedReserve, [
|
rows: [{ id: "r1", expectedCircuitId: "c1" }],
|
||||||
{ id: "c1", isReserve: true },
|
targetCircuitId: "c2",
|
||||||
{ id: "c2", isReserve: false },
|
createTargetCircuit: undefined,
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
it("moving a device row to placeholder creates a new circuit in target section", async () => {
|
||||||
let createdCircuitPayload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean } | null = null;
|
let preparedTarget:
|
||||||
|
| {
|
||||||
|
circuitListId: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
deviceRowRepository: {
|
deviceRowRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
return { id: "r1", circuitId: "c1" } as never;
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
},
|
},
|
||||||
async countByCircuit(circuitId: string) {
|
moveRowsTransactional(input: { rows: Array<{ id: string }>; createTargetCircuit?: typeof preparedTarget }) {
|
||||||
if (circuitId === "c-new") {
|
preparedTarget = input.createTargetCircuit;
|
||||||
return 0;
|
return {
|
||||||
}
|
movedRowIds: input.rows.map((row) => row.id),
|
||||||
return 1;
|
targetCircuitId: "c-new",
|
||||||
},
|
createdCircuitId: "c-new",
|
||||||
async moveToCircuit() {
|
};
|
||||||
return;
|
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
@@ -383,28 +387,11 @@ describe("circuit write service rules", () => {
|
|||||||
isReserve: 0,
|
isReserve: 0,
|
||||||
} as never;
|
} as never;
|
||||||
}
|
}
|
||||||
if (circuitId === "c-new") {
|
|
||||||
return {
|
|
||||||
id: "c-new",
|
|
||||||
sectionId: "s2",
|
|
||||||
circuitListId: "l1",
|
|
||||||
equipmentIdentifier: "-2F8",
|
|
||||||
sortOrder: 50,
|
|
||||||
isReserve: 0,
|
|
||||||
} as never;
|
|
||||||
}
|
|
||||||
return null as never;
|
return null as never;
|
||||||
},
|
},
|
||||||
async listBySection() {
|
async listBySection() {
|
||||||
return [{ sortOrder: 40 }] as never[];
|
return [{ sortOrder: 40 }] as never[];
|
||||||
},
|
},
|
||||||
async create(payload: { sectionId: string; equipmentIdentifier: string; isReserve?: boolean }) {
|
|
||||||
createdCircuitPayload = payload;
|
|
||||||
return "c-new";
|
|
||||||
},
|
|
||||||
async update() {
|
|
||||||
return;
|
|
||||||
},
|
|
||||||
} as never,
|
} as never,
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
@@ -419,9 +406,44 @@ describe("circuit write service rules", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
await service.moveDeviceRow("r1", { targetSectionId: "s2", createNewCircuit: true });
|
||||||
assert.equal(createdCircuitPayload?.sectionId, "s2");
|
assert.deepEqual(preparedTarget, {
|
||||||
assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8");
|
circuitListId: "l1",
|
||||||
assert.equal(createdCircuitPayload?.isReserve, false);
|
sectionId: "s2",
|
||||||
|
equipmentIdentifier: "-2F8",
|
||||||
|
displayName: "Neuer Stromkreis",
|
||||||
|
sortOrder: 50,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("moving a device row to its current circuit remains a no-op", async () => {
|
||||||
|
let transactionalMoveCount = 0;
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
deviceRowRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "r1", circuitId: "c1" } as never;
|
||||||
|
},
|
||||||
|
moveRowsTransactional() {
|
||||||
|
transactionalMoveCount += 1;
|
||||||
|
throw new Error("same-circuit move must not write");
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async findById() {
|
||||||
|
return {
|
||||||
|
id: "c1",
|
||||||
|
sectionId: "s1",
|
||||||
|
circuitListId: "l1",
|
||||||
|
equipmentIdentifier: "-1F1",
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: 0,
|
||||||
|
} as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.moveDeviceRow("r1", { targetCircuitId: "c1" });
|
||||||
|
assert.equal(transactionalMoveCount, 0);
|
||||||
|
assert.equal(result?.id, "r1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
it("moving multiple device rows to a circuit preserves input order and toggles reserve", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user