Bugfix editor
This commit is contained in:
@@ -61,6 +61,8 @@ interface EditingCell extends SelectedCell {
|
||||
draft: string;
|
||||
}
|
||||
|
||||
type SaveDirection = "stay" | "next" | "prev";
|
||||
|
||||
const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier" },
|
||||
{ key: "name", label: "Name" },
|
||||
@@ -228,6 +230,17 @@ function parseNumeric(cellKey: CellKey, draft: string): number | undefined {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function normalizeUiError(err: unknown): string {
|
||||
const message = err instanceof Error ? err.message : "Operation failed.";
|
||||
if (message.includes("Duplicate equipmentIdentifier")) {
|
||||
return "Das Betriebsmittelkennzeichen ist in dieser Stromkreisliste bereits vorhanden.";
|
||||
}
|
||||
if (message.includes("Invalid number")) {
|
||||
return "Bitte einen gültigen Zahlenwert eingeben.";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
||||
const { projectId, circuitListId } = props;
|
||||
const [data, setData] = useState<CircuitTreeResponseDto | null>(null);
|
||||
@@ -238,7 +251,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||
const pendingSelectionAfterReload = useRef<SelectedCell | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
async function loadTree() {
|
||||
setIsLoading(true);
|
||||
@@ -246,10 +261,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
try {
|
||||
const tree = await getCircuitTree(projectId, circuitListId);
|
||||
setData(tree);
|
||||
if (pendingSelectionAfterReload.current) {
|
||||
setSelectedCell(pendingSelectionAfterReload.current);
|
||||
pendingSelectionAfterReload.current = null;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Could not load circuit tree.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,10 +330,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
setSelectedCell(pendingFocus);
|
||||
setEditingCell({ ...pendingFocus, draft: "" });
|
||||
startEdit(pendingFocus);
|
||||
setPendingFocus(null);
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}, [pendingFocus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
inputRef.current?.focus();
|
||||
inputRef.current?.select();
|
||||
});
|
||||
}, [editingCell]);
|
||||
|
||||
const editableCells = useMemo(() => {
|
||||
const cells: SelectedCell[] = [];
|
||||
for (const row of gridRows) {
|
||||
@@ -326,6 +357,22 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return cells;
|
||||
}, [gridRows]);
|
||||
|
||||
const rowEditableCells = useMemo(() => {
|
||||
const map = new Map<string, CellKey[]>();
|
||||
for (const row of gridRows) {
|
||||
const keys = columns.map((col) => col.key).filter((key) => canEditCell(row, key));
|
||||
if (keys.length > 0) {
|
||||
map.set(row.rowKey, keys);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [gridRows]);
|
||||
|
||||
const editableRowOrder = useMemo(
|
||||
() => gridRows.filter((row) => rowEditableCells.has(row.rowKey)).map((row) => row.rowKey),
|
||||
[gridRows, rowEditableCells]
|
||||
);
|
||||
|
||||
function findRow(rowKey: string) {
|
||||
return gridRows.find((row) => row.rowKey === rowKey);
|
||||
}
|
||||
@@ -337,9 +384,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (cellKey === "rowTotalPower" || cellKey === "circuitTotalPower") {
|
||||
return false;
|
||||
}
|
||||
if (row.kind === "summary" || row.kind === "reserve") {
|
||||
if (row.kind === "summary") {
|
||||
return isCircuitField(cellKey);
|
||||
}
|
||||
if (row.kind === "reserve") {
|
||||
return isCircuitField(cellKey) || isDeviceField(cellKey);
|
||||
}
|
||||
if (row.kind === "device") {
|
||||
return isDeviceField(cellKey);
|
||||
}
|
||||
@@ -378,24 +428,57 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setEditingCell({ ...cell, draft: value === "-" ? "" : value });
|
||||
}
|
||||
|
||||
function moveSelection(offset: number) {
|
||||
function moveHorizontal(direction: 1 | -1) {
|
||||
if (!selectedCell) {
|
||||
if (editableCells.length > 0) {
|
||||
if (editableCells.length) {
|
||||
setSelectedCell(editableCells[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const index = editableCells.findIndex(
|
||||
(cell) => cell.rowKey === selectedCell.rowKey && cell.cellKey === selectedCell.cellKey
|
||||
);
|
||||
if (index < 0) {
|
||||
const keys = rowEditableCells.get(selectedCell.rowKey);
|
||||
if (!keys || !keys.length) {
|
||||
return;
|
||||
}
|
||||
const nextIndex = Math.min(editableCells.length - 1, Math.max(0, index + offset));
|
||||
setSelectedCell(editableCells[nextIndex]);
|
||||
const currentIndex = keys.indexOf(selectedCell.cellKey);
|
||||
const nextIndex = Math.min(keys.length - 1, Math.max(0, currentIndex + direction));
|
||||
setSelectedCell({ rowKey: selectedCell.rowKey, cellKey: keys[nextIndex] });
|
||||
}
|
||||
|
||||
async function saveEditingCell(nextCell?: SelectedCell | null) {
|
||||
function moveVertical(direction: 1 | -1) {
|
||||
if (!selectedCell) {
|
||||
if (editableCells.length) {
|
||||
setSelectedCell(editableCells[0]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const rowIndex = editableRowOrder.indexOf(selectedCell.rowKey);
|
||||
if (rowIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const targetRowIndex = rowIndex + direction;
|
||||
if (targetRowIndex < 0 || targetRowIndex >= editableRowOrder.length) {
|
||||
return;
|
||||
}
|
||||
const targetRowKey = editableRowOrder[targetRowIndex];
|
||||
const targetKeys = rowEditableCells.get(targetRowKey) ?? [];
|
||||
if (!targetKeys.length) {
|
||||
return;
|
||||
}
|
||||
const preferredColIndex = columns.findIndex((column) => column.key === selectedCell.cellKey);
|
||||
let best = targetKeys[0];
|
||||
let bestDistance = Number.POSITIVE_INFINITY;
|
||||
for (const key of targetKeys) {
|
||||
const idx = columns.findIndex((column) => column.key === key);
|
||||
const distance = Math.abs(idx - preferredColIndex);
|
||||
if (distance < bestDistance) {
|
||||
bestDistance = distance;
|
||||
best = key;
|
||||
}
|
||||
}
|
||||
setSelectedCell({ rowKey: targetRowKey, cellKey: best });
|
||||
}
|
||||
|
||||
async function saveEditingCell(direction: SaveDirection = "stay") {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
}
|
||||
@@ -409,9 +492,22 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError(null);
|
||||
const key = editingCell.cellKey;
|
||||
const draft = editingCell.draft;
|
||||
let nextSelection: SelectedCell | null = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||
|
||||
if ((row.kind === "summary" || row.kind === "reserve") && isCircuitField(key)) {
|
||||
await patchCircuit(row.circuit.id, key, draft);
|
||||
} else if (row.kind === "reserve" && isDeviceField(key)) {
|
||||
const created = (await createCircuitDeviceRow(row.circuit.id, {
|
||||
name: "Reserve load",
|
||||
displayName: "Reserve load",
|
||||
phaseType: "single_phase",
|
||||
quantity: 1,
|
||||
powerPerUnit: 0,
|
||||
simultaneityFactor: 1,
|
||||
cosPhi: 1,
|
||||
})) as { id: string };
|
||||
await patchDeviceRow(created.id, key, draft);
|
||||
nextSelection = { rowKey: `compact:${row.circuit.id}`, cellKey: key };
|
||||
} else if (row.kind === "device" && row.device && isDeviceField(key)) {
|
||||
await patchDeviceRow(row.device.id, key, draft);
|
||||
} else if (row.kind === "compact") {
|
||||
@@ -422,13 +518,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
setEditingCell(null);
|
||||
await loadTree();
|
||||
if (nextCell) {
|
||||
setSelectedCell(nextCell);
|
||||
if (direction !== "stay" && nextSelection) {
|
||||
const idx = editableCells.findIndex(
|
||||
(cell) => cell.rowKey === nextSelection!.rowKey && cell.cellKey === nextSelection!.cellKey
|
||||
);
|
||||
if (idx >= 0) {
|
||||
const targetIdx =
|
||||
direction === "next"
|
||||
? Math.min(editableCells.length - 1, idx + 1)
|
||||
: Math.max(0, idx - 1);
|
||||
nextSelection = editableCells[targetIdx];
|
||||
}
|
||||
}
|
||||
|
||||
setEditingCell(null);
|
||||
pendingSelectionAfterReload.current = nextSelection;
|
||||
await loadTree();
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Save failed.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -478,8 +586,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
await loadTree();
|
||||
setActiveSectionId(sectionId);
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to add reserve circuit.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -505,7 +614,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
cellKey: "displayName",
|
||||
});
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to add device row.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -521,7 +630,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await deleteCircuitDeviceRowById(rowId);
|
||||
await loadTree();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to delete device row.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -537,7 +646,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await deleteCircuitById(circuitId);
|
||||
await loadTree();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to delete circuit.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -553,7 +662,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
await renumberCircuitSection(sectionId);
|
||||
await loadTree();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to renumber section.");
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -561,6 +670,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||
if (editingCell) {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const isCtrlPlus =
|
||||
@@ -576,16 +688,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
moveSelection(1);
|
||||
moveHorizontal(1);
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
moveSelection(-1);
|
||||
moveHorizontal(-1);
|
||||
} else if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
moveSelection(1);
|
||||
moveVertical(1);
|
||||
} else if (event.key === "ArrowUp") {
|
||||
event.preventDefault();
|
||||
moveSelection(-1);
|
||||
moveVertical(-1);
|
||||
} else if (event.key === "Enter" || event.key === "F2") {
|
||||
event.preventDefault();
|
||||
if (selectedCell) {
|
||||
@@ -599,6 +711,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
function handleContainerFocus() {
|
||||
if (editingCell) {
|
||||
const row = findRow(editingCell.rowKey);
|
||||
if (!row || !canEditCell(row, editingCell.cellKey)) {
|
||||
setEditingCell(null);
|
||||
} else {
|
||||
requestAnimationFrame(() => inputRef.current?.focus());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!selectedCell && editableCells.length > 0) {
|
||||
setSelectedCell(editableCells[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="notice info">Loading circuit tree editor...</div>;
|
||||
}
|
||||
@@ -612,7 +739,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return (
|
||||
<div className="tree-editor-shell">
|
||||
{isSaving ? <div className="notice info">Saving...</div> : null}
|
||||
<div className="tree-grid-wrap" ref={containerRef} tabIndex={0} onKeyDown={handleKeyDown}>
|
||||
<div
|
||||
className="tree-grid-wrap"
|
||||
ref={containerRef}
|
||||
tabIndex={0}
|
||||
onKeyDown={handleKeyDown}
|
||||
onFocus={handleContainerFocus}
|
||||
>
|
||||
<table className="tree-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -673,11 +806,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
key={column.key}
|
||||
className={`${column.numeric ? "num" : ""} ${selected ? "cell-selected" : ""} ${editable ? "cell-editable" : ""}`}
|
||||
onClick={() => setSelectedCell({ rowKey: row.rowKey, cellKey: column.key })}
|
||||
onClickCapture={() => requestAnimationFrame(() => containerRef.current?.focus())}
|
||||
onDoubleClick={() => startEdit({ rowKey: row.rowKey, cellKey: column.key })}
|
||||
>
|
||||
{editing ? (
|
||||
<input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
value={editingCell.draft}
|
||||
onChange={(event) =>
|
||||
setEditingCell((current) =>
|
||||
@@ -687,27 +821,31 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
void saveEditingCell(selectedCell);
|
||||
void saveEditingCell("stay");
|
||||
} else if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
setEditingCell(null);
|
||||
setSelectedCell({ rowKey: row.rowKey, cellKey: column.key });
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
} else if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
const idx = editableCells.findIndex(
|
||||
(cell) =>
|
||||
cell.rowKey === editingCell.rowKey && cell.cellKey === editingCell.cellKey
|
||||
);
|
||||
const next =
|
||||
idx >= 0
|
||||
? editableCells[
|
||||
event.shiftKey
|
||||
? Math.max(0, idx - 1)
|
||||
: Math.min(editableCells.length - 1, idx + 1)
|
||||
]
|
||||
: null;
|
||||
void saveEditingCell(next);
|
||||
void saveEditingCell(event.shiftKey ? "prev" : "next");
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!editingCell) {
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
const insideEditor =
|
||||
!!active && !!containerRef.current && containerRef.current.contains(active);
|
||||
if (!insideEditor) {
|
||||
setEditingCell(null);
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
formatValue(value)
|
||||
@@ -718,16 +856,28 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<td className="action-cell">
|
||||
{row.circuit && row.kind !== "device" ? (
|
||||
<>
|
||||
<button type="button" onClick={() => void handleAddManualDevice(row.circuit!, row.sectionId)}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={editingCell ? -1 : 0}
|
||||
onClick={() => void handleAddManualDevice(row.circuit!, row.sectionId)}
|
||||
>
|
||||
Add manual device
|
||||
</button>
|
||||
<button type="button" onClick={() => void handleDeleteCircuit(row.circuit!.id)}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={editingCell ? -1 : 0}
|
||||
onClick={() => void handleDeleteCircuit(row.circuit!.id)}
|
||||
>
|
||||
Delete circuit
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{row.device ? (
|
||||
<button type="button" onClick={() => void handleDeleteDevice(row.device!.id)}>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={editingCell ? -1 : 0}
|
||||
onClick={() => void handleDeleteDevice(row.device!.id)}
|
||||
>
|
||||
Delete device
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user