Harden grid deletion and BMK validation
This commit is contained in:
@@ -9,6 +9,11 @@ import {
|
||||
getInsertionSortOrder,
|
||||
resolveGridInsertionIntent,
|
||||
} from "../utils/circuit-grid-insertion";
|
||||
import {
|
||||
findDuplicateEquipmentIdentifierCircuitIds,
|
||||
hasEquipmentIdentifierConflict,
|
||||
resolveGridDeleteIntent,
|
||||
} from "../utils/circuit-grid-safety";
|
||||
import {
|
||||
createCircuit,
|
||||
createCircuitDeviceRow,
|
||||
@@ -788,6 +793,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
const allCircuits = useMemo(
|
||||
() => data?.sections.flatMap((section) => section.circuits) ?? [],
|
||||
[data]
|
||||
);
|
||||
const duplicateEquipmentIdentifierCircuitIds = useMemo(
|
||||
() => new Set(findDuplicateEquipmentIdentifierCircuitIds(allCircuits)),
|
||||
[allCircuits]
|
||||
);
|
||||
|
||||
function makeRow(
|
||||
rowType: RowType,
|
||||
sectionId: string,
|
||||
@@ -1495,6 +1509,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setEditingCell(null);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
editingCell.cellKey === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, editingCell.draft)
|
||||
) {
|
||||
setError("Dieses Betriebsmittelkennzeichen ist bereits vorhanden. Die Änderung wurde nicht gespeichert.");
|
||||
return;
|
||||
}
|
||||
|
||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||
const nextSelectionIntent = () => {
|
||||
@@ -2377,16 +2399,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
// Delete undo recreates the row from captured snapshot because delete is destructive.
|
||||
async function handleDeleteDevice(rowId: string) {
|
||||
if (!confirm("Delete this device row?")) {
|
||||
return;
|
||||
}
|
||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||
const sourceCircuit = sourceCircuitId ? getCircuitSnapshot(sourceCircuitId) : null;
|
||||
const rowSnapshot = sourceCircuit?.deviceRows.find((row) => row.id === rowId);
|
||||
if (!sourceCircuitId || !rowSnapshot) {
|
||||
if (!sourceCircuitId || !sourceCircuit || !rowSnapshot) {
|
||||
setError("Invalid device row id.");
|
||||
return;
|
||||
}
|
||||
if (sourceCircuit.deviceRows.length === 1) {
|
||||
const keepAsReserve = confirm(
|
||||
`"${rowSnapshot.displayName}" is the last device in ${sourceCircuit.equipmentIdentifier}.\n\n` +
|
||||
"OK: delete the device and keep the empty circuit as reserve.\n" +
|
||||
"Cancel: choose whether to delete the whole circuit instead."
|
||||
);
|
||||
if (!keepAsReserve) {
|
||||
const deleteCircuit = confirm(
|
||||
`Delete the complete circuit ${sourceCircuit.equipmentIdentifier}?\n\n` +
|
||||
"OK: delete circuit and device.\nCancel: do not delete anything."
|
||||
);
|
||||
if (deleteCircuit) {
|
||||
await handleDeleteCircuit(sourceCircuitId, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else if (!confirm(`Delete device row "${rowSnapshot.displayName}"?`)) {
|
||||
return;
|
||||
}
|
||||
let recreatedRowId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Delete device row",
|
||||
@@ -2423,15 +2461,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
// Deletes a circuit and all child rows; undo recreates from captured snapshot.
|
||||
async function handleDeleteCircuit(circuitId: string) {
|
||||
if (!confirm("Delete this circuit and all assigned device rows?")) {
|
||||
return;
|
||||
}
|
||||
async function handleDeleteCircuit(circuitId: string, askForConfirmation = true) {
|
||||
const snapshot = getCircuitSnapshot(circuitId);
|
||||
if (!snapshot) {
|
||||
setError("Invalid circuit id.");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
askForConfirmation &&
|
||||
!confirm(
|
||||
`Delete circuit ${snapshot.equipmentIdentifier} and ${snapshot.deviceRows.length} assigned device row(s)?\n\n` +
|
||||
"Existing circuit identifiers will not be renumbered."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let recreatedCircuitId: string | null = null;
|
||||
await runCommand({
|
||||
label: "Delete circuit",
|
||||
@@ -2447,6 +2491,29 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}
|
||||
|
||||
async function handleDeleteSelection() {
|
||||
const row = selectedCell ? findRow(selectedCell.rowKey) : null;
|
||||
const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey);
|
||||
const intent = resolveGridDeleteIntent(
|
||||
row && cell && row.rowType !== "section"
|
||||
? {
|
||||
rowType: row.rowType,
|
||||
cellKind: cell.kind,
|
||||
circuitId: row.circuit?.id,
|
||||
deviceId: row.device?.id,
|
||||
}
|
||||
: null
|
||||
);
|
||||
if (!intent) {
|
||||
return;
|
||||
}
|
||||
if (intent.kind === "device") {
|
||||
await handleDeleteDevice(intent.deviceId);
|
||||
return;
|
||||
}
|
||||
await handleDeleteCircuit(intent.circuitId);
|
||||
}
|
||||
|
||||
// Explicit renumber action. Undo restores previous BMKs via safe section identifier update.
|
||||
async function handleRenumberSection(sectionId: string) {
|
||||
if (!confirm("Renumber this section? Only circuits in this section will change.")) {
|
||||
@@ -2506,6 +2573,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
return;
|
||||
}
|
||||
const eventTarget = event.target as HTMLElement;
|
||||
if (eventTarget.closest("button, input, select, textarea")) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
if (selectedRowKeys.length > 0) {
|
||||
event.preventDefault();
|
||||
@@ -2536,6 +2607,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
void handleInsertBelowSelection();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Delete" && selectedCell) {
|
||||
event.preventDefault();
|
||||
void handleDeleteSelection();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Tab" && selectedCell) {
|
||||
event.preventDefault();
|
||||
const idx = editableCells.findIndex(
|
||||
@@ -2583,7 +2659,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (isLoading && !data) {
|
||||
return <div className="notice info">Loading circuit tree editor...</div>;
|
||||
}
|
||||
if (error) {
|
||||
if (error && !data) {
|
||||
return <div className="notice error">{error}</div>;
|
||||
}
|
||||
if (!data || data.sections.length === 0) {
|
||||
@@ -2657,6 +2733,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
{error ? (
|
||||
<div className="notice error editor-error-notice" role="alert">
|
||||
<span>{error}</span>
|
||||
<button type="button" onClick={() => setError(null)}>Dismiss</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="notice muted">No matching circuits for active filters.</div>
|
||||
</div>
|
||||
);
|
||||
@@ -2773,6 +2855,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
{isSortedView && hasActiveFilters ? (
|
||||
<div className="notice muted">Apply sorted order is disabled while filters are active.</div>
|
||||
) : null}
|
||||
{error ? (
|
||||
<div className="notice error editor-error-notice" role="alert">
|
||||
<span>{error}</span>
|
||||
<button type="button" onClick={() => setError(null)}>Dismiss</button>
|
||||
</div>
|
||||
) : null}
|
||||
{duplicateEquipmentIdentifierCircuitIds.size > 0 ? (
|
||||
<div className="notice warning" role="alert">
|
||||
Duplicate equipment identifiers are highlighted. Renumbering remains an explicit action.
|
||||
</div>
|
||||
) : null}
|
||||
{isSaving ? <div className="notice info">Saving...</div> : null}
|
||||
<div className="tree-editor-layout">
|
||||
<aside className="project-device-sidebar">
|
||||
@@ -3327,10 +3420,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
selectedCell?.rowKey === row.rowKey && selectedCell.cellKey === column.key;
|
||||
const isEditing =
|
||||
editingCell?.rowKey === row.rowKey && editingCell.cellKey === column.key;
|
||||
const hasDraftIdentifierConflict = Boolean(
|
||||
isEditing &&
|
||||
column.key === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, editingCell?.draft ?? "")
|
||||
);
|
||||
const hasIdentifierConflict = Boolean(
|
||||
column.key === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
(duplicateEquipmentIdentifierCircuitIds.has(row.circuit.id) || hasDraftIdentifierConflict)
|
||||
);
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
className={`${column.numeric ? "num" : ""} ${cell.editable ? "cell-editable" : ""} ${isSelected ? "cell-selected" : ""} ${
|
||||
className={`${column.numeric ? "num" : ""} ${cell.editable ? "cell-editable" : ""} ${isSelected ? "cell-selected" : ""} ${hasIdentifierConflict ? "cell-invalid" : ""} ${
|
||||
Boolean(row.device) && column.key === "displayName" && (row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
? "device-drag-handle"
|
||||
: ""
|
||||
@@ -3426,6 +3530,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<input
|
||||
ref={inputRef}
|
||||
value={editingCell.draft}
|
||||
aria-invalid={hasDraftIdentifierConflict}
|
||||
title={hasDraftIdentifierConflict ? "Equipment identifier already exists." : undefined}
|
||||
onChange={(event) =>
|
||||
setEditingCell((current) =>
|
||||
current ? { ...current, draft: event.target.value } : current
|
||||
|
||||
Reference in New Issue
Block a user