forked from jappel/leistungsbilanz-ts
Add cell copy/paste, multi-row edits and a context menu to the circuit grid
Ctrl+C/Ctrl+V copy and paste grid cells without opening an editor, using the document copy/paste events so it also works over plain HTTP in the LAN. Protection and cable summaries copy the complete circuit record; tab-separated blocks from Excel spread over visible rows and columns. With a Ctrl/Shift multi-selection, typing, pasting and the protection dialog apply to every selected row that can take the value, including device power and GZF; other cells are skipped and reported. All of these writes use the new circuit-list.bulk-update command. It carries complete circuit.update, circuit-device-row.update and circuit-protection.update commands, applies them with the single-command rules in one transaction and stores the reversed inverses, so each edit is one undo step. The single-command apply logic is extracted for reuse. A right-click menu (also context-menu key / Shift+F10) offers copy, paste, edit, protection, cable sizing, insert circuit below, add device and delete. The protection summary is now selected by click and opened by double click, Enter or F2. Existing numbers open in the inline editor with a decimal comma, so confirming an unchanged decimal no longer fails. Refs #26 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
07bc52a134
commit
17a3945813
18 changed files with 2548 additions and 391 deletions
|
|
@ -7,8 +7,11 @@ import {
|
|||
ArrowDown01,
|
||||
ArrowLeft,
|
||||
ArrowUp,
|
||||
BetweenHorizontalStart,
|
||||
Calculator,
|
||||
Check,
|
||||
ClipboardCopy,
|
||||
ClipboardPaste,
|
||||
Columns3,
|
||||
FilterX,
|
||||
FolderPlus,
|
||||
|
|
@ -22,6 +25,7 @@ import {
|
|||
Pencil,
|
||||
Plus,
|
||||
Redo2,
|
||||
Shield,
|
||||
ShieldPlus,
|
||||
TableProperties,
|
||||
Trash2,
|
||||
|
|
@ -148,9 +152,22 @@ import {
|
|||
updateCircuitDeviceRowById,
|
||||
updateCircuitProtectionCommand,
|
||||
updateCircuitGroupCommand,
|
||||
updateCircuitListCellsCommand,
|
||||
updateDistributionBoardComponentCommand,
|
||||
undoProjectCommand,
|
||||
} from "../utils/api";
|
||||
import {
|
||||
buildCellClipboard,
|
||||
formatCellEditText,
|
||||
gridDataRowTypes,
|
||||
isCircuitRecordCell,
|
||||
isCircuitRow,
|
||||
planCellWrites,
|
||||
resolveClipboardCells,
|
||||
resolvePasteWrites,
|
||||
type CellClipboard,
|
||||
type CellWrite,
|
||||
} from "../utils/circuit-grid-clipboard";
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeComponentDto,
|
||||
|
|
@ -173,6 +190,7 @@ import { DistributionBoardComponentModal } from "./distribution-board-component-
|
|||
import { GzfPickerModal } from "./gzf-picker-modal";
|
||||
import { CircuitGroupModal } from "./circuit-group-modal";
|
||||
import { CircuitProtectionModal } from "./circuit-protection-modal";
|
||||
import { GridContextMenu, type GridContextMenuEntry } from "./grid-context-menu";
|
||||
import { CableSizingModal } from "./cable-sizing-modal";
|
||||
import {
|
||||
circuitGroupCategoryLabels,
|
||||
|
|
@ -321,6 +339,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
// editingCell is mounted input session state (draft + mode + focus token).
|
||||
// It should only exist for currently editable grid cells.
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<
|
||||
{ x: number; y: number; rowKey: string; cellKey: CellKey } | null
|
||||
>(null);
|
||||
// Last cells copied in this editor. Pasting the same text again reuses the
|
||||
// structured values (complete protection/cable records).
|
||||
const cellClipboardRef = useRef<CellClipboard | null>(null);
|
||||
const [hasCellClipboard, setHasCellClipboard] = useState(false);
|
||||
const [gridNotice, setGridNotice] = useState<string | null>(null);
|
||||
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
// Synchronous re-entry guard: isSaving is React state and only reflects
|
||||
|
|
@ -904,7 +930,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
() =>
|
||||
visibleRows.flatMap((row) =>
|
||||
row.cells
|
||||
.filter((cell) => cell.editable)
|
||||
.filter((cell) => cell.editable || isCircuitRecordCell(row, cell.cellKey))
|
||||
.map((cell) => ({ rowKey: row.rowKey, cellKey: cell.cellKey as CellKey }))
|
||||
),
|
||||
[visibleRows]
|
||||
|
|
@ -917,7 +943,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
const map = new Map<string, CellKey[]>();
|
||||
for (const row of visibleRows) {
|
||||
const keys = row.cells
|
||||
.filter((cell) => cell.editable && visibleKeySet.has(cell.cellKey))
|
||||
.filter(
|
||||
(cell) =>
|
||||
(cell.editable || isCircuitRecordCell(row, cell.cellKey)) &&
|
||||
visibleKeySet.has(cell.cellKey)
|
||||
)
|
||||
.map((cell) => cell.cellKey as CellKey);
|
||||
if (keys.length) {
|
||||
map.set(row.rowKey, keys);
|
||||
|
|
@ -1520,6 +1550,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
});
|
||||
}
|
||||
|
||||
// Circuit rows of a multi-selection that contains the edited circuit.
|
||||
function getProtectionMultiSelectionRows(circuitId: string) {
|
||||
const circuitRowKey = visibleRows.find(
|
||||
(row) => row.circuit?.id === circuitId && isCircuitRow(row)
|
||||
)?.rowKey;
|
||||
const rows = circuitRowKey ? getMultiSelectionRows(circuitRowKey) : null;
|
||||
const circuitRows = rows?.filter((row) => isCircuitRow(row)) ?? [];
|
||||
return circuitRows.length > 1 ? circuitRows : null;
|
||||
}
|
||||
|
||||
async function handleSaveCircuitProtection(
|
||||
protection: CircuitTreeProtectionDeviceDto
|
||||
) {
|
||||
|
|
@ -1527,6 +1567,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
if (!circuit) {
|
||||
return;
|
||||
}
|
||||
const multiSelectionRows = getProtectionMultiSelectionRows(circuit.id);
|
||||
if (multiSelectionRows) {
|
||||
const { circuitId: _circuitId, ...protectionValue } =
|
||||
toCircuitProtectionSnapshot(circuit.id, protection);
|
||||
setProtectionEditorCircuit(null);
|
||||
await applyCellWrites(
|
||||
multiSelectionRows.map((row) => ({
|
||||
row,
|
||||
cellKey: "protectionSummary",
|
||||
value: { kind: "protection", text: "", protection: protectionValue },
|
||||
})),
|
||||
"Stromkreisschutz für mehrere Stromkreise bearbeiten"
|
||||
);
|
||||
return;
|
||||
}
|
||||
await runCommand({
|
||||
label: "Stromkreisschutz bearbeiten",
|
||||
redo: async () => {
|
||||
|
|
@ -1858,15 +1913,27 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
// Opens edit session for selected cell.
|
||||
// Enter/F2/double-click use selectExisting; type-to-edit uses replaceWithTypedChar.
|
||||
function startEdit(cell: SelectedCell, mode: StartEditMode, typedChar?: string) {
|
||||
const row = findRow(cell.rowKey);
|
||||
const visibleCell = findCell(cell.rowKey, cell.cellKey);
|
||||
// The protection summary is edited in its dialog (Enter/F2/double click).
|
||||
if (
|
||||
row?.circuit &&
|
||||
cell.cellKey === "protectionSummary" &&
|
||||
isCircuitRecordCell(row, cell.cellKey) &&
|
||||
mode !== "replaceWithTypedChar"
|
||||
) {
|
||||
setProtectionEditorCircuit(row.circuit);
|
||||
return;
|
||||
}
|
||||
if (!visibleCell || !visibleCell.editable) {
|
||||
return;
|
||||
}
|
||||
// Spreadsheet behavior: typing on a selected cell starts overwrite mode.
|
||||
// Existing numbers use the decimal comma the edit parser expects.
|
||||
const currentDisplay =
|
||||
mode === "replaceWithTypedChar" && cell.cellKey !== "phaseType"
|
||||
? typedChar ?? ""
|
||||
: String(visibleCell.editValue ?? visibleCell.value ?? "");
|
||||
: formatCellEditText(visibleCell.editValue ?? visibleCell.value);
|
||||
focusTokenRef.current += 1;
|
||||
setEditingCell({
|
||||
...cell,
|
||||
|
|
@ -2048,6 +2115,24 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
return buildSelectionIntent(selected);
|
||||
};
|
||||
|
||||
// With several selected rows the value goes to the same column of every
|
||||
// selected row that can take it, as one command and one undo step.
|
||||
const multiSelectionRows = getMultiSelectionRows(row.rowKey);
|
||||
if (multiSelectionRows) {
|
||||
setEditingCell(null);
|
||||
await applyCellWrites(
|
||||
multiSelectionRows.map((targetRow) => ({
|
||||
row: targetRow,
|
||||
cellKey: session.cellKey,
|
||||
value: { kind: "text", text: draft },
|
||||
})),
|
||||
"Mehrere Zeilen bearbeiten",
|
||||
{ allowEquipmentIdentifier: false },
|
||||
nextSelectionIntent
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Placeholder rows are not persisted entities; editing them materializes a real circuit first.
|
||||
if (row.rowType === "placeholder") {
|
||||
const command: HistoryCommand = {
|
||||
|
|
@ -2179,6 +2264,373 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
focusGridWithoutScroll();
|
||||
}
|
||||
|
||||
// Selected data rows in grid order while a multi-selection contains the
|
||||
// given row; null for a single-row selection.
|
||||
function getMultiSelectionRows(rowKey: string): VisibleGridRow[] | null {
|
||||
if (selectedRowKeys.length < 2 || !selectedRowKeys.includes(rowKey)) {
|
||||
return null;
|
||||
}
|
||||
const selected = new Set(selectedRowKeys);
|
||||
return visibleRows.filter(
|
||||
(row) => selected.has(row.rowKey) && gridDataRowTypes.has(row.rowType)
|
||||
);
|
||||
}
|
||||
|
||||
// Plans cell writes and stores them as one circuit-list.bulk-update.
|
||||
async function applyCellWrites(
|
||||
writes: CellWrite[],
|
||||
label: string,
|
||||
options: { allowEquipmentIdentifier?: boolean } = {},
|
||||
selectionIntent: () => SelectionIntent | null = () => null,
|
||||
extraSkippedCellCount = 0
|
||||
) {
|
||||
let plan: ReturnType<typeof planCellWrites>;
|
||||
try {
|
||||
plan = planCellWrites(writes, options);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
return;
|
||||
}
|
||||
const skipped = plan.skippedCellCount + extraSkippedCellCount;
|
||||
const skippedText =
|
||||
skipped > 0
|
||||
? `${skipped} Zelle(n) übersprungen (nicht bearbeitbar oder nicht passend).`
|
||||
: "";
|
||||
if (plan.updates.length === 0) {
|
||||
setGridNotice(
|
||||
skippedText || "Keine Änderung – die Werte sind bereits gesetzt."
|
||||
);
|
||||
return;
|
||||
}
|
||||
setGridNotice(
|
||||
`${plan.changedCellCount} Zelle(n) geändert.${skippedText ? ` ${skippedText}` : ""}`
|
||||
);
|
||||
await runCommand({
|
||||
label,
|
||||
redo: async () => {
|
||||
const result = await updateCircuitListCellsCommand(
|
||||
projectId,
|
||||
getExpectedProjectRevision(),
|
||||
plan.updates,
|
||||
label
|
||||
);
|
||||
applyProjectCommandResult(result);
|
||||
return selectionIntent();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Cells to copy: the selected cell, or its column for every selected row.
|
||||
function buildSelectionClipboard(): CellClipboard | null {
|
||||
if (!selectedCell) {
|
||||
return null;
|
||||
}
|
||||
const row = findRow(selectedCell.rowKey);
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
return buildCellClipboard(
|
||||
getMultiSelectionRows(row.rowKey) ?? [row],
|
||||
[selectedCell.cellKey]
|
||||
);
|
||||
}
|
||||
|
||||
function copySelectionToClipboard(clipboardData: DataTransfer | null) {
|
||||
const clipboard = buildSelectionClipboard();
|
||||
if (!clipboard) {
|
||||
return false;
|
||||
}
|
||||
cellClipboardRef.current = clipboard;
|
||||
setHasCellClipboard(true);
|
||||
clipboardData?.setData("text/plain", clipboard.text);
|
||||
const count = clipboard.cells.length;
|
||||
setGridNotice(
|
||||
count > 1 ? `${count} Zellen kopiert.` : "Zelle kopiert."
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function pasteClipboardText(text: string) {
|
||||
if (!selectedCell || isSaving || historyBusy) {
|
||||
return;
|
||||
}
|
||||
const cells = resolveClipboardCells(text, cellClipboardRef.current);
|
||||
if (cells.length === 0) {
|
||||
return;
|
||||
}
|
||||
const { writes, overflowCellCount } = resolvePasteWrites({
|
||||
cells,
|
||||
anchor: selectedCell,
|
||||
selectedRowKeys,
|
||||
rows: visibleRows,
|
||||
columnKeys: visibleColumns.map((column) => column.key),
|
||||
});
|
||||
await applyCellWrites(
|
||||
writes,
|
||||
"Zellen einfügen",
|
||||
{},
|
||||
() => buildSelectionIntent(selectedCell),
|
||||
overflowCellCount
|
||||
);
|
||||
}
|
||||
|
||||
// Menu paste has no clipboard event (browsers block reading it there), so
|
||||
// it reuses the cells last copied in this editor.
|
||||
function pasteFromCellClipboard() {
|
||||
const clipboard = cellClipboardRef.current;
|
||||
if (clipboard) {
|
||||
void pasteClipboardText(clipboard.text);
|
||||
}
|
||||
}
|
||||
|
||||
// execCommand("copy") also works over plain HTTP and runs the grid's copy
|
||||
// listener, which writes the system clipboard.
|
||||
function copyFromMenu() {
|
||||
focusGridWithoutScroll();
|
||||
let copied = false;
|
||||
try {
|
||||
copied = document.execCommand("copy");
|
||||
} catch {
|
||||
copied = false;
|
||||
}
|
||||
if (!copied) {
|
||||
copySelectionToClipboard(null);
|
||||
}
|
||||
}
|
||||
|
||||
function openContextMenu(row: VisibleGridRow, cellKey: CellKey, x: number, y: number) {
|
||||
if (editingCell) {
|
||||
setEditingCell(null);
|
||||
}
|
||||
if (selectedRowKeys.length > 1 && selectedRowKeys.includes(row.rowKey)) {
|
||||
setSelectedCell({ rowKey: row.rowKey, cellKey });
|
||||
} else {
|
||||
handleRowSelectionClick(row, cellKey);
|
||||
}
|
||||
setContextMenu({ x, y, rowKey: row.rowKey, cellKey });
|
||||
}
|
||||
|
||||
// Keyboard access (context-menu key or Shift+F10) opens the menu at the cell.
|
||||
function openContextMenuForSelectedCell() {
|
||||
if (!selectedCell) {
|
||||
return;
|
||||
}
|
||||
const row = findRow(selectedCell.rowKey);
|
||||
const element = containerRef.current?.querySelector(
|
||||
`td[data-row-key="${CSS.escape(selectedCell.rowKey)}"][data-cell-key="${CSS.escape(selectedCell.cellKey)}"]`
|
||||
);
|
||||
if (!row || !element) {
|
||||
return;
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
openContextMenu(row, selectedCell.cellKey, rect.left + 8, rect.bottom);
|
||||
}
|
||||
|
||||
function closeContextMenu(restoreFocus: boolean) {
|
||||
setContextMenu(null);
|
||||
if (restoreFocus) {
|
||||
focusGridWithoutScroll();
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-selection hint and the result of the last copy/paste/bulk edit.
|
||||
function renderGridStatus() {
|
||||
const selectionRows = selectedCell
|
||||
? getMultiSelectionRows(selectedCell.rowKey)
|
||||
: null;
|
||||
if (!selectionRows && !gridNotice) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="grid-status-line" role="status">
|
||||
{selectionRows ? (
|
||||
<span className="grid-status-selection">
|
||||
{selectionRows.length} Zeilen ausgewählt – Eingaben, Einfügen und
|
||||
Schutzgerät gelten für alle passenden Zeilen.
|
||||
</span>
|
||||
) : null}
|
||||
{gridNotice ? <span>{gridNotice}</span> : null}
|
||||
{gridNotice ? (
|
||||
<button type="button" onClick={() => setGridNotice(null)}>
|
||||
Ausblenden
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function buildContextMenuEntries(): GridContextMenuEntry[] {
|
||||
const row = contextMenu ? findRow(contextMenu.rowKey) : null;
|
||||
if (!contextMenu || !row) {
|
||||
return [];
|
||||
}
|
||||
const busy = isSaving || historyBusy;
|
||||
const { circuit, device } = row;
|
||||
const cell = row.cells.find((entry) => entry.cellKey === contextMenu.cellKey);
|
||||
const multiSelectionRows = getMultiSelectionRows(row.rowKey);
|
||||
const protectionRows = circuit
|
||||
? getProtectionMultiSelectionRows(circuit.id)
|
||||
: null;
|
||||
const feedsDownstreamBoard = (circuit?.downstreamBoardLoads?.length ?? 0) > 0;
|
||||
const entries: GridContextMenuEntry[] = [
|
||||
{
|
||||
kind: "action",
|
||||
label: multiSelectionRows
|
||||
? `${multiSelectionRows.length} Zellen kopieren`
|
||||
: "Kopieren",
|
||||
icon: ClipboardCopy,
|
||||
shortcut: "Strg+C",
|
||||
onSelect: copyFromMenu,
|
||||
},
|
||||
{
|
||||
kind: "action",
|
||||
label: multiSelectionRows
|
||||
? `In ${multiSelectionRows.length} Zeilen einfügen`
|
||||
: "Einfügen",
|
||||
icon: ClipboardPaste,
|
||||
shortcut: "Strg+V",
|
||||
disabled: !hasCellClipboard || busy,
|
||||
title: hasCellClipboard
|
||||
? undefined
|
||||
: "Zuerst Zellen dieser Stromkreisliste kopieren. Werte aus Excel mit Strg+V einfügen.",
|
||||
onSelect: pasteFromCellClipboard,
|
||||
},
|
||||
{ kind: "separator" },
|
||||
];
|
||||
if (cell?.editable) {
|
||||
entries.push({
|
||||
kind: "action",
|
||||
label: multiSelectionRows
|
||||
? `Bearbeiten (${multiSelectionRows.length} Zeilen)`
|
||||
: "Bearbeiten",
|
||||
icon: Pencil,
|
||||
shortcut: "Enter",
|
||||
disabled: busy,
|
||||
onSelect: () =>
|
||||
startEdit({ rowKey: row.rowKey, cellKey: contextMenu.cellKey }, "selectExisting"),
|
||||
});
|
||||
}
|
||||
if (circuit && isCircuitRow(row)) {
|
||||
entries.push(
|
||||
{
|
||||
kind: "action",
|
||||
label: protectionRows
|
||||
? `Schutzgerät für ${protectionRows.length} Stromkreise bearbeiten`
|
||||
: "Schutzgerät bearbeiten",
|
||||
icon: Shield,
|
||||
disabled: busy,
|
||||
onSelect: () => setProtectionEditorCircuit(circuit),
|
||||
},
|
||||
{
|
||||
kind: "action",
|
||||
label: "Leitungsberechnung",
|
||||
icon: Calculator,
|
||||
disabled: busy,
|
||||
onSelect: () => setCableSizingEditorCircuit(circuit),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (circuit) {
|
||||
entries.push(
|
||||
{ kind: "separator" },
|
||||
{
|
||||
kind: "action",
|
||||
label: "Stromkreis unterhalb einfügen",
|
||||
icon: BetweenHorizontalStart,
|
||||
disabled: busy,
|
||||
onSelect: () => void handleAddReserveCircuit(row.sectionId, circuit.id),
|
||||
},
|
||||
{
|
||||
kind: "action",
|
||||
label: "Manuelles Gerät hinzufügen",
|
||||
icon: ListPlus,
|
||||
disabled: busy,
|
||||
onSelect: () =>
|
||||
void handleAddManualDevice(circuit, row.sectionId, device?.id),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (device || (circuit && isCircuitRow(row))) {
|
||||
entries.push({ kind: "separator" });
|
||||
}
|
||||
if (device) {
|
||||
entries.push({
|
||||
kind: "action",
|
||||
label: `Gerät „${device.displayName || device.name}“ löschen`,
|
||||
icon: ListX,
|
||||
variant: "danger",
|
||||
disabled: busy,
|
||||
onSelect: () => void handleDeleteDevice(device.id),
|
||||
});
|
||||
}
|
||||
if (circuit && isCircuitRow(row)) {
|
||||
entries.push({
|
||||
kind: "action",
|
||||
label: `Stromkreis ${circuit.equipmentIdentifier} löschen`,
|
||||
icon: Trash2,
|
||||
variant: "danger",
|
||||
disabled: busy || feedsDownstreamBoard,
|
||||
title: feedsDownstreamBoard
|
||||
? "Speist eine Unterverteilung – zuerst deren Einspeisung ändern"
|
||||
: undefined,
|
||||
onSelect: () => void handleDeleteCircuit(circuit.id),
|
||||
});
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Latest handlers for the document-level clipboard listeners below.
|
||||
const gridClipboardHandlersRef = useRef<{
|
||||
copy: (event: ClipboardEvent) => void;
|
||||
paste: (event: ClipboardEvent) => void;
|
||||
} | null>(null);
|
||||
gridClipboardHandlersRef.current = {
|
||||
copy: (event) => {
|
||||
if (!isGridClipboardTarget()) {
|
||||
return;
|
||||
}
|
||||
if (copySelectionToClipboard(event.clipboardData)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
paste: (event) => {
|
||||
if (!isGridClipboardTarget()) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
void pasteClipboardText(event.clipboardData?.getData("text/plain") ?? "");
|
||||
},
|
||||
};
|
||||
|
||||
// Copy/paste act on grid cells only while the grid itself has focus, no
|
||||
// inline editor or dialog is open and no page text is selected.
|
||||
function isGridClipboardTarget() {
|
||||
const active = document.activeElement;
|
||||
return Boolean(
|
||||
selectedCell &&
|
||||
!editingCell &&
|
||||
!gzfPickerCell &&
|
||||
!protectionEditorCircuit &&
|
||||
active &&
|
||||
containerRef.current?.contains(active) &&
|
||||
!isGridEditorControlTarget(active) &&
|
||||
!window.getSelection()?.toString()
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const handleCopy = (event: ClipboardEvent) =>
|
||||
gridClipboardHandlersRef.current?.copy(event);
|
||||
const handlePaste = (event: ClipboardEvent) =>
|
||||
gridClipboardHandlersRef.current?.paste(event);
|
||||
document.addEventListener("copy", handleCopy);
|
||||
document.addEventListener("paste", handlePaste);
|
||||
return () => {
|
||||
document.removeEventListener("copy", handleCopy);
|
||||
document.removeEventListener("paste", handlePaste);
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function handleAddReserveCircuit(sectionId: string, afterCircuitId?: string) {
|
||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||
if (!section) {
|
||||
|
|
@ -3191,6 +3643,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
if (eventTarget.closest("button, input, select, textarea")) {
|
||||
return;
|
||||
}
|
||||
if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) {
|
||||
event.preventDefault();
|
||||
openContextMenuForSelectedCell();
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
if (selectedRowKeys.length > 0) {
|
||||
event.preventDefault();
|
||||
|
|
@ -3579,6 +4036,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
</div>
|
||||
) : null}
|
||||
{isSaving ? <div className="notice info">Wird gespeichert …</div> : null}
|
||||
{renderGridStatus()}
|
||||
{contextMenu ? (
|
||||
<GridContextMenu
|
||||
ariaLabel="Zellenaktionen"
|
||||
entries={buildContextMenuEntries()}
|
||||
onClose={closeContextMenu}
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
/>
|
||||
) : null}
|
||||
<div className="tree-editor-layout">
|
||||
{isRevitDrawerOpen ? (
|
||||
<aside className="project-device-sidebar project-device-drawer" id="revit-object-drawer">
|
||||
|
|
@ -3772,6 +4239,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
tabIndex={0}
|
||||
onFocus={handleContainerFocus}
|
||||
onKeyDown={handleContainerKeyDown}
|
||||
onContextMenu={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
event.preventDefault();
|
||||
openContextMenuForSelectedCell();
|
||||
}
|
||||
}}
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setSelectedRowKeys([]);
|
||||
|
|
@ -4665,14 +5138,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
row.circuit &&
|
||||
(duplicateEquipmentIdentifierCircuitIds.has(row.circuit.id) || hasDraftIdentifierConflict)
|
||||
);
|
||||
const isProtectionTrigger = Boolean(
|
||||
const isProtectionTrigger =
|
||||
column.key === "protectionSummary" &&
|
||||
row.circuit &&
|
||||
row.rowType !== "deviceRow"
|
||||
);
|
||||
isCircuitRecordCell(row, column.key);
|
||||
const isSelectableCell =
|
||||
cell.editable || isCircuitRecordCell(row, column.key);
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
data-cell-key={column.key}
|
||||
data-row-key={row.rowKey}
|
||||
className={`${column.numeric ? "num" : ""} ${cell.editable ? "cell-editable" : ""} ${isProtectionTrigger ? "cell-protection-trigger" : ""} ${isSelected ? "cell-selected" : ""} ${hasIdentifierConflict ? "cell-invalid" : ""} ${
|
||||
Boolean(row.device) && column.key === "displayName" && (row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
? "device-drag-handle"
|
||||
|
|
@ -4694,7 +5169,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
cell.lockedReason
|
||||
? cell.lockedReason
|
||||
: isProtectionTrigger
|
||||
? "Schutzgerät bearbeiten"
|
||||
? "Doppelklick oder Enter: Schutzgerät bearbeiten"
|
||||
: column.key === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
(row.rowType === "circuitCompact" ||
|
||||
|
|
@ -4770,11 +5245,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
if (isGridEditorControlTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (isProtectionTrigger) {
|
||||
setProtectionEditorCircuit(row.circuit!);
|
||||
return;
|
||||
}
|
||||
if (cell.editable) {
|
||||
if (isSelectableCell) {
|
||||
handleRowSelectionClick(row, column.key, {
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
|
|
@ -4786,10 +5257,17 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
if (isGridEditorControlTarget(event.target)) {
|
||||
return;
|
||||
}
|
||||
if (cell.editable) {
|
||||
if (isSelectableCell) {
|
||||
startEdit({ rowKey: row.rowKey, cellKey: column.key }, "selectExisting");
|
||||
}
|
||||
}}
|
||||
onContextMenu={(event) => {
|
||||
if (isGridEditorControlTarget(event.target) || !isSelectableCell) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
openContextMenu(row, column.key, event.clientX, event.clientY);
|
||||
}}
|
||||
>
|
||||
{isEditing && column.key === "phaseType" ? (
|
||||
<select
|
||||
|
|
@ -5054,7 +5532,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
</div>
|
||||
</div>
|
||||
<p className="todo-hint">
|
||||
Strg+Plus fügt unterhalb des ausgewählten Stromkreises oder Geräts ein. Mit „Rückgängig“ lässt sich das Einfügen zurücknehmen.
|
||||
Strg+Plus fügt unterhalb des ausgewählten Stromkreises oder Geräts ein. Rechtsklick öffnet die Zellenaktionen, Strg+C/Strg+V kopiert und fügt Zellen ein; mit Strg/Umschalt ausgewählte Zeilen werden gemeinsam bearbeitet.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue