Undo Redo working
This commit is contained in:
@@ -46,6 +46,23 @@ body {
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editor-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar button {
|
||||||
|
border: 1px solid #c4cddc;
|
||||||
|
background: #fff;
|
||||||
|
padding: 0.28rem 0.6rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-toolbar button:disabled {
|
||||||
|
opacity: 0.45;
|
||||||
|
}
|
||||||
|
|
||||||
.tree-grid-wrap {
|
.tree-grid-wrap {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: 1px solid #d9dee8;
|
border: 1px solid #d9dee8;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import { and, asc, eq, ne } from "drizzle-orm";
|
import { and, asc, eq, inArray, ne } from "drizzle-orm";
|
||||||
import { db } from "../client.js";
|
import { db } from "../client.js";
|
||||||
import { circuits } from "../schema/circuits.js";
|
import { circuits } from "../schema/circuits.js";
|
||||||
|
|
||||||
@@ -132,4 +132,44 @@ export class CircuitRepository {
|
|||||||
.where(eq(circuits.sectionId, sectionId))
|
.where(eq(circuits.sectionId, sectionId))
|
||||||
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
.orderBy(asc(circuits.sortOrder), asc(circuits.equipmentIdentifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateEquipmentIdentifiersSafely(
|
||||||
|
circuitListId: string,
|
||||||
|
updates: Array<{ id: string; equipmentIdentifier: string }>,
|
||||||
|
tempNamespace: string
|
||||||
|
) {
|
||||||
|
if (updates.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
db.transaction((tx) => {
|
||||||
|
const ids = updates.map((entry) => entry.id);
|
||||||
|
const existing = tx
|
||||||
|
.select({ id: circuits.id })
|
||||||
|
.from(circuits)
|
||||||
|
.where(and(eq(circuits.circuitListId, circuitListId), inArray(circuits.id, ids)))
|
||||||
|
.all();
|
||||||
|
if (existing.length !== ids.length) {
|
||||||
|
throw new Error("One or more circuit ids are invalid for circuit list.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const stamp = Date.now();
|
||||||
|
for (let index = 0; index < updates.length; index += 1) {
|
||||||
|
const entry = updates[index];
|
||||||
|
const tempIdentifier = `__tmp_renumber_${tempNamespace}_${stamp}_${index}`;
|
||||||
|
tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({ equipmentIdentifier: tempIdentifier })
|
||||||
|
.where(and(eq(circuits.circuitListId, circuitListId), eq(circuits.id, entry.id)))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of updates) {
|
||||||
|
tx
|
||||||
|
.update(circuits)
|
||||||
|
.set({ equipmentIdentifier: entry.equipmentIdentifier })
|
||||||
|
.where(and(eq(circuits.circuitListId, circuitListId), eq(circuits.id, entry.id)))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
CreateCircuitInput,
|
CreateCircuitInput,
|
||||||
MoveCircuitDeviceRowInput,
|
MoveCircuitDeviceRowInput,
|
||||||
ReorderSectionCircuitsInput,
|
ReorderSectionCircuitsInput,
|
||||||
|
UpdateSectionEquipmentIdentifiersInput,
|
||||||
UpdateCircuitDeviceRowInput,
|
UpdateCircuitDeviceRowInput,
|
||||||
UpdateCircuitInput,
|
UpdateCircuitInput,
|
||||||
} from "../../shared/validation/circuit.schemas.js";
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
@@ -379,6 +380,7 @@ export class CircuitWriteService {
|
|||||||
);
|
);
|
||||||
const otherIdentifiers = new Set(otherCircuits.map((circuit) => circuit.equipmentIdentifier));
|
const otherIdentifiers = new Set(otherCircuits.map((circuit) => circuit.equipmentIdentifier));
|
||||||
|
|
||||||
|
const finalAssignments: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||||
let index = 1;
|
let index = 1;
|
||||||
for (const circuit of sectionCircuits) {
|
for (const circuit of sectionCircuits) {
|
||||||
let candidate = `${section.prefix}${index}`;
|
let candidate = `${section.prefix}${index}`;
|
||||||
@@ -386,30 +388,45 @@ export class CircuitWriteService {
|
|||||||
index += 1;
|
index += 1;
|
||||||
candidate = `${section.prefix}${index}`;
|
candidate = `${section.prefix}${index}`;
|
||||||
}
|
}
|
||||||
await this.circuitRepository.update(circuit.id, {
|
finalAssignments.push({ id: circuit.id, equipmentIdentifier: candidate });
|
||||||
sectionId: circuit.sectionId,
|
|
||||||
equipmentIdentifier: candidate,
|
|
||||||
displayName: circuit.displayName ?? undefined,
|
|
||||||
sortOrder: circuit.sortOrder,
|
|
||||||
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,
|
|
||||||
status: circuit.status ?? undefined,
|
|
||||||
isReserve: Boolean(circuit.isReserve),
|
|
||||||
remark: circuit.remark ?? undefined,
|
|
||||||
});
|
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
||||||
|
section.circuitListId,
|
||||||
|
finalAssignments,
|
||||||
|
sectionId
|
||||||
|
);
|
||||||
|
|
||||||
return this.circuitRepository.listBySection(sectionId);
|
return this.circuitRepository.listBySection(sectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateSectionEquipmentIdentifiers(
|
||||||
|
sectionId: string,
|
||||||
|
input: UpdateSectionEquipmentIdentifiersInput
|
||||||
|
) {
|
||||||
|
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||||
|
if (!section) {
|
||||||
|
throw new Error("Invalid section id.");
|
||||||
|
}
|
||||||
|
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
|
||||||
|
const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id));
|
||||||
|
if (input.identifiers.length !== sectionCircuits.length) {
|
||||||
|
throw new Error("identifiers must include all circuits in the section.");
|
||||||
|
}
|
||||||
|
for (const entry of input.identifiers) {
|
||||||
|
if (!sectionIds.has(entry.circuitId)) {
|
||||||
|
throw new Error("Circuit id does not belong to section.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.circuitRepository.updateEquipmentIdentifiersSafely(
|
||||||
|
section.circuitListId,
|
||||||
|
input.identifiers.map((entry) => ({ id: entry.circuitId, equipmentIdentifier: entry.equipmentIdentifier })),
|
||||||
|
sectionId
|
||||||
|
);
|
||||||
|
return this.circuitRepository.listBySection(sectionId);
|
||||||
|
}
|
||||||
|
|
||||||
async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) {
|
async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) {
|
||||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||||
if (!section) {
|
if (!section) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
moveCircuitDeviceRowById,
|
moveCircuitDeviceRowById,
|
||||||
reorderSectionCircuits,
|
reorderSectionCircuits,
|
||||||
renumberCircuitSection,
|
renumberCircuitSection,
|
||||||
|
updateSectionEquipmentIdentifiers,
|
||||||
updateCircuitById,
|
updateCircuitById,
|
||||||
updateCircuitDeviceRowById,
|
updateCircuitDeviceRowById,
|
||||||
} from "../utils/api";
|
} from "../utils/api";
|
||||||
@@ -76,6 +77,33 @@ interface VisibleGridRow {
|
|||||||
cells: VisibleGridCell[];
|
cells: VisibleGridCell[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HistoryCommand {
|
||||||
|
label: string;
|
||||||
|
redo: () => Promise<SelectionIntent | null | void>;
|
||||||
|
undo: () => Promise<SelectionIntent | null | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CircuitSnapshot {
|
||||||
|
id: string;
|
||||||
|
sectionId: string;
|
||||||
|
equipmentIdentifier: string;
|
||||||
|
displayName?: string;
|
||||||
|
sortOrder: number;
|
||||||
|
protectionType?: string;
|
||||||
|
protectionRatedCurrent?: number;
|
||||||
|
protectionCharacteristic?: string;
|
||||||
|
cableType?: string;
|
||||||
|
cableCrossSection?: string;
|
||||||
|
cableLength?: number;
|
||||||
|
rcdAssignment?: string;
|
||||||
|
terminalDesignation?: string;
|
||||||
|
voltage?: number;
|
||||||
|
status?: string;
|
||||||
|
isReserve: boolean;
|
||||||
|
remark?: string;
|
||||||
|
deviceRows: CircuitTreeDeviceRowDto[];
|
||||||
|
}
|
||||||
|
|
||||||
type ProjectDeviceDropIntent =
|
type ProjectDeviceDropIntent =
|
||||||
| { kind: "new-circuit"; sectionId: string }
|
| { kind: "new-circuit"; sectionId: string }
|
||||||
| { kind: "add-to-circuit"; circuitId: string; sectionId: string };
|
| { kind: "add-to-circuit"; circuitId: string; sectionId: string };
|
||||||
@@ -282,6 +310,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | null>(null);
|
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | null>(null);
|
||||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||||
|
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
||||||
|
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
||||||
|
const [historyBusy, setHistoryBusy] = useState(false);
|
||||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||||
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
@@ -511,6 +542,69 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return editableCells[0] ?? null;
|
return editableCells[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reloadWithIntent(intent?: SelectionIntent | null) {
|
||||||
|
if (intent) {
|
||||||
|
pendingSelectionAfterReload.current = intent;
|
||||||
|
}
|
||||||
|
await loadTree({ showLoading: false });
|
||||||
|
if (!intent) {
|
||||||
|
requestAnimationFrame(() => containerRef.current?.focus());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(command: HistoryCommand) {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setIsSaving(true);
|
||||||
|
const intent = await command.redo();
|
||||||
|
await reloadWithIntent(intent ?? null);
|
||||||
|
setUndoStack((current) => [...current, command]);
|
||||||
|
setRedoStack([]);
|
||||||
|
} catch (err) {
|
||||||
|
setError(normalizeUiError(err));
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyHistory(command: HistoryCommand, mode: "undo" | "redo") {
|
||||||
|
try {
|
||||||
|
setError(null);
|
||||||
|
setHistoryBusy(true);
|
||||||
|
setIsSaving(true);
|
||||||
|
const intent = mode === "undo" ? await command.undo() : await command.redo();
|
||||||
|
await reloadWithIntent(intent ?? null);
|
||||||
|
if (mode === "undo") {
|
||||||
|
setUndoStack((current) => current.slice(0, -1));
|
||||||
|
setRedoStack((current) => [...current, command]);
|
||||||
|
} else {
|
||||||
|
setRedoStack((current) => current.slice(0, -1));
|
||||||
|
setUndoStack((current) => [...current, command]);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError(normalizeUiError(err));
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
setHistoryBusy(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUndo() {
|
||||||
|
if (historyBusy || isSaving || undoStack.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const command = undoStack[undoStack.length - 1];
|
||||||
|
await applyHistory(command, "undo");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRedo() {
|
||||||
|
if (historyBusy || isSaving || redoStack.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const command = redoStack[redoStack.length - 1];
|
||||||
|
await applyHistory(command, "redo");
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const intent = pendingSelectionAfterReload.current;
|
const intent = pendingSelectionAfterReload.current;
|
||||||
if (!intent || !data) {
|
if (!intent || !data) {
|
||||||
@@ -693,6 +787,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
return { rowKey: `reserveCircuit:${createdCircuit.id}`, cellKey: key };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCurrentDraftForCell(row: VisibleGridRow, cellKey: CellKey, kind: CellKind) {
|
||||||
|
if (kind === "deviceField" && row.device) {
|
||||||
|
return String(getDeviceValue(row.device, cellKey) ?? "");
|
||||||
|
}
|
||||||
|
if (kind === "circuitField" && row.circuit) {
|
||||||
|
return String(getCircuitValue(row.circuit, cellKey) ?? "");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
async function commitEdit(direction: SaveDirection = "stay") {
|
async function commitEdit(direction: SaveDirection = "stay") {
|
||||||
if (!editingCell) {
|
if (!editingCell) {
|
||||||
return;
|
return;
|
||||||
@@ -704,20 +808,93 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||||
setIsSaving(true);
|
const nextSelectionIntent = () => {
|
||||||
setError(null);
|
let selected = targetSelection;
|
||||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
if (direction !== "stay") {
|
||||||
|
const idx = editableCells.findIndex(
|
||||||
|
(entry) => entry.rowKey === selected.rowKey && entry.cellKey === selected.cellKey
|
||||||
|
);
|
||||||
|
if (idx >= 0) {
|
||||||
|
const targetIdx =
|
||||||
|
direction === "next" ? Math.min(editableCells.length - 1, idx + 1) : Math.max(0, idx - 1);
|
||||||
|
selected = editableCells[targetIdx];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return buildSelectionIntent(selected);
|
||||||
|
};
|
||||||
|
|
||||||
if (row.rowType === "placeholder") {
|
if (row.rowType === "placeholder") {
|
||||||
targetSelection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft);
|
let createdCircuitId: string | null = null;
|
||||||
} else if (cell.kind === "circuitField" && row.circuit) {
|
const command: HistoryCommand = {
|
||||||
await patchCircuit(row.circuit.id, editingCell.cellKey, editingCell.draft);
|
label: "Create from placeholder",
|
||||||
} else if (cell.kind === "deviceField") {
|
redo: async () => {
|
||||||
if (row.device) {
|
const selection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft);
|
||||||
await patchDeviceRow(row.device.id, editingCell.cellKey, editingCell.draft);
|
targetSelection = selection;
|
||||||
} else if (row.rowType === "reserveCircuit" && row.circuit) {
|
const createdRow = selection.rowKey.startsWith("circuitCompact:")
|
||||||
const created = (await createCircuitDeviceRow(row.circuit.id, {
|
? selection.rowKey.replace("circuitCompact:", "")
|
||||||
|
: selection.rowKey.replace("reserveCircuit:", "");
|
||||||
|
createdCircuitId = createdRow;
|
||||||
|
return nextSelectionIntent();
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (!createdCircuitId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
await deleteCircuitById(createdCircuitId);
|
||||||
|
return buildSelectionIntent({ rowKey: `placeholder:${row.sectionId}`, cellKey: editingCell.cellKey });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setEditingCell(null);
|
||||||
|
await runCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.kind === "circuitField" && row.circuit) {
|
||||||
|
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
||||||
|
const next = editingCell.draft;
|
||||||
|
const command: HistoryCommand = {
|
||||||
|
label: "Edit circuit field",
|
||||||
|
redo: async () => {
|
||||||
|
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
||||||
|
return nextSelectionIntent();
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await patchCircuit(row.circuit!.id, editingCell.cellKey, previous);
|
||||||
|
return buildSelectionIntent({ rowKey: row.rowKey, cellKey: editingCell.cellKey });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setEditingCell(null);
|
||||||
|
await runCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.kind === "deviceField" && row.device) {
|
||||||
|
const previous = getCurrentDraftForCell(row, editingCell.cellKey, cell.kind);
|
||||||
|
const next = editingCell.draft;
|
||||||
|
const command: HistoryCommand = {
|
||||||
|
label: "Edit device field",
|
||||||
|
redo: async () => {
|
||||||
|
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
||||||
|
return nextSelectionIntent();
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await patchDeviceRow(row.device!.id, editingCell.cellKey, previous);
|
||||||
|
return buildSelectionIntent({ rowKey: `device:${row.device!.id}`, cellKey: editingCell.cellKey });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
setEditingCell(null);
|
||||||
|
await runCommand(command);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cell.kind === "deviceField" && row.rowType === "reserveCircuit" && row.circuit) {
|
||||||
|
const next = editingCell.draft;
|
||||||
|
let createdRowId: string | null = null;
|
||||||
|
const command: HistoryCommand = {
|
||||||
|
label: "Create reserve device row",
|
||||||
|
redo: async () => {
|
||||||
|
const created = (await createCircuitDeviceRow(row.circuit!.id, {
|
||||||
name: "Reserve load",
|
name: "Reserve load",
|
||||||
displayName: "Reserve load",
|
displayName: "Reserve load",
|
||||||
phaseType: "single_phase",
|
phaseType: "single_phase",
|
||||||
@@ -726,37 +903,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
simultaneityFactor: 1,
|
simultaneityFactor: 1,
|
||||||
cosPhi: 1,
|
cosPhi: 1,
|
||||||
})) as { id: string };
|
})) as { id: string };
|
||||||
await patchDeviceRow(created.id, editingCell.cellKey, editingCell.draft);
|
createdRowId = created.id;
|
||||||
targetSelection = { rowKey: `circuitCompact:${row.circuit.id}`, cellKey: editingCell.cellKey };
|
await patchDeviceRow(created.id, editingCell.cellKey, next);
|
||||||
}
|
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||||
}
|
return nextSelectionIntent();
|
||||||
|
},
|
||||||
if (direction !== "stay") {
|
undo: async () => {
|
||||||
const idx = editableCells.findIndex(
|
if (!createdRowId) {
|
||||||
(entry) => entry.rowKey === targetSelection.rowKey && entry.cellKey === targetSelection.cellKey
|
return null;
|
||||||
);
|
}
|
||||||
if (idx >= 0) {
|
await deleteCircuitDeviceRowById(createdRowId);
|
||||||
const targetIdx =
|
return buildSelectionIntent({ rowKey: `reserveCircuit:${row.circuit!.id}`, cellKey: editingCell.cellKey });
|
||||||
direction === "next"
|
},
|
||||||
? Math.min(editableCells.length - 1, idx + 1)
|
};
|
||||||
: Math.max(0, idx - 1);
|
|
||||||
targetSelection = editableCells[targetIdx];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const intent = buildSelectionIntent(targetSelection);
|
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
if (intent) {
|
await runCommand(command);
|
||||||
pendingSelectionAfterReload.current = intent;
|
|
||||||
}
|
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
if (!intent) {
|
|
||||||
requestAnimationFrame(() => containerRef.current?.focus());
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,56 +931,86 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddReserveCircuit(sectionId: string) {
|
async function handleAddReserveCircuit(sectionId: string) {
|
||||||
try {
|
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||||
setError(null);
|
if (!section) {
|
||||||
setIsSaving(true);
|
return;
|
||||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
|
||||||
if (!section) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const next = await getNextCircuitIdentifier(sectionId);
|
|
||||||
const sortOrder =
|
|
||||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
|
||||||
await createCircuit(projectId, circuitListId, {
|
|
||||||
sectionId,
|
|
||||||
equipmentIdentifier: next.nextIdentifier,
|
|
||||||
displayName: "Reserve",
|
|
||||||
sortOrder,
|
|
||||||
isReserve: true,
|
|
||||||
});
|
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
setActiveSectionId(sectionId);
|
|
||||||
} catch (err) {
|
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
|
let createdCircuitId: string | null = null;
|
||||||
|
await runCommand({
|
||||||
|
label: "Add circuit",
|
||||||
|
redo: async () => {
|
||||||
|
const next = await getNextCircuitIdentifier(sectionId);
|
||||||
|
const sortOrder =
|
||||||
|
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||||
|
const created = (await createCircuit(projectId, circuitListId, {
|
||||||
|
sectionId,
|
||||||
|
equipmentIdentifier: next.nextIdentifier,
|
||||||
|
displayName: "Reserve",
|
||||||
|
sortOrder,
|
||||||
|
isReserve: true,
|
||||||
|
})) as CircuitTreeCircuitDto;
|
||||||
|
createdCircuitId = created.id;
|
||||||
|
setActiveSectionId(sectionId);
|
||||||
|
return {
|
||||||
|
rowKey: `reserveCircuit:${created.id}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "reserveCircuit",
|
||||||
|
sectionId,
|
||||||
|
circuitId: created.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdCircuitId) {
|
||||||
|
await deleteCircuitById(createdCircuitId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rowKey: `placeholder:${sectionId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddManualDevice(circuit: CircuitTreeCircuitDto, sectionId: string) {
|
async function handleAddManualDevice(circuit: CircuitTreeCircuitDto, sectionId: string) {
|
||||||
try {
|
let createdRowId: string | null = null;
|
||||||
setError(null);
|
await runCommand({
|
||||||
setIsSaving(true);
|
label: "Add manual device",
|
||||||
const created = (await createCircuitDeviceRow(circuit.id, {
|
redo: async () => {
|
||||||
name: "Manual device",
|
const created = (await createCircuitDeviceRow(circuit.id, {
|
||||||
displayName: "Manual device",
|
name: "Manual device",
|
||||||
phaseType: "single_phase",
|
displayName: "Manual device",
|
||||||
quantity: 1,
|
phaseType: "single_phase",
|
||||||
powerPerUnit: 0,
|
quantity: 1,
|
||||||
simultaneityFactor: 1,
|
powerPerUnit: 0,
|
||||||
cosPhi: 1,
|
simultaneityFactor: 1,
|
||||||
})) as { id: string };
|
cosPhi: 1,
|
||||||
await loadTree({ showLoading: false });
|
})) as { id: string };
|
||||||
setActiveSectionId(sectionId);
|
createdRowId = created.id;
|
||||||
setPendingFocus({
|
setActiveSectionId(sectionId);
|
||||||
rowKey: circuit.deviceRows.length === 0 ? `circuitCompact:${circuit.id}` : `device:${created.id}`,
|
return {
|
||||||
cellKey: "displayName",
|
rowKey: `device:${created.id}`,
|
||||||
});
|
cellKey: "displayName",
|
||||||
} catch (err) {
|
rowType: "deviceRow",
|
||||||
setError(normalizeUiError(err));
|
sectionId,
|
||||||
} finally {
|
circuitId: circuit.id,
|
||||||
setIsSaving(false);
|
deviceId: created.id,
|
||||||
}
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdRowId) {
|
||||||
|
await deleteCircuitDeviceRowById(createdRowId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rowKey: `reserveCircuit:${circuit.id}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "reserveCircuit",
|
||||||
|
sectionId,
|
||||||
|
circuitId: circuit.id,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePhaseType(device: ProjectDeviceDto): string {
|
function resolvePhaseType(device: ProjectDeviceDto): string {
|
||||||
@@ -846,15 +1037,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError("Please select a target section.");
|
setError("Please select a target section.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
let createdCircuitId: string | null = null;
|
||||||
setError(null);
|
let createdRowId: string | null = null;
|
||||||
setIsSaving(true);
|
await runCommand({
|
||||||
await insertProjectDeviceAsNewCircuit(device, targetSectionId);
|
label: "Add project device as new circuit",
|
||||||
} catch (err) {
|
redo: async () => {
|
||||||
setError(normalizeUiError(err));
|
const created = await insertProjectDeviceAsNewCircuit(device, targetSectionId);
|
||||||
} finally {
|
createdCircuitId = created.circuitId;
|
||||||
setIsSaving(false);
|
createdRowId = created.rowId;
|
||||||
}
|
return {
|
||||||
|
rowKey: `device:${created.rowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: targetSectionId,
|
||||||
|
circuitId: created.circuitId,
|
||||||
|
deviceId: created.rowId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdCircuitId) {
|
||||||
|
await deleteCircuitById(createdCircuitId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rowKey: `placeholder:${targetSectionId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "placeholder",
|
||||||
|
sectionId: targetSectionId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAddProjectDeviceToCircuit() {
|
async function handleAddProjectDeviceToCircuit() {
|
||||||
@@ -875,15 +1086,34 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
let createdRowId: string | null = null;
|
||||||
setError(null);
|
await runCommand({
|
||||||
setIsSaving(true);
|
label: "Add project device to circuit",
|
||||||
await insertProjectDeviceToCircuit(device, circuitId);
|
redo: async () => {
|
||||||
} catch (err) {
|
const created = await insertProjectDeviceToCircuit(device, circuitId);
|
||||||
setError(normalizeUiError(err));
|
createdRowId = created.rowId;
|
||||||
} finally {
|
return {
|
||||||
setIsSaving(false);
|
rowKey: `device:${created.rowId}`,
|
||||||
}
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: findCircuitSectionId(circuitId) ?? "",
|
||||||
|
circuitId,
|
||||||
|
deviceId: created.rowId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdRowId) {
|
||||||
|
await deleteCircuitDeviceRowById(createdRowId);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
rowKey: `circuitSummary:${circuitId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "circuitSummary",
|
||||||
|
sectionId: findCircuitSectionId(circuitId) ?? "",
|
||||||
|
circuitId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertProjectDeviceAsNewCircuit(device: ProjectDeviceDto, sectionId: string) {
|
async function insertProjectDeviceAsNewCircuit(device: ProjectDeviceDto, sectionId: string) {
|
||||||
@@ -914,10 +1144,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
category: device.category ?? undefined,
|
category: device.category ?? undefined,
|
||||||
})) as { id: string };
|
})) as { id: string };
|
||||||
|
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
setActiveSectionId(sectionId);
|
setActiveSectionId(sectionId);
|
||||||
setTargetCircuitId(createdCircuit.id);
|
setTargetCircuitId(createdCircuit.id);
|
||||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
return { circuitId: createdCircuit.id, rowId: createdRow.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
||||||
@@ -932,8 +1161,84 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
cosPhi: device.powerFactor ?? undefined,
|
cosPhi: device.powerFactor ?? undefined,
|
||||||
category: device.category ?? undefined,
|
category: device.category ?? undefined,
|
||||||
})) as { id: string };
|
})) as { id: string };
|
||||||
await loadTree({ showLoading: false });
|
return { rowId: createdRow.id };
|
||||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
}
|
||||||
|
|
||||||
|
function getCircuitSnapshot(circuitId: string): CircuitSnapshot | null {
|
||||||
|
for (const section of data?.sections ?? []) {
|
||||||
|
const found = section.circuits.find((entry) => entry.id === circuitId);
|
||||||
|
if (!found) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: found.id,
|
||||||
|
sectionId: found.sectionId,
|
||||||
|
equipmentIdentifier: found.equipmentIdentifier,
|
||||||
|
displayName: found.displayName ?? undefined,
|
||||||
|
sortOrder: found.sortOrder,
|
||||||
|
protectionType: found.protectionType ?? undefined,
|
||||||
|
protectionRatedCurrent: found.protectionRatedCurrent ?? undefined,
|
||||||
|
protectionCharacteristic: found.protectionCharacteristic ?? undefined,
|
||||||
|
cableType: found.cableType ?? undefined,
|
||||||
|
cableCrossSection: found.cableCrossSection ?? undefined,
|
||||||
|
cableLength: found.cableLength ?? undefined,
|
||||||
|
rcdAssignment: found.rcdAssignment ?? undefined,
|
||||||
|
terminalDesignation: found.terminalDesignation ?? undefined,
|
||||||
|
voltage: found.voltage ?? undefined,
|
||||||
|
status: found.status ?? undefined,
|
||||||
|
isReserve: found.isReserve,
|
||||||
|
remark: found.remark ?? undefined,
|
||||||
|
deviceRows: found.deviceRows.map((row) => ({ ...row })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recreateCircuit(snapshot: CircuitSnapshot) {
|
||||||
|
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||||
|
sectionId: snapshot.sectionId,
|
||||||
|
equipmentIdentifier: snapshot.equipmentIdentifier,
|
||||||
|
displayName: snapshot.displayName,
|
||||||
|
sortOrder: snapshot.sortOrder,
|
||||||
|
protectionType: snapshot.protectionType,
|
||||||
|
protectionRatedCurrent: snapshot.protectionRatedCurrent,
|
||||||
|
protectionCharacteristic: snapshot.protectionCharacteristic,
|
||||||
|
cableType: snapshot.cableType,
|
||||||
|
cableCrossSection: snapshot.cableCrossSection,
|
||||||
|
cableLength: snapshot.cableLength,
|
||||||
|
rcdAssignment: snapshot.rcdAssignment,
|
||||||
|
terminalDesignation: snapshot.terminalDesignation,
|
||||||
|
voltage: snapshot.voltage,
|
||||||
|
status: snapshot.status,
|
||||||
|
isReserve: snapshot.isReserve,
|
||||||
|
remark: snapshot.remark,
|
||||||
|
})) as CircuitTreeCircuitDto;
|
||||||
|
|
||||||
|
const createdRowIds: string[] = [];
|
||||||
|
for (const row of snapshot.deviceRows) {
|
||||||
|
const created = (await createCircuitDeviceRow(createdCircuit.id, {
|
||||||
|
linkedProjectDeviceId: row.linkedProjectDeviceId,
|
||||||
|
name: row.name,
|
||||||
|
displayName: row.displayName,
|
||||||
|
phaseType: row.phaseType,
|
||||||
|
connectionKind: row.connectionKind,
|
||||||
|
costGroup: row.costGroup,
|
||||||
|
category: row.category,
|
||||||
|
level: row.level,
|
||||||
|
roomId: row.roomId,
|
||||||
|
roomNumberSnapshot: row.roomNumberSnapshot,
|
||||||
|
roomNameSnapshot: row.roomNameSnapshot,
|
||||||
|
quantity: row.quantity,
|
||||||
|
powerPerUnit: row.powerPerUnit,
|
||||||
|
simultaneityFactor: row.simultaneityFactor,
|
||||||
|
cosPhi: row.cosPhi,
|
||||||
|
remark: row.remark,
|
||||||
|
overriddenFields: row.overriddenFields,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
})) as { id: string };
|
||||||
|
createdRowIds.push(created.id);
|
||||||
|
}
|
||||||
|
return { circuitId: createdCircuit.id, rowIds: createdRowIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
||||||
@@ -1005,19 +1310,53 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError("Invalid project device drop source.");
|
setError("Invalid project device drop source.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
if (intent.kind === "new-circuit") {
|
||||||
setError(null);
|
let createdCircuitId: string | null = null;
|
||||||
setIsSaving(true);
|
await runCommand({
|
||||||
if (intent.kind === "new-circuit") {
|
label: "Drop project device to new circuit",
|
||||||
await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
redo: async () => {
|
||||||
} else {
|
const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
||||||
await insertProjectDeviceToCircuit(device, intent.circuitId);
|
createdCircuitId = created.circuitId;
|
||||||
}
|
return {
|
||||||
} catch (err) {
|
rowKey: `device:${created.rowId}`,
|
||||||
setError(normalizeUiError(err));
|
cellKey: "displayName",
|
||||||
} finally {
|
rowType: "deviceRow",
|
||||||
setIsSaving(false);
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: created.circuitId,
|
||||||
|
deviceId: created.rowId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdCircuitId) {
|
||||||
|
await deleteCircuitById(createdCircuitId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
let createdRowId: string | null = null;
|
||||||
|
await runCommand({
|
||||||
|
label: "Drop project device to circuit",
|
||||||
|
redo: async () => {
|
||||||
|
const created = await insertProjectDeviceToCircuit(device, intent.circuitId);
|
||||||
|
createdRowId = created.rowId;
|
||||||
|
return {
|
||||||
|
rowKey: `device:${created.rowId}`,
|
||||||
|
cellKey: "displayName",
|
||||||
|
rowType: "deviceRow",
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: intent.circuitId,
|
||||||
|
deviceId: created.rowId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
if (createdRowId) {
|
||||||
|
await deleteCircuitDeviceRowById(createdRowId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
||||||
@@ -1037,27 +1376,43 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (intent.kind === "move-to-circuit") {
|
||||||
setError(null);
|
if (intent.circuitId === sourceCircuitId) {
|
||||||
setIsSaving(true);
|
return;
|
||||||
if (intent.kind === "move-to-circuit") {
|
}
|
||||||
if (intent.circuitId === sourceCircuitId) {
|
await runCommand({
|
||||||
return;
|
label: "Move device row",
|
||||||
}
|
redo: async () => {
|
||||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
||||||
} else {
|
return null;
|
||||||
await moveCircuitDeviceRowById(rowId, {
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await moveCircuitDeviceRowById(rowId, { targetCircuitId: sourceCircuitId });
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let createdCircuitId: string | null = null;
|
||||||
|
await runCommand({
|
||||||
|
label: "Move device row to new circuit",
|
||||||
|
redo: async () => {
|
||||||
|
const moved = (await moveCircuitDeviceRowById(rowId, {
|
||||||
targetSectionId: intent.sectionId,
|
targetSectionId: intent.sectionId,
|
||||||
createNewCircuit: true,
|
createNewCircuit: true,
|
||||||
});
|
})) as { circuitId?: string };
|
||||||
}
|
createdCircuitId = moved.circuitId ?? null;
|
||||||
await loadTree({ showLoading: false });
|
return null;
|
||||||
setPendingFocus({ rowKey: `device:${rowId}`, cellKey: "displayName" });
|
},
|
||||||
} catch (err) {
|
undo: async () => {
|
||||||
setError(normalizeUiError(err));
|
await moveCircuitDeviceRowById(rowId, { targetCircuitId: sourceCircuitId });
|
||||||
} finally {
|
if (createdCircuitId) {
|
||||||
setIsSaving(false);
|
await deleteCircuitById(createdCircuitId);
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
||||||
@@ -1074,71 +1429,135 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
setError("Cross-section circuit move is not allowed in this phase.");
|
setError("Cross-section circuit move is not allowed in this phase.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
||||||
setError(null);
|
if (!section) {
|
||||||
setIsSaving(true);
|
setError("Invalid section id.");
|
||||||
await applyCircuitReorder(intent, sourceCircuitId);
|
return;
|
||||||
pendingSelectionAfterReload.current = {
|
|
||||||
rowKey: `circuitSummary:${sourceCircuitId}`,
|
|
||||||
cellKey: "equipmentIdentifier",
|
|
||||||
rowType: "circuitSummary",
|
|
||||||
sectionId: intent.sectionId,
|
|
||||||
circuitId: sourceCircuitId,
|
|
||||||
};
|
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
} catch (err) {
|
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
|
const beforeOrder = section.circuits.map((circuit) => circuit.id);
|
||||||
|
await runCommand({
|
||||||
|
label: "Reorder circuits",
|
||||||
|
redo: async () => {
|
||||||
|
await applyCircuitReorder(intent, sourceCircuitId);
|
||||||
|
return {
|
||||||
|
rowKey: `circuitSummary:${sourceCircuitId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "circuitSummary",
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: sourceCircuitId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await reorderSectionCircuits(intent.sectionId, beforeOrder);
|
||||||
|
return {
|
||||||
|
rowKey: `circuitSummary:${sourceCircuitId}`,
|
||||||
|
cellKey: "equipmentIdentifier",
|
||||||
|
rowType: "circuitSummary",
|
||||||
|
sectionId: intent.sectionId,
|
||||||
|
circuitId: sourceCircuitId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteDevice(rowId: string) {
|
async function handleDeleteDevice(rowId: string) {
|
||||||
if (!confirm("Delete this device row?")) {
|
if (!confirm("Delete this device row?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||||
setError(null);
|
const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null;
|
||||||
setIsSaving(true);
|
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
|
||||||
await deleteCircuitDeviceRowById(rowId);
|
if (!sourceCircuitId || !rowSnapshot) {
|
||||||
await loadTree({ showLoading: false });
|
setError("Invalid device row id.");
|
||||||
} catch (err) {
|
return;
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
|
let recreatedRowId: string | null = null;
|
||||||
|
await runCommand({
|
||||||
|
label: "Delete device row",
|
||||||
|
redo: async () => {
|
||||||
|
await deleteCircuitDeviceRowById(recreatedRowId ?? rowId);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
const created = (await createCircuitDeviceRow(sourceCircuitId, {
|
||||||
|
linkedProjectDeviceId: rowSnapshot.linkedProjectDeviceId,
|
||||||
|
name: rowSnapshot.name,
|
||||||
|
displayName: rowSnapshot.displayName,
|
||||||
|
phaseType: rowSnapshot.phaseType,
|
||||||
|
connectionKind: rowSnapshot.connectionKind,
|
||||||
|
costGroup: rowSnapshot.costGroup,
|
||||||
|
category: rowSnapshot.category,
|
||||||
|
level: rowSnapshot.level,
|
||||||
|
roomId: rowSnapshot.roomId,
|
||||||
|
roomNumberSnapshot: rowSnapshot.roomNumberSnapshot,
|
||||||
|
roomNameSnapshot: rowSnapshot.roomNameSnapshot,
|
||||||
|
quantity: rowSnapshot.quantity,
|
||||||
|
powerPerUnit: rowSnapshot.powerPerUnit,
|
||||||
|
simultaneityFactor: rowSnapshot.simultaneityFactor,
|
||||||
|
cosPhi: rowSnapshot.cosPhi,
|
||||||
|
remark: rowSnapshot.remark,
|
||||||
|
overriddenFields: rowSnapshot.overriddenFields,
|
||||||
|
sortOrder: rowSnapshot.sortOrder,
|
||||||
|
})) as { id: string };
|
||||||
|
recreatedRowId = created.id;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteCircuit(circuitId: string) {
|
async function handleDeleteCircuit(circuitId: string) {
|
||||||
if (!confirm("Delete this circuit and all assigned device rows?")) {
|
if (!confirm("Delete this circuit and all assigned device rows?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const snapshot = getCircuitSnapshot(circuitId);
|
||||||
setError(null);
|
if (!snapshot) {
|
||||||
setIsSaving(true);
|
setError("Invalid circuit id.");
|
||||||
await deleteCircuitById(circuitId);
|
return;
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
} catch (err) {
|
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
|
let recreatedCircuitId: string | null = null;
|
||||||
|
await runCommand({
|
||||||
|
label: "Delete circuit",
|
||||||
|
redo: async () => {
|
||||||
|
await deleteCircuitById(recreatedCircuitId ?? circuitId);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
const recreated = await recreateCircuit(snapshot);
|
||||||
|
recreatedCircuitId = recreated.circuitId;
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRenumberSection(sectionId: string) {
|
async function handleRenumberSection(sectionId: string) {
|
||||||
if (!confirm("Renumber this section? Only circuits in this section will change.")) {
|
if (!confirm("Renumber this section? Only circuits in this section will change.")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||||
setError(null);
|
if (!section) {
|
||||||
setIsSaving(true);
|
return;
|
||||||
await renumberCircuitSection(sectionId);
|
|
||||||
await loadTree({ showLoading: false });
|
|
||||||
} catch (err) {
|
|
||||||
setError(normalizeUiError(err));
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
}
|
||||||
|
const before = section.circuits.map((circuit) => ({
|
||||||
|
id: circuit.id,
|
||||||
|
equipmentIdentifier: circuit.equipmentIdentifier,
|
||||||
|
}));
|
||||||
|
await runCommand({
|
||||||
|
label: "Renumber section",
|
||||||
|
redo: async () => {
|
||||||
|
await renumberCircuitSection(sectionId);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
undo: async () => {
|
||||||
|
await updateSectionEquipmentIdentifiers(
|
||||||
|
sectionId,
|
||||||
|
before.map((entry) => ({
|
||||||
|
circuitId: entry.id,
|
||||||
|
equipmentIdentifier: entry.equipmentIdentifier,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleContainerFocus() {
|
function handleContainerFocus() {
|
||||||
@@ -1162,6 +1581,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (event.ctrlKey && event.key.toLowerCase() === "z") {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.shiftKey) {
|
||||||
|
void handleRedo();
|
||||||
|
} else {
|
||||||
|
void handleUndo();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.ctrlKey && event.key.toLowerCase() === "y") {
|
||||||
|
event.preventDefault();
|
||||||
|
void handleRedo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const isCtrlPlus =
|
const isCtrlPlus =
|
||||||
event.ctrlKey &&
|
event.ctrlKey &&
|
||||||
(event.key === "+" || (event.shiftKey && event.key === "=") || event.code === "NumpadAdd");
|
(event.key === "+" || (event.shiftKey && event.key === "=") || event.code === "NumpadAdd");
|
||||||
@@ -1228,6 +1661,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tree-editor-shell">
|
<div className="tree-editor-shell">
|
||||||
|
<div className="editor-toolbar">
|
||||||
|
<button type="button" onClick={() => void handleUndo()} disabled={undoStack.length === 0 || historyBusy || isSaving}>
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => void handleRedo()} disabled={redoStack.length === 0 || historyBusy || isSaving}>
|
||||||
|
Redo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{isSaving ? <div className="notice info">Saving...</div> : null}
|
{isSaving ? <div className="notice info">Saving...</div> : null}
|
||||||
<div className="tree-editor-layout">
|
<div className="tree-editor-layout">
|
||||||
<aside className="project-device-sidebar">
|
<aside className="project-device-sidebar">
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ export function reorderSectionCircuits(sectionId: string, orderedCircuitIds: str
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateSectionEquipmentIdentifiers(
|
||||||
|
sectionId: string,
|
||||||
|
identifiers: Array<{ circuitId: string; equipmentIdentifier: string }>
|
||||||
|
) {
|
||||||
|
return request(`/api/circuit-sections/${sectionId}/equipment-identifiers`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ identifiers }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function listFloors(projectId: string) {
|
export function listFloors(projectId: string) {
|
||||||
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||||
import { reorderSectionCircuitsSchema } from "../../shared/validation/circuit.schemas.js";
|
import {
|
||||||
|
reorderSectionCircuitsSchema,
|
||||||
|
updateSectionEquipmentIdentifiersSchema,
|
||||||
|
} from "../../shared/validation/circuit.schemas.js";
|
||||||
|
|
||||||
const circuitWriteService = new CircuitWriteService();
|
const circuitWriteService = new CircuitWriteService();
|
||||||
|
|
||||||
@@ -36,3 +39,22 @@ export async function reorderSectionCircuits(req: Request, res: Response) {
|
|||||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to reorder circuits." });
|
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to reorder circuits." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateSectionEquipmentIdentifiers(req: Request, res: Response) {
|
||||||
|
const { sectionId } = req.params;
|
||||||
|
if (typeof sectionId !== "string") {
|
||||||
|
return res.status(400).json({ error: "Invalid sectionId" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = updateSectionEquipmentIdentifiersSchema.safeParse(req.body);
|
||||||
|
if (!parsed.success) {
|
||||||
|
return res.status(400).json({ error: parsed.error.flatten() });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const updatedCircuits = await circuitWriteService.updateSectionEquipmentIdentifiers(sectionId, parsed.data);
|
||||||
|
return res.json({ sectionId, circuits: updatedCircuits });
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to update section identifiers." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { renumberCircuitSection, reorderSectionCircuits } from "../controllers/circuit-section.controller.js";
|
import {
|
||||||
|
renumberCircuitSection,
|
||||||
|
reorderSectionCircuits,
|
||||||
|
updateSectionEquipmentIdentifiers,
|
||||||
|
} from "../controllers/circuit-section.controller.js";
|
||||||
|
|
||||||
export const circuitSectionRouter = Router();
|
export const circuitSectionRouter = Router();
|
||||||
|
|
||||||
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
|
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
|
||||||
circuitSectionRouter.patch("/circuit-sections/:sectionId/circuits/reorder", reorderSectionCircuits);
|
circuitSectionRouter.patch("/circuit-sections/:sectionId/circuits/reorder", reorderSectionCircuits);
|
||||||
|
circuitSectionRouter.patch("/circuit-sections/:sectionId/equipment-identifiers", updateSectionEquipmentIdentifiers);
|
||||||
|
|||||||
@@ -66,9 +66,21 @@ export const reorderSectionCircuitsSchema = z.object({
|
|||||||
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const updateSectionEquipmentIdentifiersSchema = z.object({
|
||||||
|
identifiers: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
circuitId: z.string().min(1),
|
||||||
|
equipmentIdentifier: z.string().min(1),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
});
|
||||||
|
|
||||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
||||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
||||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
||||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||||
|
export type UpdateSectionEquipmentIdentifiersInput = z.infer<typeof updateSectionEquipmentIdentifiersSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
import { CircuitWriteService } from "../src/domain/services/circuit-write.service.js";
|
||||||
|
import { CircuitRepository } from "../src/db/repositories/circuit.repository.js";
|
||||||
|
import { db } from "../src/db/client.js";
|
||||||
|
|
||||||
describe("circuit write service rules", () => {
|
describe("circuit write service rules", () => {
|
||||||
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
it("rejects duplicate equipment identifiers in same circuit list", async () => {
|
||||||
@@ -146,8 +148,8 @@ describe("circuit write service rules", () => {
|
|||||||
assert.equal(reserveFlag, false);
|
assert.equal(reserveFlag, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renumber affects only circuits in selected section and keeps row order untouched", async () => {
|
it("renumber uses safe bulk identifier update for swapped identifiers", async () => {
|
||||||
const updatedIds: string[] = [];
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||||
const service = new CircuitWriteService({
|
const service = new CircuitWriteService({
|
||||||
circuitSectionRepository: {
|
circuitSectionRepository: {
|
||||||
async findById() {
|
async findById() {
|
||||||
@@ -157,24 +159,94 @@ describe("circuit write service rules", () => {
|
|||||||
circuitRepository: {
|
circuitRepository: {
|
||||||
async listBySection() {
|
async listBySection() {
|
||||||
return [
|
return [
|
||||||
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-2F7", sortOrder: 10, isReserve: 0 },
|
{ id: "cB", sectionId: "s1", equipmentIdentifier: "-2F2", sortOrder: 10, isReserve: 0 },
|
||||||
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-2F9", sortOrder: 20, isReserve: 1 },
|
{ id: "cA", sectionId: "s1", equipmentIdentifier: "-2F1", sortOrder: 20, isReserve: 1 },
|
||||||
] as never[];
|
] as never[];
|
||||||
},
|
},
|
||||||
async listByCircuitList() {
|
async listByCircuitList() {
|
||||||
return [{ id: "x1", sectionId: "s2", equipmentIdentifier: "-3F1" }] as never[];
|
return [{ id: "x1", sectionId: "s2", equipmentIdentifier: "-3F1" }] as never[];
|
||||||
},
|
},
|
||||||
async update(circuitId: string) {
|
async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
||||||
updatedIds.push(circuitId);
|
safeUpdatePayload = updates;
|
||||||
},
|
},
|
||||||
} as never,
|
} as never,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await service.renumberSection("s1");
|
const result = await service.renumberSection("s1");
|
||||||
assert.deepEqual(updatedIds, ["c1", "c2"]);
|
assert.deepEqual(safeUpdatePayload, [
|
||||||
|
{ id: "cB", equipmentIdentifier: "-2F1" },
|
||||||
|
{ id: "cA", equipmentIdentifier: "-2F2" },
|
||||||
|
]);
|
||||||
assert.equal(result.length, 2);
|
assert.equal(result.length, 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renumber shifts forward/backward and respects other sections", async () => {
|
||||||
|
let safeUpdatePayload: Array<{ id: string; equipmentIdentifier: string }> = [];
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
circuitSectionRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async listBySection() {
|
||||||
|
return [
|
||||||
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 10, isReserve: 0 },
|
||||||
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 20, isReserve: 0 },
|
||||||
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
|
||||||
|
] as never[];
|
||||||
|
},
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [{ id: "o1", sectionId: "s2", equipmentIdentifier: "-1F2" }] as never[];
|
||||||
|
},
|
||||||
|
async updateEquipmentIdentifiersSafely(_listId: string, updates: Array<{ id: string; equipmentIdentifier: string }>) {
|
||||||
|
safeUpdatePayload = updates;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.renumberSection("s1");
|
||||||
|
assert.deepEqual(safeUpdatePayload, [
|
||||||
|
{ id: "c2", equipmentIdentifier: "-1F1" },
|
||||||
|
{ id: "c1", equipmentIdentifier: "-1F3" },
|
||||||
|
{ id: "c3", equipmentIdentifier: "-1F4" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renumber handles gaps and keeps device rows untouched by identifier-only update path", async () => {
|
||||||
|
let safeCalled = 0;
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
circuitSectionRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async listBySection() {
|
||||||
|
return [
|
||||||
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
|
||||||
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F5", sortOrder: 20, isReserve: 0 },
|
||||||
|
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-1F9", sortOrder: 30, isReserve: 0 },
|
||||||
|
] as never[];
|
||||||
|
},
|
||||||
|
async listByCircuitList() {
|
||||||
|
return [] as never[];
|
||||||
|
},
|
||||||
|
async updateEquipmentIdentifiersSafely() {
|
||||||
|
safeCalled += 1;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
deviceRowRepository: {
|
||||||
|
async update() {
|
||||||
|
throw new Error("device rows must not be touched");
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
await service.renumberSection("s1");
|
||||||
|
assert.equal(safeCalled, 1);
|
||||||
|
});
|
||||||
|
|
||||||
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 }> = [];
|
const updatedReserve: Array<{ id: string; isReserve: boolean }> = [];
|
||||||
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
const movedCalls: Array<{ rowId: string; targetCircuitId: string; sortOrder: number }> = [];
|
||||||
@@ -330,4 +402,93 @@ describe("circuit write service rules", () => {
|
|||||||
{ id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" },
|
{ id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("safe section identifier bulk update validates full section set (undo safety)", async () => {
|
||||||
|
let safeCalled = false;
|
||||||
|
const service = new CircuitWriteService({
|
||||||
|
circuitSectionRepository: {
|
||||||
|
async findById() {
|
||||||
|
return { id: "s1", circuitListId: "l1", prefix: "-1F" } as never;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
circuitRepository: {
|
||||||
|
async listBySection() {
|
||||||
|
return [
|
||||||
|
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-1F1", sortOrder: 10, isReserve: 0 },
|
||||||
|
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-1F2", sortOrder: 20, isReserve: 0 },
|
||||||
|
] as never[];
|
||||||
|
},
|
||||||
|
async updateEquipmentIdentifiersSafely() {
|
||||||
|
safeCalled = true;
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
await service.updateSectionEquipmentIdentifiers("s1", {
|
||||||
|
identifiers: [
|
||||||
|
{ circuitId: "c1", equipmentIdentifier: "-1F2" },
|
||||||
|
{ circuitId: "c2", equipmentIdentifier: "-1F1" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(safeCalled, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("safe identifier bulk update uses synchronous transaction callback", async () => {
|
||||||
|
const repository = new CircuitRepository();
|
||||||
|
const originalTransaction = (db as unknown as { transaction: unknown }).transaction;
|
||||||
|
|
||||||
|
let callbackReturnedPromise = false;
|
||||||
|
(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" }];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
update() {
|
||||||
|
return {
|
||||||
|
set() {
|
||||||
|
return {
|
||||||
|
where() {
|
||||||
|
return {
|
||||||
|
run() {
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = cb(fakeTx);
|
||||||
|
callbackReturnedPromise = Boolean(result && typeof (result as Promise<unknown>).then === "function");
|
||||||
|
if (callbackReturnedPromise) {
|
||||||
|
throw new Error("Transaction function cannot return a promise");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await repository.updateEquipmentIdentifiersSafely(
|
||||||
|
"l1",
|
||||||
|
[
|
||||||
|
{ id: "c1", equipmentIdentifier: "-1F1" },
|
||||||
|
{ id: "c2", equipmentIdentifier: "-1F2" },
|
||||||
|
],
|
||||||
|
"s1"
|
||||||
|
);
|
||||||
|
assert.equal(callbackReturnedPromise, false);
|
||||||
|
} finally {
|
||||||
|
(db as unknown as { transaction: unknown }).transaction = originalTransaction;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user