Integrate compact editor actions and fix header units
This commit is contained in:
commit
203757b92c
5 changed files with 467 additions and 217 deletions
|
|
@ -1,6 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import { DragEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowDown01,
|
||||
ArrowUp,
|
||||
Check,
|
||||
Columns3,
|
||||
FilterX,
|
||||
FolderPlus,
|
||||
Funnel,
|
||||
ListOrdered,
|
||||
ListPlus,
|
||||
ListX,
|
||||
PackagePlus,
|
||||
PanelRightClose,
|
||||
PanelRightOpen,
|
||||
Pencil,
|
||||
Plus,
|
||||
Redo2,
|
||||
ShieldPlus,
|
||||
Trash2,
|
||||
Undo2,
|
||||
} from "lucide-react";
|
||||
import { IconButton, IconButtonSeparator } from "./icon-button";
|
||||
import {
|
||||
inferProjectDeviceSectionKey,
|
||||
isProjectDevicePlacementValid,
|
||||
|
|
@ -247,6 +270,24 @@ function createValuesFromEditPatch(
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Header labels break only between words: hyphens become non-breaking and a
|
||||
* trailing unit such as "[kW]" is always shown on its own second line, so
|
||||
* header heights stay uniform.
|
||||
*/
|
||||
function renderColumnHeaderLabel(label: string) {
|
||||
const match = /^(.*?)\s*(\[[^\]]+\])$/.exec(label);
|
||||
const name = (match ? match[1] : label).replace(/-/g, "‑");
|
||||
return match ? (
|
||||
<>
|
||||
{name}
|
||||
<span className="header-unit">{match[2]}</span>
|
||||
</>
|
||||
) : (
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
export function CircuitTreeEditor(props: { projectId: string; circuitListId: string }) {
|
||||
/*
|
||||
Circuit-tree editor invariant overview:
|
||||
|
|
@ -2996,32 +3037,37 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
return (
|
||||
<div className="tree-editor-shell">
|
||||
<div className="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleUndo()}
|
||||
<IconButton
|
||||
disabled={!historyState || historyState.undoDepth === 0 || historyBusy || isSaving}
|
||||
>
|
||||
Rückgängig
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRedo()}
|
||||
icon={Undo2}
|
||||
label="Rückgängig"
|
||||
onClick={() => void handleUndo()}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
disabled={!historyState || historyState.redoDepth === 0 || historyBusy || isSaving}
|
||||
>
|
||||
Wiederholen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSortAndFilters}
|
||||
disabled={!Boolean(sortState) && !hasActiveFilters}
|
||||
>
|
||||
Sortierung/Filter zurücksetzen
|
||||
</button>
|
||||
<button type="button" onClick={toggleColumnSettingsMenu} aria-expanded={isColumnMenuOpen}>
|
||||
Spalten
|
||||
</button>
|
||||
{renderColumnSettingsMenu("col-empty")}
|
||||
</div>
|
||||
icon={Redo2}
|
||||
label="Wiederholen"
|
||||
onClick={() => void handleRedo()}
|
||||
showLabel
|
||||
/>
|
||||
<IconButtonSeparator />
|
||||
<IconButton
|
||||
ariaExpanded={isColumnMenuOpen}
|
||||
icon={Columns3}
|
||||
label="Spalten"
|
||||
onClick={toggleColumnSettingsMenu}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
disabled={!Boolean(sortState) && !hasActiveFilters}
|
||||
icon={FilterX}
|
||||
label="Sortierung/Filter zurücksetzen"
|
||||
onClick={clearSortAndFilters}
|
||||
showLabel
|
||||
/>
|
||||
{renderColumnSettingsMenu("col-empty")}
|
||||
</div>
|
||||
{renderActiveViewSummary()}
|
||||
{renderDistributionPowerSummary()}
|
||||
{error ? (
|
||||
|
|
@ -3141,82 +3187,90 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
/>
|
||||
) : null}
|
||||
<div className="editor-toolbar">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleUndo()}
|
||||
<IconButton
|
||||
disabled={!historyState || historyState.undoDepth === 0 || historyBusy || isSaving}
|
||||
icon={Undo2}
|
||||
label="Rückgängig"
|
||||
onClick={() => void handleUndo()}
|
||||
showLabel
|
||||
title={
|
||||
historyState?.undoDepth
|
||||
? `${historyState.undoDepth} Änderung(en) können rückgängig gemacht werden.`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Rückgängig
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRedo()}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={!historyState || historyState.redoDepth === 0 || historyBusy || isSaving}
|
||||
icon={Redo2}
|
||||
label="Wiederholen"
|
||||
onClick={() => void handleRedo()}
|
||||
showLabel
|
||||
title={
|
||||
historyState?.redoDepth
|
||||
? `${historyState.redoDepth} Änderung(en) können wiederholt werden.`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Wiederholen
|
||||
</button>
|
||||
/>
|
||||
<IconButtonSeparator />
|
||||
{isSortedView ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleApplySortedOrder()}
|
||||
<IconButton
|
||||
disabled={hasActiveFilters || isSaving || historyBusy}
|
||||
icon={Check}
|
||||
label="Sortierung übernehmen"
|
||||
onClick={() => void handleApplySortedOrder()}
|
||||
showLabel
|
||||
title={hasActiveFilters ? "Vor dem Übernehmen der Sortierung zuerst die Filter zurücksetzen." : undefined}
|
||||
>
|
||||
Sortierung übernehmen
|
||||
</button>
|
||||
/>
|
||||
) : null}
|
||||
<button type="button" onClick={toggleColumnSettingsMenu} aria-expanded={isColumnMenuOpen}>
|
||||
Spalten
|
||||
</button>
|
||||
<button
|
||||
aria-controls="project-device-drawer"
|
||||
aria-expanded={isProjectDeviceDrawerOpen}
|
||||
className="project-device-drawer-toggle"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setIsProjectDeviceDrawerOpen((current) => !current)
|
||||
}
|
||||
>
|
||||
{isProjectDeviceDrawerOpen
|
||||
? "Projektgeräte schließen"
|
||||
: `Projektgeräte öffnen (${projectDevices.length})`}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<IconButton
|
||||
ariaExpanded={isColumnMenuOpen}
|
||||
icon={Columns3}
|
||||
label="Spalten"
|
||||
onClick={toggleColumnSettingsMenu}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
disabled={!isSortedView && !hasActiveFilters}
|
||||
icon={FilterX}
|
||||
label="Sortierung/Filter zurücksetzen"
|
||||
onClick={clearSortAndFilters}
|
||||
showLabel
|
||||
/>
|
||||
<IconButtonSeparator />
|
||||
<IconButton
|
||||
disabled={isSaving || historyBusy}
|
||||
icon={FolderPlus}
|
||||
label="Stromkreisgruppe hinzufügen"
|
||||
onClick={() => setCircuitGroupEditorIntent({ kind: "create" })}
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
disabled={isSaving || historyBusy}
|
||||
icon={PackagePlus}
|
||||
label="Verteilergerät hinzufügen"
|
||||
onClick={() =>
|
||||
setComponentEditorIntent({
|
||||
kind: "create",
|
||||
role: "auxiliary",
|
||||
})
|
||||
}
|
||||
>
|
||||
Verteilergerät hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSaving || historyBusy}
|
||||
onClick={() => setCircuitGroupEditorIntent({ kind: "create" })}
|
||||
>
|
||||
Stromkreisgruppe hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearSortAndFilters}
|
||||
disabled={!isSortedView && !hasActiveFilters}
|
||||
>
|
||||
Sortierung/Filter zurücksetzen
|
||||
</button>
|
||||
showLabel
|
||||
/>
|
||||
<IconButton
|
||||
ariaControls="project-device-drawer"
|
||||
ariaExpanded={isProjectDeviceDrawerOpen}
|
||||
className="project-device-drawer-toggle"
|
||||
icon={isProjectDeviceDrawerOpen ? PanelRightClose : PanelRightOpen}
|
||||
label={
|
||||
isProjectDeviceDrawerOpen
|
||||
? "Projektgeräte schließen"
|
||||
: `Projektgeräte öffnen (${projectDevices.length})`
|
||||
}
|
||||
onClick={() =>
|
||||
setIsProjectDeviceDrawerOpen((current) => !current)
|
||||
}
|
||||
showLabel
|
||||
/>
|
||||
{renderColumnSettingsMenu("col")}
|
||||
</div>
|
||||
{renderActiveViewSummary()}
|
||||
|
|
@ -3406,7 +3460,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
});
|
||||
}}
|
||||
>
|
||||
<span>{column.label}</span>
|
||||
<span className="header-label">
|
||||
{renderColumnHeaderLabel(column.label)}
|
||||
</span>
|
||||
{sortState?.key === column.key ? (
|
||||
<span className="sort-indicator" aria-label={sortState.direction === "asc" ? "aufsteigend" : "absteigend"}>
|
||||
{sortState.direction === "asc" ? "↑" : "↓"}
|
||||
|
|
@ -3417,6 +3473,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
type="button"
|
||||
className={`header-filter-btn ${(columnFilters[column.key]?.length ?? 0) > 0 ? "active" : ""}`}
|
||||
onClick={() => toggleColumnFilterMenu(column.key)}
|
||||
aria-label={`${getFullColumnLabel(column)} filtern`}
|
||||
aria-pressed={(columnFilters[column.key]?.length ?? 0) > 0}
|
||||
title={
|
||||
(columnFilters[column.key]?.length ?? 0) > 0
|
||||
|
|
@ -3424,7 +3481,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
: `${getFullColumnLabel(column)} filtern`
|
||||
}
|
||||
>
|
||||
Filtern{(columnFilters[column.key]?.length ?? 0) > 0 ? ` (${columnFilters[column.key]?.length})` : ""}
|
||||
<Funnel aria-hidden="true" size={13} strokeWidth={2.2} />
|
||||
{(columnFilters[column.key]?.length ?? 0) > 0 ? (
|
||||
<span className="header-filter-count">{columnFilters[column.key]?.length}</span>
|
||||
) : null}
|
||||
</button>
|
||||
</div>
|
||||
{openFilterColumn === column.key ? (
|
||||
|
|
@ -3718,105 +3778,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
</span>
|
||||
</div>
|
||||
<div className="section-actions">
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={!section.category || !section.groupNumber}
|
||||
onClick={() =>
|
||||
setCircuitGroupEditorIntent({
|
||||
kind: "edit",
|
||||
section,
|
||||
})
|
||||
}
|
||||
>
|
||||
Gruppe bearbeiten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={
|
||||
isSaving ||
|
||||
!buildCircuitGroupReorderAssignments(
|
||||
data.sections,
|
||||
section.id,
|
||||
-1
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
void handleMoveCircuitGroup(section.id, -1)
|
||||
}
|
||||
title="Innerhalb derselben Kategorie nach oben verschieben"
|
||||
>
|
||||
Gruppe nach oben
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={
|
||||
isSaving ||
|
||||
!buildCircuitGroupReorderAssignments(
|
||||
data.sections,
|
||||
section.id,
|
||||
1
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
void handleMoveCircuitGroup(section.id, 1)
|
||||
}
|
||||
title="Innerhalb derselben Kategorie nach unten verschieben"
|
||||
>
|
||||
Gruppe nach unten
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={
|
||||
isSaving ||
|
||||
hasActiveSortOrFilter ||
|
||||
!section.category ||
|
||||
!canRenumberCircuitGroups(
|
||||
data.sections,
|
||||
section.category
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
section.category
|
||||
? void handleRenumberCircuitGroups(
|
||||
section.category
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
title={
|
||||
hasActiveSortOrFilter
|
||||
? "Vor der Gruppenneunummerierung Sortierung und Filter zurücksetzen."
|
||||
: "Alle Gruppen dieser Kategorie gemäß ihrer Reihenfolge neu nummerieren"
|
||||
}
|
||||
>
|
||||
Gruppen-BMK neu nummerieren
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
title={
|
||||
canDeleteCircuitGroup(section)
|
||||
? "Leere Stromkreisgruppe entfernen"
|
||||
: "Befüllte Stromkreisgruppe mit vollständiger Warnung entfernen"
|
||||
}
|
||||
onClick={() =>
|
||||
void handleDeleteCircuitGroup(section)
|
||||
}
|
||||
>
|
||||
Gruppe entfernen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
icon={Plus}
|
||||
label="Stromkreis hinzufügen"
|
||||
onClick={() => void handleAddReserveCircuit(section.id)}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={
|
||||
hasUpstreamProtection ||
|
||||
!section.category ||
|
||||
!section.groupNumber
|
||||
}
|
||||
icon={ShieldPlus}
|
||||
label="Vorsicherung hinzufügen"
|
||||
onClick={() =>
|
||||
setComponentEditorIntent({
|
||||
kind: "create",
|
||||
|
|
@ -3829,17 +3805,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Vorsicherung hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="icon-button-text">VS</span>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
disabled={
|
||||
hasGroupRcd ||
|
||||
!section.category ||
|
||||
!section.groupNumber
|
||||
}
|
||||
icon={ShieldPlus}
|
||||
label="Gruppen-FI hinzufügen"
|
||||
onClick={() =>
|
||||
setComponentEditorIntent({
|
||||
kind: "create",
|
||||
|
|
@ -3852,26 +3829,112 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
Gruppen-FI hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
disabled={isSaving}
|
||||
onClick={() => void handleAddReserveCircuit(section.id)}
|
||||
>
|
||||
Stromkreis hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
<span className="icon-button-text">FI</span>
|
||||
</IconButton>
|
||||
<IconButtonSeparator />
|
||||
<IconButton
|
||||
disabled={!section.category || !section.groupNumber}
|
||||
icon={Pencil}
|
||||
label="Gruppe bearbeiten"
|
||||
onClick={() =>
|
||||
setCircuitGroupEditorIntent({
|
||||
kind: "edit",
|
||||
section,
|
||||
})
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={
|
||||
isSaving ||
|
||||
!buildCircuitGroupReorderAssignments(
|
||||
data.sections,
|
||||
section.id,
|
||||
-1
|
||||
)
|
||||
}
|
||||
icon={ArrowUp}
|
||||
label="Gruppe nach oben"
|
||||
onClick={() =>
|
||||
void handleMoveCircuitGroup(section.id, -1)
|
||||
}
|
||||
tabIndex={-1}
|
||||
title="Gruppe nach oben (innerhalb derselben Kategorie)"
|
||||
/>
|
||||
<IconButton
|
||||
disabled={
|
||||
isSaving ||
|
||||
!buildCircuitGroupReorderAssignments(
|
||||
data.sections,
|
||||
section.id,
|
||||
1
|
||||
)
|
||||
}
|
||||
icon={ArrowDown}
|
||||
label="Gruppe nach unten"
|
||||
onClick={() =>
|
||||
void handleMoveCircuitGroup(section.id, 1)
|
||||
}
|
||||
tabIndex={-1}
|
||||
title="Gruppe nach unten (innerhalb derselben Kategorie)"
|
||||
/>
|
||||
<IconButtonSeparator />
|
||||
<IconButton
|
||||
disabled={hasActiveSortOrFilter || isSaving}
|
||||
icon={ArrowDown01}
|
||||
label="Bereich neu nummerieren"
|
||||
onClick={() => void handleRenumberSection(section.id)}
|
||||
title={hasActiveSortOrFilter ? "Vor der Neunummerierung Sortierung und Filter zurücksetzen." : undefined}
|
||||
>
|
||||
Bereich neu nummerieren
|
||||
</button>
|
||||
tabIndex={-1}
|
||||
title={
|
||||
hasActiveSortOrFilter
|
||||
? "Vor der Neunummerierung Sortierung und Filter zurücksetzen."
|
||||
: "Stromkreise dieser Gruppe neu nummerieren"
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={
|
||||
isSaving ||
|
||||
hasActiveSortOrFilter ||
|
||||
!section.category ||
|
||||
!canRenumberCircuitGroups(
|
||||
data.sections,
|
||||
section.category
|
||||
)
|
||||
}
|
||||
icon={ListOrdered}
|
||||
label="Gruppen-BMK neu nummerieren"
|
||||
onClick={() =>
|
||||
section.category
|
||||
? void handleRenumberCircuitGroups(
|
||||
section.category
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
tabIndex={-1}
|
||||
title={
|
||||
hasActiveSortOrFilter
|
||||
? "Vor der Gruppenneunummerierung Sortierung und Filter zurücksetzen."
|
||||
: "Gruppen-BMK: alle Gruppen dieser Kategorie gemäß ihrer Reihenfolge neu nummerieren"
|
||||
}
|
||||
/>
|
||||
<IconButtonSeparator />
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={Trash2}
|
||||
label="Gruppe entfernen"
|
||||
onClick={() =>
|
||||
void handleDeleteCircuitGroup(section)
|
||||
}
|
||||
tabIndex={-1}
|
||||
title={
|
||||
canDeleteCircuitGroup(section)
|
||||
? "Leere Stromkreisgruppe entfernen"
|
||||
: "Befüllte Stromkreisgruppe entfernen (mit Warnung)"
|
||||
}
|
||||
variant="danger"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? (
|
||||
|
|
@ -4468,28 +4531,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
|||
>
|
||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={ListPlus}
|
||||
label="Manuelles Gerät hinzufügen"
|
||||
onClick={() => void handleAddManualDevice(row.circuit!, row.sectionId)}
|
||||
>
|
||||
Manuelles Gerät hinzufügen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={Trash2}
|
||||
label="Stromkreis löschen"
|
||||
onClick={() => void handleDeleteCircuit(row.circuit!.id)}
|
||||
>
|
||||
Stromkreis löschen
|
||||
</button>
|
||||
tabIndex={-1}
|
||||
variant="danger"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{row.device ? (
|
||||
<button type="button" tabIndex={-1} disabled={isSaving} onClick={() => void handleDeleteDevice(row.device!.id)}>
|
||||
Gerät löschen
|
||||
</button>
|
||||
<IconButton
|
||||
disabled={isSaving}
|
||||
icon={ListX}
|
||||
label="Gerät löschen"
|
||||
onClick={() => void handleDeleteDevice(row.device!.id)}
|
||||
tabIndex={-1}
|
||||
variant="danger"
|
||||
/>
|
||||
) : null}
|
||||
{dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue