Merge remote-tracking branch 'origin/main' into feature/cable-sizing
This commit is contained in:
commit
598003554a
136 changed files with 27182 additions and 1669 deletions
|
|
@ -21,6 +21,7 @@ import {
|
|||
Plus,
|
||||
Redo2,
|
||||
ShieldPlus,
|
||||
TableProperties,
|
||||
Trash2,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
|
|
@ -100,6 +101,7 @@ import {
|
|||
type MutableDistributionBoardComponentRole,
|
||||
} from "../utils/distribution-board-component-editing";
|
||||
import { loadCircuitEditorSnapshot } from "../utils/circuit-editor-history";
|
||||
import { createClientId } from "../utils/client-id";
|
||||
import type {
|
||||
CellKey,
|
||||
ColumnDef,
|
||||
|
|
@ -159,6 +161,7 @@ import type {
|
|||
CircuitSnapshot,
|
||||
} from "../../domain/models/circuit-structure-project-command.model";
|
||||
import { DistributionBoardComponentModal } from "./distribution-board-component-modal";
|
||||
import { GzfPickerModal } from "./gzf-picker-modal";
|
||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||
import { CableSizingModal } from "./cable-sizing-modal";
|
||||
|
|
@ -277,7 +280,7 @@ function createValuesFromEditPatch(
|
|||
* header heights stay uniform.
|
||||
*/
|
||||
function renderColumnHeaderLabel(label: string) {
|
||||
const match = /^(.*?)s*([[^]]+])$/.exec(label);
|
||||
const match = /^(.*?)\s*(\[[^\]]+\])$/.exec(label);
|
||||
const name = (match ? match[1] : label).replace(/-/g, "‑");
|
||||
return match ? (
|
||||
<>
|
||||
|
|
@ -319,6 +322,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
useState<StructureComponentEditorIntent | null>(null);
|
||||
const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] =
|
||||
useState<CircuitGroupEditorIntent | null>(null);
|
||||
// GZF cell the AMEV picker writes to; null while the picker is closed.
|
||||
const [gzfPickerCell, setGzfPickerCell] = useState<SelectedCell | null>(null);
|
||||
const [protectionEditorCircuit, setProtectionEditorCircuit] =
|
||||
useState<CircuitTreeCircuitDto | null>(null);
|
||||
const [cableSizingEditorCircuit, setCableSizingEditorCircuit] =
|
||||
|
|
@ -1022,7 +1027,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
defaultSortOrder: number
|
||||
) {
|
||||
return buildCircuitDeviceRowInsertSnapshot({
|
||||
id: crypto.randomUUID(),
|
||||
id: createClientId(),
|
||||
circuitId,
|
||||
values,
|
||||
defaultSortOrder,
|
||||
|
|
@ -1046,7 +1051,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
),
|
||||
data
|
||||
);
|
||||
const circuitId = crypto.randomUUID();
|
||||
const circuitId = createClientId();
|
||||
const deviceRows = deviceRowValues.map((row, index) =>
|
||||
createDeviceRowSnapshot(circuitId, row, (index + 1) * 10)
|
||||
);
|
||||
|
|
@ -1214,7 +1219,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
(section) => section.id === intent.sectionId
|
||||
)?.components ?? [];
|
||||
const snapshot = buildDistributionBoardComponentSnapshot({
|
||||
id: crypto.randomUUID(),
|
||||
id: createClientId(),
|
||||
circuitListId,
|
||||
role: intent.role,
|
||||
sectionId: intent.sectionId,
|
||||
|
|
@ -1279,7 +1284,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
buildNewCircuitGroupSnapshot({
|
||||
id: crypto.randomUUID(),
|
||||
id: createClientId(),
|
||||
circuitListId,
|
||||
category: values.category,
|
||||
displayName: values.displayName,
|
||||
|
|
@ -1899,22 +1904,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
|
||||
// Commits the active editor input through command history so edits remain undoable.
|
||||
// `target` commits a value for a cell without an open inline editor
|
||||
// (AMEV GZF picker); the regular edit path passes no target.
|
||||
async function commitEdit(
|
||||
direction: SaveDirection = "stay",
|
||||
draftOverride?: string
|
||||
draftOverride?: string,
|
||||
target?: SelectedCell
|
||||
) {
|
||||
if (!editingCell) {
|
||||
const session = target ?? editingCell;
|
||||
if (!session) {
|
||||
return;
|
||||
}
|
||||
const row = findRow(editingCell.rowKey);
|
||||
const cell = row?.cells.find((entry) => entry.cellKey === editingCell.cellKey);
|
||||
const row = findRow(session.rowKey);
|
||||
const cell = row?.cells.find((entry) => entry.cellKey === session.cellKey);
|
||||
if (!row || !cell || !cell.editable) {
|
||||
setEditingCell(null);
|
||||
return;
|
||||
}
|
||||
const draft = draftOverride ?? editingCell.draft;
|
||||
const draft = draftOverride ?? editingCell?.draft ?? "";
|
||||
if (
|
||||
editingCell.cellKey === "equipmentIdentifier" &&
|
||||
session.cellKey === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
hasEquipmentIdentifierConflict(allCircuits, row.circuit.id, draft)
|
||||
) {
|
||||
|
|
@ -1922,7 +1931,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
return;
|
||||
}
|
||||
|
||||
let targetSelection: SelectedCell = { rowKey: editingCell.rowKey, cellKey: editingCell.cellKey };
|
||||
let targetSelection: SelectedCell = { rowKey: session.rowKey, cellKey: session.cellKey };
|
||||
const nextSelectionIntent = () => {
|
||||
let selected = targetSelection;
|
||||
if (direction !== "stay") {
|
||||
|
|
@ -1945,7 +1954,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
redo: async () => {
|
||||
const selection = await createFromPlaceholder(
|
||||
row.sectionId,
|
||||
editingCell.cellKey,
|
||||
session.cellKey,
|
||||
draft
|
||||
);
|
||||
targetSelection = selection;
|
||||
|
|
@ -1962,7 +1971,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const command: HistoryCommand = {
|
||||
label: "Stromkreisfeld bearbeiten",
|
||||
redo: async () => {
|
||||
await patchCircuit(row.circuit!.id, editingCell.cellKey, next);
|
||||
await patchCircuit(row.circuit!.id, session.cellKey, next);
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
};
|
||||
|
|
@ -1976,7 +1985,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const command: HistoryCommand = {
|
||||
label: "Gerätefeld bearbeiten",
|
||||
redo: async () => {
|
||||
await patchDeviceRow(row.device!.id, editingCell.cellKey, next);
|
||||
await patchDeviceRow(row.device!.id, session.cellKey, next);
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
};
|
||||
|
|
@ -1991,7 +2000,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
label: "Gerätezeile im Reservestromkreis erstellen",
|
||||
redo: async () => {
|
||||
const editValues = createValuesFromEditPatch(
|
||||
buildDeviceRowEditPatch(editingCell.cellKey, next)
|
||||
buildDeviceRowEditPatch(session.cellKey, next)
|
||||
);
|
||||
const snapshot = createDeviceRowSnapshot(
|
||||
row.circuit!.id,
|
||||
|
|
@ -2011,7 +2020,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
snapshot,
|
||||
"Gerätezeile im Reservestromkreis erstellen"
|
||||
);
|
||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: editingCell.cellKey };
|
||||
targetSelection = { rowKey: `circuitCompact:${row.circuit!.id}`, cellKey: session.cellKey };
|
||||
return nextSelectionIntent();
|
||||
},
|
||||
};
|
||||
|
|
@ -2020,6 +2029,45 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
}
|
||||
}
|
||||
|
||||
function isGzfPickerCell(target: SelectedCell | null): target is SelectedCell {
|
||||
return Boolean(
|
||||
target &&
|
||||
target.cellKey === "simultaneityFactor" &&
|
||||
findCell(target.rowKey, target.cellKey)?.editable
|
||||
);
|
||||
}
|
||||
|
||||
// Opens the AMEV simultaneity-factor table for one GZF cell. A running
|
||||
// inline edit is discarded because the picked value replaces it.
|
||||
function openGzfPicker(target: SelectedCell) {
|
||||
if (isSaving || historyBusy || !isGzfPickerCell(target)) {
|
||||
return;
|
||||
}
|
||||
setEditingCell(null);
|
||||
setSelectedCell(target);
|
||||
setGzfPickerCell(target);
|
||||
}
|
||||
|
||||
function closeGzfPicker() {
|
||||
setGzfPickerCell(null);
|
||||
focusGridWithoutScroll();
|
||||
}
|
||||
|
||||
// Commits the picked mean value like a typed edit (undoable command).
|
||||
function handleGzfPicked(value: number) {
|
||||
const target = gzfPickerCell;
|
||||
setGzfPickerCell(null);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
// parseNumeric accepts only the German decimal comma.
|
||||
const draft = value.toLocaleString("de-DE", {
|
||||
maximumFractionDigits: 2,
|
||||
useGrouping: false,
|
||||
});
|
||||
void commitEdit("stay", draft, target);
|
||||
}
|
||||
|
||||
// Cancels current edit session and restores grid keyboard focus.
|
||||
function cancelEdit() {
|
||||
if (!editingCell) {
|
||||
|
|
@ -2963,6 +3011,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
// - printable key starts overwrite edit (type-to-edit)
|
||||
// - Arrow/Tab move selectedCell when not editing
|
||||
function handleContainerKeyDown(event: KeyboardEvent<HTMLDivElement>) {
|
||||
// The picker modal sits outside the grid; keep grid keys from acting behind it.
|
||||
if (gzfPickerCell) {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeGzfPicker();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (editingCell) {
|
||||
if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
|
|
@ -3030,6 +3086,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
moveHorizontal(-1);
|
||||
return;
|
||||
}
|
||||
if (event.altKey && event.key === "ArrowDown" && isGzfPickerCell(selectedCell)) {
|
||||
event.preventDefault();
|
||||
openGzfPicker(selectedCell);
|
||||
return;
|
||||
}
|
||||
if (event.key === "ArrowDown") {
|
||||
event.preventDefault();
|
||||
moveVertical(1);
|
||||
|
|
@ -3197,6 +3258,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
onSave={handleSaveCircuitGroup}
|
||||
/>
|
||||
) : null}
|
||||
{gzfPickerCell ? (
|
||||
<GzfPickerModal onClose={closeGzfPicker} onSelect={handleGzfPicked} />
|
||||
) : null}
|
||||
{protectionEditorCircuit ? (
|
||||
<CircuitProtectionModal
|
||||
equipmentIdentifier={
|
||||
|
|
@ -4530,11 +4594,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
} else if (event.key === "Tab") {
|
||||
event.preventDefault();
|
||||
void commitEdit(event.shiftKey ? "prev" : "next");
|
||||
} else if (
|
||||
event.altKey &&
|
||||
event.key === "ArrowDown" &&
|
||||
column.key === "simultaneityFactor"
|
||||
) {
|
||||
event.preventDefault();
|
||||
openGzfPicker({ rowKey: row.rowKey, cellKey: column.key });
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
requestAnimationFrame(() => {
|
||||
const active = document.activeElement as HTMLElement | null;
|
||||
if (active?.closest('[role="dialog"][aria-modal="true"]')) {
|
||||
return;
|
||||
}
|
||||
if (!active || !containerRef.current?.contains(active)) {
|
||||
setEditingCell(null);
|
||||
focusGridWithoutScroll();
|
||||
|
|
@ -4542,6 +4616,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
});
|
||||
}}
|
||||
/>
|
||||
) : column.key === "simultaneityFactor" &&
|
||||
cell.editable &&
|
||||
isSelected ? (
|
||||
<>
|
||||
<button
|
||||
aria-label="Gleichzeitigkeitsfaktor nach AMEV wählen"
|
||||
className="cell-picker-trigger"
|
||||
disabled={isSaving || historyBusy}
|
||||
tabIndex={-1}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
openGzfPicker({ rowKey: row.rowKey, cellKey: column.key });
|
||||
}}
|
||||
onDoubleClick={(event) => event.stopPropagation()}
|
||||
title="Gleichzeitigkeitsfaktor nach AMEV wählen (Alt+↓)"
|
||||
type="button"
|
||||
>
|
||||
<TableProperties aria-hidden="true" size={13} />
|
||||
</button>
|
||||
{formatCellValue(cell.value, column.key)}
|
||||
</>
|
||||
) : (
|
||||
formatCellValue(cell.value, column.key)
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue