Extract circuit grid projection
This commit is contained in:
@@ -17,16 +17,11 @@ import {
|
||||
} from "../utils/circuit-grid-safety";
|
||||
import {
|
||||
allColumns,
|
||||
circuitOnlyColumns,
|
||||
compareSortValues,
|
||||
defaultVisibleColumnKeys,
|
||||
deviceFieldKeys,
|
||||
formatValue,
|
||||
getBlockSortValue,
|
||||
getCellKind,
|
||||
getCircuitValue,
|
||||
getDeviceValue,
|
||||
normalizeFilterValue,
|
||||
parseNumeric,
|
||||
} from "../utils/circuit-grid-model";
|
||||
import type {
|
||||
@@ -35,6 +30,12 @@ import type {
|
||||
ColumnDef,
|
||||
RowType,
|
||||
} from "../utils/circuit-grid-model";
|
||||
import {
|
||||
buildVisibleGridRows,
|
||||
filterAndSortCircuitSections,
|
||||
getDistinctFilterValues,
|
||||
} from "../utils/circuit-grid-projection";
|
||||
import type { VisibleGridRow } from "../utils/circuit-grid-projection";
|
||||
import {
|
||||
createCircuit,
|
||||
createCircuitDeviceRow,
|
||||
@@ -82,22 +83,6 @@ interface SelectionIntent {
|
||||
deviceId?: string;
|
||||
}
|
||||
|
||||
interface VisibleGridCell {
|
||||
cellKey: CellKey;
|
||||
editable: boolean;
|
||||
kind: CellKind;
|
||||
value: string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
interface VisibleGridRow {
|
||||
rowKey: string;
|
||||
rowType: RowType;
|
||||
sectionId: string;
|
||||
circuit?: CircuitTreeCircuitDto;
|
||||
device?: CircuitTreeDeviceRowDto;
|
||||
cells: VisibleGridCell[];
|
||||
}
|
||||
|
||||
interface HistoryCommand {
|
||||
label: string;
|
||||
redo: () => Promise<SelectionIntent | null | void>;
|
||||
@@ -332,129 +317,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const distinctValuesByColumn = useMemo(() => {
|
||||
// Filter options are generated only for visible columns to match current header UI.
|
||||
// Hidden columns still exist in row model, but are intentionally not filterable from header.
|
||||
const result = {} as Record<CellKey, string[]>;
|
||||
for (const column of visibleColumns) {
|
||||
const set = new Set<string>();
|
||||
for (const section of data?.sections ?? []) {
|
||||
for (const circuit of section.circuits) {
|
||||
set.add(normalizeFilterValue(getCircuitValue(circuit, column.key)));
|
||||
for (const row of circuit.deviceRows) {
|
||||
set.add(normalizeFilterValue(getDeviceValue(row, column.key)));
|
||||
}
|
||||
}
|
||||
}
|
||||
result[column.key] = [...set].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
}
|
||||
return result;
|
||||
return getDistinctFilterValues(data?.sections ?? [], visibleColumns);
|
||||
}, [data, visibleColumns]);
|
||||
|
||||
const filteredSortedSections = useMemo(() => {
|
||||
// Filtering and sorting are frontend view state. Backend sort order remains unchanged
|
||||
// until user explicitly applies sorted order.
|
||||
if (!data) {
|
||||
return [] as CircuitTreeResponseDto["sections"];
|
||||
}
|
||||
const matchesColumnFilter = (
|
||||
circuit: CircuitTreeCircuitDto,
|
||||
row: CircuitTreeDeviceRowDto | null,
|
||||
key: CellKey,
|
||||
selected: string[]
|
||||
) => {
|
||||
if (!selected.length) {
|
||||
return true;
|
||||
}
|
||||
const values = new Set<string>();
|
||||
values.add(normalizeFilterValue(getCircuitValue(circuit, key)));
|
||||
if (row) {
|
||||
values.add(normalizeFilterValue(getDeviceValue(row, key)));
|
||||
} else {
|
||||
for (const device of circuit.deviceRows) {
|
||||
values.add(normalizeFilterValue(getDeviceValue(device, key)));
|
||||
}
|
||||
}
|
||||
return selected.some((entry) => values.has(entry));
|
||||
};
|
||||
|
||||
const sections = data.sections
|
||||
.map((section) => {
|
||||
let circuits = section.circuits
|
||||
.map((circuit) => {
|
||||
const selectedFilters = Object.entries(columnFilters).filter(([, values]) => (values?.length ?? 0) > 0);
|
||||
const circuitMatchesAll = selectedFilters.every(([key, values]) =>
|
||||
matchesColumnFilter(circuit, null, key as CellKey, values ?? [])
|
||||
);
|
||||
if (!circuitMatchesAll) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!hasActiveFilters || circuit.deviceRows.length <= 1) {
|
||||
return circuit;
|
||||
}
|
||||
|
||||
const matchingRows = circuit.deviceRows.filter((row) =>
|
||||
selectedFilters.every(([key, values]) =>
|
||||
matchesColumnFilter(circuit, row, key as CellKey, values ?? [])
|
||||
)
|
||||
);
|
||||
|
||||
if (matchingRows.length > 0) {
|
||||
return { ...circuit, deviceRows: matchingRows };
|
||||
}
|
||||
if (selectedFilters.some(([key]) => circuitOnlyColumns.has(key as CellKey))) {
|
||||
return circuit;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter(Boolean) as CircuitTreeCircuitDto[];
|
||||
|
||||
if (sortState) {
|
||||
// Sort on circuit blocks so multi-device circuits keep row grouping integrity.
|
||||
circuits = [...circuits].sort((a, b) => {
|
||||
const cmp = compareSortValues(getBlockSortValue(a, sortState.key), getBlockSortValue(b, sortState.key));
|
||||
return sortState.direction === "asc" ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
return { ...section, circuits };
|
||||
})
|
||||
.filter((section) => section.circuits.length > 0);
|
||||
|
||||
return sections;
|
||||
}, [data, columnFilters, hasActiveFilters, sortState]);
|
||||
return filterAndSortCircuitSections(data?.sections ?? [], columnFilters, sortState);
|
||||
}, [data, columnFilters, sortState]);
|
||||
|
||||
// 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(() => {
|
||||
if (!filteredSortedSections.length) {
|
||||
return [] as VisibleGridRow[];
|
||||
}
|
||||
const rows: VisibleGridRow[] = [];
|
||||
for (const section of filteredSortedSections) {
|
||||
rows.push({
|
||||
rowKey: `section:${section.id}`,
|
||||
rowType: "section",
|
||||
sectionId: section.id,
|
||||
// Section headers are structural markers only: visible, but never editable/selectable.
|
||||
cells: allColumns.map((col) => ({ cellKey: col.key, editable: false, kind: "readonly", value: undefined })),
|
||||
});
|
||||
for (const circuit of section.circuits) {
|
||||
if (circuit.deviceRows.length === 0) {
|
||||
rows.push(makeRow("reserveCircuit", section.id, circuit));
|
||||
continue;
|
||||
}
|
||||
if (circuit.deviceRows.length === 1) {
|
||||
rows.push(makeRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
||||
continue;
|
||||
}
|
||||
rows.push(makeRow("circuitSummary", section.id, circuit));
|
||||
for (const device of circuit.deviceRows) {
|
||||
rows.push(makeRow("deviceRow", section.id, circuit, device));
|
||||
}
|
||||
}
|
||||
// Placeholder row is virtual (-frei-) but intentionally part of editable grid because
|
||||
// editing/dropping here is the fast path to create a real circuit in this section.
|
||||
rows.push(makeRow("placeholder", section.id));
|
||||
}
|
||||
return rows;
|
||||
return buildVisibleGridRows(filteredSortedSections);
|
||||
}, [filteredSortedSections]);
|
||||
|
||||
const searchableProjectDevices = useMemo(() => {
|
||||
@@ -490,40 +365,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
[allCircuits]
|
||||
);
|
||||
|
||||
function makeRow(
|
||||
rowType: RowType,
|
||||
sectionId: string,
|
||||
circuit?: CircuitTreeCircuitDto,
|
||||
device?: CircuitTreeDeviceRowDto
|
||||
): VisibleGridRow {
|
||||
// Row keys are UI identities, not persisted identities. They are rebuilt on each tree refresh.
|
||||
const rowKey =
|
||||
rowType === "placeholder"
|
||||
? `placeholder:${sectionId}`
|
||||
: rowType === "deviceRow" && device
|
||||
? `device:${device.id}`
|
||||
: `${rowType}:${circuit?.id ?? sectionId}`;
|
||||
const cells = allColumns.map((col) => {
|
||||
const kind = getCellKind(rowType, col.key);
|
||||
// Hidden columns remain in the normalized row data; only visibility controls rendering.
|
||||
const editable = kind === "circuitField" || kind === "deviceField";
|
||||
let value: string | number | boolean | undefined;
|
||||
if (rowType === "placeholder") {
|
||||
value = col.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
||||
} else if (kind === "deviceField" && device) {
|
||||
value = getDeviceValue(device, col.key);
|
||||
} else if (kind === "circuitField" && circuit) {
|
||||
value = getCircuitValue(circuit, col.key);
|
||||
} else if (col.key === "circuitTotalPower" && circuit) {
|
||||
value = circuit.circuitTotalPower;
|
||||
} else if (col.key === "rowTotalPower" && device) {
|
||||
value = device.rowTotalPower;
|
||||
}
|
||||
return { cellKey: col.key, editable, kind, value };
|
||||
});
|
||||
return { rowKey, rowType, sectionId, circuit, device, cells };
|
||||
}
|
||||
|
||||
const editableCells = useMemo(
|
||||
() =>
|
||||
visibleRows.flatMap((row) =>
|
||||
|
||||
Reference in New Issue
Block a user