Extract circuit grid projection
This commit is contained in:
@@ -8,6 +8,7 @@ The circuit-list editor is a circuit-first planning workspace for distribution-b
|
||||
|
||||
- `circuit-tree-editor.tsx` owns React state, API commands, undo/redo and drag-and-drop orchestration.
|
||||
- `circuit-grid-model.ts` owns column metadata, circuit/device cell ownership, value projection, formatting, numeric parsing and block sort values.
|
||||
- `circuit-grid-projection.ts` owns block-preserving filtering/sorting and the normalized visible row projection.
|
||||
- `circuit-grid-insertion.ts` resolves insertion intent and insertion sort positions.
|
||||
- `circuit-grid-safety.ts` resolves delete intent, BMK conflicts and cross-section move confirmation requirements.
|
||||
|
||||
|
||||
+2
-2
@@ -11,8 +11,8 @@
|
||||
"build:api": "tsc -p tsconfig.json",
|
||||
"build:web": "next build",
|
||||
"start": "node dist/server/index.js",
|
||||
"test": "tsx --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts",
|
||||
"test:watch": "tsx --watch --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts",
|
||||
"test": "tsx --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts",
|
||||
"test:watch": "tsx --watch --test tests/power-calculation.test.ts tests/consumer-linking.service.test.ts tests/consumer-schema-options.test.ts tests/project-device-schema.test.ts tests/project-device-placement.service.test.ts tests/project-device-sync.service.test.ts tests/legacy-consumer-migration-planner.test.ts tests/circuit-numbering.service.test.ts tests/circuit-write.rules.test.ts tests/circuit-power-calculation.test.ts tests/circuit-tree.controller.test.ts tests/circuit-grid-insertion.test.ts tests/circuit-grid-safety.test.ts tests/circuit-grid-model.test.ts tests/circuit-grid-projection.test.ts",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:backup": "node scripts/db-backup.js",
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
CircuitTreeSectionDto,
|
||||
} from "../types.js";
|
||||
import {
|
||||
allColumns,
|
||||
circuitOnlyColumns,
|
||||
compareSortValues,
|
||||
deviceFieldKeys,
|
||||
getBlockSortValue,
|
||||
getCellKind,
|
||||
getCircuitValue,
|
||||
getDeviceValue,
|
||||
normalizeFilterValue,
|
||||
} from "./circuit-grid-model";
|
||||
import type {
|
||||
CellKey,
|
||||
CellKind,
|
||||
ColumnDef,
|
||||
GridValue,
|
||||
RowType,
|
||||
} from "./circuit-grid-model";
|
||||
|
||||
export type ColumnFilters = Partial<Record<CellKey, string[]>>;
|
||||
|
||||
export interface GridSortState {
|
||||
key: CellKey;
|
||||
direction: "asc" | "desc";
|
||||
}
|
||||
|
||||
export interface VisibleGridCell {
|
||||
cellKey: CellKey;
|
||||
editable: boolean;
|
||||
kind: CellKind;
|
||||
value: GridValue;
|
||||
}
|
||||
|
||||
export interface VisibleGridRow {
|
||||
rowKey: string;
|
||||
rowType: RowType;
|
||||
sectionId: string;
|
||||
circuit?: CircuitTreeCircuitDto;
|
||||
device?: CircuitTreeDeviceRowDto;
|
||||
cells: VisibleGridCell[];
|
||||
}
|
||||
|
||||
function getCircuitBlockFilterValues(circuit: CircuitTreeCircuitDto, key: CellKey): Set<string> {
|
||||
const values = new Set<string>();
|
||||
if (circuitOnlyColumns.has(key) || key === "displayName" || key === "remark") {
|
||||
values.add(normalizeFilterValue(getCircuitValue(circuit, key)));
|
||||
}
|
||||
if (!circuitOnlyColumns.has(key)) {
|
||||
for (const device of circuit.deviceRows) {
|
||||
values.add(normalizeFilterValue(getDeviceValue(device, key)));
|
||||
}
|
||||
}
|
||||
if (values.size === 0 && deviceFieldKeys.has(key)) {
|
||||
values.add(normalizeFilterValue(undefined));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
export function getDistinctFilterValues(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
columns: readonly ColumnDef[]
|
||||
): Record<CellKey, string[]> {
|
||||
const result = {} as Record<CellKey, string[]>;
|
||||
for (const column of columns) {
|
||||
const values = new Set<string>();
|
||||
for (const section of sections) {
|
||||
for (const circuit of section.circuits) {
|
||||
for (const value of getCircuitBlockFilterValues(circuit, column.key)) {
|
||||
values.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
result[column.key] = [...values].sort((left, right) =>
|
||||
left.localeCompare(right, undefined, { numeric: true })
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function filterAndSortCircuitSections(
|
||||
sections: readonly CircuitTreeSectionDto[],
|
||||
columnFilters: ColumnFilters,
|
||||
sortState: GridSortState | null
|
||||
): CircuitTreeSectionDto[] {
|
||||
const activeFilters = Object.entries(columnFilters).filter(([, values]) => (values?.length ?? 0) > 0) as Array<
|
||||
[CellKey, string[]]
|
||||
>;
|
||||
|
||||
return sections
|
||||
.map((section) => {
|
||||
let circuits = section.circuits.filter((circuit) =>
|
||||
activeFilters.every(([key, selectedValues]) => {
|
||||
const blockValues = getCircuitBlockFilterValues(circuit, key);
|
||||
return selectedValues.some((selectedValue) => blockValues.has(selectedValue));
|
||||
})
|
||||
);
|
||||
|
||||
if (sortState) {
|
||||
// Circuit blocks are sorted as units; their device row membership/order is untouched.
|
||||
circuits = [...circuits].sort((left, right) => {
|
||||
const comparison = compareSortValues(
|
||||
getBlockSortValue(left, sortState.key),
|
||||
getBlockSortValue(right, sortState.key)
|
||||
);
|
||||
return sortState.direction === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
|
||||
return { ...section, circuits };
|
||||
})
|
||||
.filter((section) => section.circuits.length > 0);
|
||||
}
|
||||
|
||||
function makeVisibleGridRow(
|
||||
rowType: RowType,
|
||||
sectionId: string,
|
||||
circuit?: CircuitTreeCircuitDto,
|
||||
device?: CircuitTreeDeviceRowDto
|
||||
): VisibleGridRow {
|
||||
const rowKey =
|
||||
rowType === "placeholder"
|
||||
? `placeholder:${sectionId}`
|
||||
: rowType === "deviceRow" && device
|
||||
? `device:${device.id}`
|
||||
: `${rowType}:${circuit?.id ?? sectionId}`;
|
||||
|
||||
const cells = allColumns.map((column): VisibleGridCell => {
|
||||
const kind = getCellKind(rowType, column.key);
|
||||
const editable = kind === "circuitField" || kind === "deviceField";
|
||||
let value: GridValue;
|
||||
|
||||
if (rowType === "placeholder") {
|
||||
value = column.key === "equipmentIdentifier" ? "-frei-" : undefined;
|
||||
} else if (kind === "deviceField" && device) {
|
||||
value = getDeviceValue(device, column.key);
|
||||
} else if (kind === "circuitField" && circuit) {
|
||||
value = getCircuitValue(circuit, column.key);
|
||||
} else if (column.key === "circuitTotalPower" && circuit) {
|
||||
value = circuit.circuitTotalPower;
|
||||
} else if (column.key === "rowTotalPower" && device) {
|
||||
value = device.rowTotalPower;
|
||||
}
|
||||
|
||||
return { cellKey: column.key, editable, kind, value };
|
||||
});
|
||||
|
||||
return { rowKey, rowType, sectionId, circuit, device, cells };
|
||||
}
|
||||
|
||||
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,
|
||||
})),
|
||||
});
|
||||
|
||||
for (const circuit of section.circuits) {
|
||||
if (circuit.deviceRows.length === 0) {
|
||||
rows.push(makeVisibleGridRow("reserveCircuit", section.id, circuit));
|
||||
} else if (circuit.deviceRows.length === 1) {
|
||||
rows.push(makeVisibleGridRow("circuitCompact", section.id, circuit, circuit.deviceRows[0]));
|
||||
} else {
|
||||
rows.push(makeVisibleGridRow("circuitSummary", section.id, circuit));
|
||||
for (const device of circuit.deviceRows) {
|
||||
rows.push(makeVisibleGridRow("deviceRow", section.id, circuit, device));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rows.push(makeVisibleGridRow("placeholder", section.id));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
buildVisibleGridRows,
|
||||
filterAndSortCircuitSections,
|
||||
getDistinctFilterValues,
|
||||
} from "../src/frontend/utils/circuit-grid-projection.js";
|
||||
import type {
|
||||
CircuitTreeCircuitDto,
|
||||
CircuitTreeDeviceRowDto,
|
||||
CircuitTreeSectionDto,
|
||||
} from "../src/frontend/types.js";
|
||||
|
||||
function makeDevice(
|
||||
id: string,
|
||||
displayName: string,
|
||||
roomNumberSnapshot: string,
|
||||
sortOrder: number
|
||||
): CircuitTreeDeviceRowDto {
|
||||
return {
|
||||
id,
|
||||
sortOrder,
|
||||
name: displayName,
|
||||
displayName,
|
||||
roomNumberSnapshot,
|
||||
quantity: 1,
|
||||
powerPerUnit: 0.1,
|
||||
simultaneityFactor: 1,
|
||||
rowTotalPower: 0.1,
|
||||
};
|
||||
}
|
||||
|
||||
function makeCircuit(
|
||||
id: string,
|
||||
equipmentIdentifier: string,
|
||||
displayName: string,
|
||||
deviceRows: CircuitTreeDeviceRowDto[],
|
||||
sortOrder: number
|
||||
): CircuitTreeCircuitDto {
|
||||
return {
|
||||
id,
|
||||
circuitListId: "list-1",
|
||||
sectionId: "section-1",
|
||||
equipmentIdentifier,
|
||||
displayName,
|
||||
sortOrder,
|
||||
isReserve: deviceRows.length === 0,
|
||||
circuitTotalPower: deviceRows.reduce((total, row) => total + row.rowTotalPower, 0),
|
||||
deviceRows,
|
||||
};
|
||||
}
|
||||
|
||||
const multiDeviceCircuit = makeCircuit(
|
||||
"circuit-multi",
|
||||
"-1F2",
|
||||
"Office lighting",
|
||||
[makeDevice("device-office", "Office light", "1.01", 10), makeDevice("device-store", "Store light", "1.02", 20)],
|
||||
20
|
||||
);
|
||||
const compactCircuit = makeCircuit(
|
||||
"circuit-compact",
|
||||
"-1F1",
|
||||
"Corridor lighting",
|
||||
[makeDevice("device-corridor", "Corridor light", "1.03", 10)],
|
||||
10
|
||||
);
|
||||
const reserveCircuit = makeCircuit("circuit-reserve", "-1F3", "Reserve", [], 30);
|
||||
|
||||
const sections: CircuitTreeSectionDto[] = [
|
||||
{
|
||||
id: "section-1",
|
||||
key: "lighting",
|
||||
displayName: "Lighting",
|
||||
prefix: "-1F",
|
||||
sortOrder: 10,
|
||||
circuits: [compactCircuit, multiDeviceCircuit, reserveCircuit],
|
||||
},
|
||||
];
|
||||
|
||||
describe("circuit grid projection", () => {
|
||||
it("keeps every device row in a circuit block when one device matches a filter", () => {
|
||||
const result = filterAndSortCircuitSections(sections, { roomNumberSnapshot: ["1.01"] }, null);
|
||||
|
||||
assert.deepEqual(result.map((section) => section.circuits.map((circuit) => circuit.id)), [["circuit-multi"]]);
|
||||
assert.deepEqual(result[0].circuits[0].deviceRows.map((row) => row.id), ["device-office", "device-store"]);
|
||||
});
|
||||
|
||||
it("combines column filters across the complete circuit block", () => {
|
||||
const result = filterAndSortCircuitSections(
|
||||
sections,
|
||||
{ equipmentIdentifier: ["-1F2"], roomNumberSnapshot: ["1.02"] },
|
||||
null
|
||||
);
|
||||
|
||||
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), ["circuit-multi"]);
|
||||
});
|
||||
|
||||
it("treats missing device values as missing without matching populated circuits", () => {
|
||||
const result = filterAndSortCircuitSections(sections, { roomNumberSnapshot: ["-"] }, null);
|
||||
|
||||
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), ["circuit-reserve"]);
|
||||
});
|
||||
|
||||
it("sorts complete circuit blocks without changing device row order", () => {
|
||||
const result = filterAndSortCircuitSections(sections, {}, { key: "equipmentIdentifier", direction: "desc" });
|
||||
|
||||
assert.deepEqual(result[0].circuits.map((circuit) => circuit.id), [
|
||||
"circuit-reserve",
|
||||
"circuit-multi",
|
||||
"circuit-compact",
|
||||
]);
|
||||
assert.deepEqual(result[0].circuits[1].deviceRows.map((row) => row.id), ["device-office", "device-store"]);
|
||||
});
|
||||
|
||||
it("builds the compact, grouped, reserve and placeholder row shapes", () => {
|
||||
const rows = buildVisibleGridRows(sections);
|
||||
|
||||
assert.deepEqual(rows.map((row) => row.rowType), [
|
||||
"section",
|
||||
"circuitCompact",
|
||||
"circuitSummary",
|
||||
"deviceRow",
|
||||
"deviceRow",
|
||||
"reserveCircuit",
|
||||
"placeholder",
|
||||
]);
|
||||
assert.equal(rows.at(-1)?.rowKey, "placeholder:section-1");
|
||||
assert.equal(
|
||||
rows.at(-1)?.cells.find((cell) => cell.cellKey === "equipmentIdentifier")?.value,
|
||||
"-frei-"
|
||||
);
|
||||
});
|
||||
|
||||
it("collects filter options from both circuit and device values", () => {
|
||||
const values = getDistinctFilterValues(sections, [
|
||||
{ key: "displayName", label: "Display name" },
|
||||
{ key: "roomNumberSnapshot", label: "Room number" },
|
||||
]);
|
||||
|
||||
assert.ok(values.displayName.includes("Office lighting"));
|
||||
assert.ok(values.displayName.includes("Store light"));
|
||||
assert.ok(values.roomNumberSnapshot.includes("1.03"));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user