Undo Redo working
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
moveCircuitDeviceRowById,
|
||||
reorderSectionCircuits,
|
||||
renumberCircuitSection,
|
||||
updateSectionEquipmentIdentifiers,
|
||||
updateCircuitById,
|
||||
updateCircuitDeviceRowById,
|
||||
} from "../utils/api";
|
||||
@@ -76,6 +77,33 @@ interface VisibleGridRow {
|
||||
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 =
|
||||
| { kind: "new-circuit"; 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 [draggingCircuitId, setDraggingCircuitId] = useState<string | 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 pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -511,6 +542,69 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
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(() => {
|
||||
const intent = pendingSelectionAfterReload.current;
|
||||
if (!intent || !data) {
|
||||
@@ -693,6 +787,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
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") {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
@@ -704,20 +808,93 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||
const nextSelectionIntent = () => {
|
||||
let selected = targetSelection;
|
||||
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") {
|
||||
targetSelection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft);
|
||||
} else if (cell.kind === "circuitField" && row.circuit) {
|
||||
await patchCircuit(row.circuit.id, editingCell.cellKey, editingCell.draft);
|
||||
} else if (cell.kind === "deviceField") {
|
||||
if (row.device) {
|
||||
await patchDeviceRow(row.device.id, editingCell.cellKey, editingCell.draft);
|
||||
} else if (row.rowType === "reserveCircuit" && row.circuit) {
|
||||
const created = (await createCircuitDeviceRow(row.circuit.id, {
|
||||
if (row.rowType === "placeholder") {
|
||||
let createdCircuitId: string | null = null;
|
||||
const command: HistoryCommand = {
|
||||
label: "Create from placeholder",
|
||||
redo: async () => {
|
||||
const selection = await createFromPlaceholder(row.sectionId, editingCell.cellKey, editingCell.draft);
|
||||
targetSelection = selection;
|
||||
const createdRow = selection.rowKey.startsWith("circuitCompact:")
|
||||
? 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",
|
||||
displayName: "Reserve load",
|
||||
phaseType: "single_phase",
|
||||
@@ -726,37 +903,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
})) as { id: string };
|
||||
await patchDeviceRow(created.id, editingCell.cellKey, editingCell.draft);
|
||||
targetSelection = { rowKey: `circuitCompact:${row.circuit.id}`, cellKey: editingCell.cellKey };
|
||||
}
|
||||
}
|
||||
|
||||
if (direction !== "stay") {
|
||||
const idx = editableCells.findIndex(
|
||||
(entry) => entry.rowKey === targetSelection.rowKey && entry.cellKey === targetSelection.cellKey
|
||||
);
|
||||
if (idx >= 0) {
|
||||
const targetIdx =
|
||||
direction === "next"
|
||||
? Math.min(editableCells.length - 1, idx + 1)
|
||||
: Math.max(0, idx - 1);
|
||||
targetSelection = editableCells[targetIdx];
|
||||
}
|
||||
}
|
||||
|
||||
const intent = buildSelectionIntent(targetSelection);
|
||||
createdRowId = created.id;
|
||||
await patchDeviceRow(created.id, editingCell.cellKey, next);
|
||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
undo: async () => {
|
||||
if (!createdRowId) {
|
||||
return null;
|
||||
}
|
||||
await deleteCircuitDeviceRowById(createdRowId);
|
||||
return buildSelectionIntent({ rowKey: `reserveCircuit:${row.circuit!.id}`, cellKey: editingCell.cellKey });
|
||||
},
|
||||
};
|
||||
setEditingCell(null);
|
||||
if (intent) {
|
||||
pendingSelectionAfterReload.current = intent;
|
||||
}
|
||||
await loadTree({ showLoading: false });
|
||||
if (!intent) {
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
await runCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -770,56 +931,86 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
async function handleAddReserveCircuit(sectionId: string) {
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
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);
|
||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
const created = (await createCircuitDeviceRow(circuit.id, {
|
||||
name: "Manual device",
|
||||
displayName: "Manual device",
|
||||
phaseType: "single_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
})) as { id: string };
|
||||
await loadTree({ showLoading: false });
|
||||
setActiveSectionId(sectionId);
|
||||
setPendingFocus({
|
||||
rowKey: circuit.deviceRows.length === 0 ? `circuitCompact:${circuit.id}` : `device:${created.id}`,
|
||||
cellKey: "displayName",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
let createdRowId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Add manual device",
|
||||
redo: async () => {
|
||||
const created = (await createCircuitDeviceRow(circuit.id, {
|
||||
name: "Manual device",
|
||||
displayName: "Manual device",
|
||||
phaseType: "single_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
})) as { id: string };
|
||||
createdRowId = created.id;
|
||||
setActiveSectionId(sectionId);
|
||||
return {
|
||||
rowKey: `device:${created.id}`,
|
||||
cellKey: "displayName",
|
||||
rowType: "deviceRow",
|
||||
sectionId,
|
||||
circuitId: circuit.id,
|
||||
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 {
|
||||
@@ -846,15 +1037,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError("Please select a target section.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await insertProjectDeviceAsNewCircuit(device, targetSectionId);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
let createdCircuitId: string | null = null;
|
||||
let createdRowId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Add project device as new circuit",
|
||||
redo: async () => {
|
||||
const created = await insertProjectDeviceAsNewCircuit(device, targetSectionId);
|
||||
createdCircuitId = created.circuitId;
|
||||
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() {
|
||||
@@ -875,15 +1086,34 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await insertProjectDeviceToCircuit(device, circuitId);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
let createdRowId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Add project device to circuit",
|
||||
redo: async () => {
|
||||
const created = await insertProjectDeviceToCircuit(device, circuitId);
|
||||
createdRowId = created.rowId;
|
||||
return {
|
||||
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) {
|
||||
@@ -914,10 +1144,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
|
||||
await loadTree({ showLoading: false });
|
||||
setActiveSectionId(sectionId);
|
||||
setTargetCircuitId(createdCircuit.id);
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
return { circuitId: createdCircuit.id, rowId: createdRow.id };
|
||||
}
|
||||
|
||||
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
||||
@@ -932,8 +1161,84 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
cosPhi: device.powerFactor ?? undefined,
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
await loadTree({ showLoading: false });
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
return { rowId: createdRow.id };
|
||||
}
|
||||
|
||||
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>) {
|
||||
@@ -1005,19 +1310,53 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError("Invalid project device drop source.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
if (intent.kind === "new-circuit") {
|
||||
await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
||||
} else {
|
||||
await insertProjectDeviceToCircuit(device, intent.circuitId);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
if (intent.kind === "new-circuit") {
|
||||
let createdCircuitId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Drop project device to new circuit",
|
||||
redo: async () => {
|
||||
const created = await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
||||
createdCircuitId = created.circuitId;
|
||||
return {
|
||||
rowKey: `device:${created.rowId}`,
|
||||
cellKey: "displayName",
|
||||
rowType: "deviceRow",
|
||||
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) {
|
||||
@@ -1037,27 +1376,43 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
||||
} else {
|
||||
await moveCircuitDeviceRowById(rowId, {
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
label: "Move device row",
|
||||
redo: async () => {
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
||||
return null;
|
||||
},
|
||||
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,
|
||||
createNewCircuit: true,
|
||||
});
|
||||
}
|
||||
await loadTree({ showLoading: false });
|
||||
setPendingFocus({ rowKey: `device:${rowId}`, cellKey: "displayName" });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
})) as { circuitId?: string };
|
||||
createdCircuitId = moved.circuitId ?? null;
|
||||
return null;
|
||||
},
|
||||
undo: async () => {
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: sourceCircuitId });
|
||||
if (createdCircuitId) {
|
||||
await deleteCircuitById(createdCircuitId);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
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.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await applyCircuitReorder(intent, sourceCircuitId);
|
||||
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 section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
||||
if (!section) {
|
||||
setError("Invalid section id.");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (!confirm("Delete this device row?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await deleteCircuitDeviceRowById(rowId);
|
||||
await loadTree({ showLoading: false });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||
const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null;
|
||||
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
|
||||
if (!sourceCircuitId || !rowSnapshot) {
|
||||
setError("Invalid device row id.");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (!confirm("Delete this circuit and all assigned device rows?")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await deleteCircuitById(circuitId);
|
||||
await loadTree({ showLoading: false });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
const snapshot = getCircuitSnapshot(circuitId);
|
||||
if (!snapshot) {
|
||||
setError("Invalid circuit id.");
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (!confirm("Renumber this section? Only circuits in this section will change.")) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await renumberCircuitSection(sectionId);
|
||||
await loadTree({ showLoading: false });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||
if (!section) {
|
||||
return;
|
||||
}
|
||||
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() {
|
||||
@@ -1162,6 +1581,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
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 =
|
||||
event.ctrlKey &&
|
||||
(event.key === "+" || (event.shiftKey && event.key === "=") || event.code === "NumpadAdd");
|
||||
@@ -1228,6 +1661,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
return (
|
||||
<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}
|
||||
<div className="tree-editor-layout">
|
||||
<aside className="project-device-sidebar">
|
||||
|
||||
Reference in New Issue
Block a user