From 567b719fa0fa0f590e92a22ef24ffd6cf9013e8f Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Mon, 4 May 2026 10:14:39 +0200 Subject: [PATCH] Bugfix editor --- .../components/circuit-tree-editor.tsx | 306 +++++++++++++----- 1 file changed, 228 insertions(+), 78 deletions(-) diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 9ca8e9a..fd396d8 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -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(null); @@ -238,7 +251,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [activeSectionId, setActiveSectionId] = useState(null); const [isSaving, setIsSaving] = useState(false); const [pendingFocus, setPendingFocus] = useState(null); + const pendingSelectionAfterReload = useRef(null); const containerRef = useRef(null); + const inputRef = useRef(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(); + 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) { 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
Loading circuit tree editor...
; } @@ -612,7 +739,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str return (
{isSaving ?
Saving...
: null} -
+
@@ -672,43 +805,48 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str );
setSelectedCell({ rowKey: row.rowKey, cellKey: column.key })} - onDoubleClick={() => startEdit({ rowKey: row.rowKey, cellKey: column.key })} - > + onClick={() => setSelectedCell({ rowKey: row.rowKey, cellKey: column.key })} + onClickCapture={() => requestAnimationFrame(() => containerRef.current?.focus())} + onDoubleClick={() => startEdit({ rowKey: row.rowKey, cellKey: column.key })} + > {editing ? ( - - setEditingCell((current) => - current ? { ...current, draft: event.target.value } : current - ) - } - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - void saveEditingCell(selectedCell); - } else if (event.key === "Escape") { - event.preventDefault(); - setEditingCell(null); - } 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); - } - }} - /> + + setEditingCell((current) => + current ? { ...current, draft: event.target.value } : current + ) + } + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + 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(); + 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) )} @@ -716,21 +854,33 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ); })} - {row.circuit && row.kind !== "device" ? ( - <> - - - - ) : null} - {row.device ? ( - - ) : null} + {row.circuit && row.kind !== "device" ? ( + <> + + + + ) : null} + {row.device ? ( + + ) : null}