Render protected circuit structure
This commit is contained in:
@@ -657,6 +657,40 @@ body {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-row td {
|
||||
padding: 0.38rem 0.6rem;
|
||||
border-bottom-color: #d8dee9;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-row.headerComponent td {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-row.groupComponent td {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-row.footerComponent td {
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-content {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-content strong {
|
||||
flex: 0 0 auto;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.tree-grid .structure-component-protection {
|
||||
color: #475569;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.tree-grid .section-drop-cell {
|
||||
padding: 0.45rem 0.6rem;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "../../domain/services/project-voltage.service";
|
||||
import {
|
||||
getInsertionSortOrder,
|
||||
isGridInsertionRowType,
|
||||
resolveGridInsertionIntent,
|
||||
} from "../utils/circuit-grid-insertion";
|
||||
import {
|
||||
@@ -53,7 +54,7 @@ import type {
|
||||
RowType,
|
||||
} from "../utils/circuit-grid-model";
|
||||
import {
|
||||
buildVisibleGridRows,
|
||||
buildVisibleGridRowsWithStructure,
|
||||
filterAndSortCircuitSections,
|
||||
getDistinctFilterValues,
|
||||
} from "../utils/circuit-grid-projection";
|
||||
@@ -386,8 +387,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
// visibleRows is the single normalized grid used by render + navigation + editability.
|
||||
// This avoids selected/rendered/editable state drifting apart after filters/sorts/reloads.
|
||||
const visibleRows = useMemo(() => {
|
||||
return buildVisibleGridRows(filteredSortedSections);
|
||||
}, [filteredSortedSections]);
|
||||
return buildVisibleGridRowsWithStructure(filteredSortedSections, {
|
||||
headerComponents: data?.headerComponents ?? [],
|
||||
footerComponents: data?.footerComponents ?? [],
|
||||
});
|
||||
}, [data, filteredSortedSections]);
|
||||
|
||||
function clearColumnFilter(key: CellKey) {
|
||||
setColumnFilters((current) => {
|
||||
@@ -1660,7 +1664,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const row = selectedCell ? findRow(selectedCell.rowKey) : null;
|
||||
const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey);
|
||||
const selection =
|
||||
row && cell && row.rowType !== "section"
|
||||
row && cell && isGridInsertionRowType(row.rowType)
|
||||
? {
|
||||
rowType: row.rowType,
|
||||
cellKind: cell.kind,
|
||||
@@ -2351,7 +2355,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const row = selectedCell ? findRow(selectedCell.rowKey) : null;
|
||||
const cell = row?.cells.find((entry) => entry.cellKey === selectedCell?.cellKey);
|
||||
const intent = resolveGridDeleteIntent(
|
||||
row && cell && row.rowType !== "section"
|
||||
row && cell && isGridInsertionRowType(row.rowType)
|
||||
? {
|
||||
rowType: row.rowType,
|
||||
cellKind: cell.kind,
|
||||
@@ -2986,6 +2990,54 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
</thead>
|
||||
<tbody>
|
||||
{visibleRows.map((row) => {
|
||||
if (
|
||||
row.rowType === "headerComponent" ||
|
||||
row.rowType === "groupComponent" ||
|
||||
row.rowType === "footerComponent"
|
||||
) {
|
||||
const component = row.component!;
|
||||
const protection = component.protectionDevice;
|
||||
const protectionSummary = protection
|
||||
? [
|
||||
protection.type,
|
||||
`${formatValue(
|
||||
protection.ratedCurrentA,
|
||||
"protectionRatedCurrent"
|
||||
)} A`,
|
||||
protection.fuseUtilizationCategory,
|
||||
protection.tripCharacteristic,
|
||||
protection.rcdType
|
||||
? `Typ ${protection.rcdType}`
|
||||
: undefined,
|
||||
protection.ratedResidualCurrentMa !== undefined
|
||||
? `${formatValue(
|
||||
protection.ratedResidualCurrentMa,
|
||||
"protectionRatedCurrent"
|
||||
)} mA`
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ")
|
||||
: null;
|
||||
return (
|
||||
<tr
|
||||
key={row.rowKey}
|
||||
className={`structure-component-row ${row.rowType}`}
|
||||
>
|
||||
<td colSpan={visibleColumns.length + 1}>
|
||||
<div className="structure-component-content">
|
||||
<strong>{component.equipmentIdentifier}</strong>
|
||||
<span>{component.name}</span>
|
||||
{protectionSummary ? (
|
||||
<span className="structure-component-protection">
|
||||
{protectionSummary}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
if (row.rowType === "section") {
|
||||
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,16 @@ export type GridInsertionRowType =
|
||||
|
||||
export type GridInsertionCellKind = "circuitField" | "deviceField" | "readonly" | "computed";
|
||||
|
||||
export function isGridInsertionRowType(rowType: string): rowType is GridInsertionRowType {
|
||||
return (
|
||||
rowType === "circuitCompact" ||
|
||||
rowType === "circuitSummary" ||
|
||||
rowType === "deviceRow" ||
|
||||
rowType === "reserveCircuit" ||
|
||||
rowType === "placeholder"
|
||||
);
|
||||
}
|
||||
|
||||
export interface GridInsertionSelection {
|
||||
rowType: GridInsertionRowType;
|
||||
cellKind: GridInsertionCellKind;
|
||||
|
||||
@@ -37,12 +37,15 @@ export type CellKey =
|
||||
| "isReserve";
|
||||
|
||||
export type RowType =
|
||||
| "headerComponent"
|
||||
| "section"
|
||||
| "groupComponent"
|
||||
| "circuitCompact"
|
||||
| "circuitSummary"
|
||||
| "deviceRow"
|
||||
| "reserveCircuit"
|
||||
| "placeholder";
|
||||
| "placeholder"
|
||||
| "footerComponent";
|
||||
export type CellKind = "circuitField" | "deviceField" | "computed" | "readonly";
|
||||
export type GridValue = string | number | boolean | undefined;
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeComponentDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
CircuitTreeSectionDto,
|
||||
} from "../types.js";
|
||||
import { buildCircuitStructureProjection } from "./circuit-structure-projection";
|
||||
import {
|
||||
allColumns,
|
||||
circuitOnlyColumns,
|
||||
@@ -42,6 +44,7 @@ export interface VisibleGridRow {
|
||||
sectionId: string;
|
||||
circuit?: CircuitTreeCircuitDto;
|
||||
device?: CircuitTreeDeviceRowDto;
|
||||
component?: CircuitTreeComponentDto;
|
||||
cells: VisibleGridCell[];
|
||||
}
|
||||
|
||||
@@ -160,21 +163,64 @@ function makeVisibleGridRow(
|
||||
}
|
||||
|
||||
export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[]): VisibleGridRow[] {
|
||||
const rows: VisibleGridRow[] = [];
|
||||
for (const section of sections) {
|
||||
rows.push({
|
||||
rowKey: `section:${section.id}`,
|
||||
rowType: "section",
|
||||
sectionId: section.id,
|
||||
cells: allColumns.map((column) => ({
|
||||
cellKey: column.key,
|
||||
editable: false,
|
||||
kind: "readonly",
|
||||
value: undefined,
|
||||
})),
|
||||
});
|
||||
return buildVisibleGridRowsWithStructure(sections, {
|
||||
headerComponents: [],
|
||||
footerComponents: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const circuit of section.circuits) {
|
||||
export function buildVisibleGridRowsWithStructure(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
structure: {
|
||||
headerComponents: readonly CircuitTreeComponentDto[];
|
||||
footerComponents: readonly CircuitTreeComponentDto[];
|
||||
}
|
||||
): VisibleGridRow[] {
|
||||
const rows: VisibleGridRow[] = [];
|
||||
const projected = buildCircuitStructureProjection({
|
||||
headerComponents: [...structure.headerComponents],
|
||||
sections: [...sections],
|
||||
footerComponents: [...structure.footerComponents],
|
||||
});
|
||||
for (const row of projected) {
|
||||
if ("component" in row) {
|
||||
const component = row.component;
|
||||
rows.push({
|
||||
rowKey: row.rowKey,
|
||||
rowType: row.rowType,
|
||||
sectionId: component.sectionId ?? `__${row.zone}__`,
|
||||
component,
|
||||
cells: allColumns.map((column) => ({
|
||||
cellKey: column.key,
|
||||
editable: false,
|
||||
kind: "readonly",
|
||||
value:
|
||||
column.key === "equipmentIdentifier"
|
||||
? component.equipmentIdentifier
|
||||
: column.key === "displayName"
|
||||
? component.name
|
||||
: undefined,
|
||||
})),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const section = row.section;
|
||||
if (row.rowType === "groupHeader") {
|
||||
rows.push({
|
||||
rowKey: `section:${section.id}`,
|
||||
rowType: "section",
|
||||
sectionId: section.id,
|
||||
cells: allColumns.map((column) => ({
|
||||
cellKey: column.key,
|
||||
editable: false,
|
||||
kind: "readonly",
|
||||
value: undefined,
|
||||
})),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (row.rowType === "circuitBlock") {
|
||||
const circuit = row.circuit;
|
||||
if (circuit.deviceRows.length === 0) {
|
||||
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
||||
} else if (circuit.deviceRows.length === 1) {
|
||||
@@ -185,8 +231,8 @@ export function buildVisibleGridRows(sections: readonly CircuitTreeSectionDto[])
|
||||
rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push(makeVisibleGridRow("placeholder", section.id));
|
||||
}
|
||||
return rows;
|
||||
|
||||
@@ -55,7 +55,7 @@ export function buildCircuitStructureProjection(
|
||||
zone: "group",
|
||||
section,
|
||||
});
|
||||
for (const component of ordered(section.components)) {
|
||||
for (const component of ordered(section.components ?? [])) {
|
||||
rows.push({
|
||||
rowKey: `group-component:${component.id}`,
|
||||
rowType: "groupComponent",
|
||||
|
||||
Reference in New Issue
Block a user