WIP multiple sections
This commit is contained in:
@@ -350,6 +350,11 @@ enthalten ihre optionale getrennte Schutzgerätekonfiguration. Die bisherigen
|
|||||||
flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv
|
flachen Stromkreis-Schutzfelder bleiben während der Übergangsphase additiv
|
||||||
erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide
|
erhalten. Separate aktive Read-Repositories lesen Komponenten- und beide
|
||||||
Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands.
|
Schutzgerätetabellen; Schreibzugriffe bleiben ausschließlich in Commands.
|
||||||
|
`src/frontend/utils/circuit-structure-projection.ts` projiziert diesen Tree
|
||||||
|
rein und deterministisch in die drei Zonen Kopf, vollständige Gruppen und Fuß.
|
||||||
|
Innerhalb einer Gruppe folgen auf die Überschrift optionale Schutzkomponenten,
|
||||||
|
vollständige Stromkreisblöcke und der freie Einfügeplatz. Leere Gruppen bleiben
|
||||||
|
sichtbar; Komponenten werden stabil nach `sortOrder` und ID sortiert.
|
||||||
`distribution-board.update` versioniert Etage, Netzart und den
|
`distribution-board.update` versioniert Etage, Netzart und den
|
||||||
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
verteilerweiten Gleichzeitigkeitsfaktor gemeinsam und stellt alle Werte über
|
||||||
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
dauerhaftes Undo/Redo wieder her. Der Faktor liegt zwischen `0` und `1` und
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
|
- [x] Phase D3a: complete validated group-subtree snapshot and warning summary.
|
||||||
- [x] Phase D3b: persistent populated-group delete/restore command.
|
- [x] Phase D3b: persistent populated-group delete/restore command.
|
||||||
- [x] Phase E1: tree read model for components, groups and protection devices.
|
- [x] Phase E1: tree read model for components, groups and protection devices.
|
||||||
- [ ] Phase E2: header/group/footer editor projection.
|
- [x] Phase E2a: pure header/group/footer structure projection.
|
||||||
|
- [ ] Phase E2b: render the structure projection in the editor grid.
|
||||||
- [ ] Phase E: editor projection and editing.
|
- [ ] Phase E: editor projection and editing.
|
||||||
- [ ] Phase F: documentation and full GUI verification.
|
- [ ] Phase F: documentation and full GUI verification.
|
||||||
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
- [ ] Keep full electrical sizing and cable-dimensioning rules separate until
|
||||||
|
|||||||
@@ -739,6 +739,9 @@ Acceptance:
|
|||||||
|
|
||||||
### E. Editor Projection and Editing
|
### E. Editor Projection and Editing
|
||||||
|
|
||||||
|
Status: In progress. The complete tree read model E1 and pure structural
|
||||||
|
projection E2a are complete. Grid rendering and editing remain pending.
|
||||||
|
|
||||||
- render fixed header components
|
- render fixed header components
|
||||||
- render group components and circuit blocks
|
- render group components and circuit blocks
|
||||||
- render sortable auxiliary footer components
|
- render sortable auxiliary footer components
|
||||||
@@ -746,6 +749,17 @@ Acceptance:
|
|||||||
- add type-dependent protection editing
|
- add type-dependent protection editing
|
||||||
- keep spreadsheet selection, keyboard and drag behavior coherent
|
- keep spreadsheet selection, keyboard and drag behavior coherent
|
||||||
|
|
||||||
|
Implemented in E1/E2a:
|
||||||
|
|
||||||
|
- the tree response exposes fixed header, group and auxiliary footer
|
||||||
|
components plus both one-to-one protection layers
|
||||||
|
- a pure projection produces the stable order header, complete groups and
|
||||||
|
footer without mixing zones
|
||||||
|
- each group keeps its heading, optional protection components, complete
|
||||||
|
circuit blocks and free placeholder together
|
||||||
|
- empty groups remain visible and circuit device rows remain owned by their
|
||||||
|
circuit block
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- the visible order is header, groups, footer
|
- the visible order is header, groups, footer
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import type {
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeComponentDto,
|
||||||
|
CircuitTreeResponseDto,
|
||||||
|
CircuitTreeSectionDto,
|
||||||
|
} from "../types.js";
|
||||||
|
|
||||||
|
export type CircuitStructureProjectionRow =
|
||||||
|
| {
|
||||||
|
rowKey: string;
|
||||||
|
rowType: "headerComponent" | "footerComponent";
|
||||||
|
zone: "header" | "footer";
|
||||||
|
component: CircuitTreeComponentDto;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
rowKey: string;
|
||||||
|
rowType: "groupHeader" | "groupPlaceholder";
|
||||||
|
zone: "group";
|
||||||
|
section: CircuitTreeSectionDto;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
rowKey: string;
|
||||||
|
rowType: "groupComponent";
|
||||||
|
zone: "group";
|
||||||
|
section: CircuitTreeSectionDto;
|
||||||
|
component: CircuitTreeComponentDto;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
rowKey: string;
|
||||||
|
rowType: "circuitBlock";
|
||||||
|
zone: "group";
|
||||||
|
section: CircuitTreeSectionDto;
|
||||||
|
circuit: CircuitTreeCircuitDto;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildCircuitStructureProjection(
|
||||||
|
tree: Pick<
|
||||||
|
CircuitTreeResponseDto,
|
||||||
|
"headerComponents" | "sections" | "footerComponents"
|
||||||
|
>
|
||||||
|
): CircuitStructureProjectionRow[] {
|
||||||
|
const rows: CircuitStructureProjectionRow[] = [];
|
||||||
|
for (const component of ordered(tree.headerComponents)) {
|
||||||
|
rows.push({
|
||||||
|
rowKey: `header-component:${component.id}`,
|
||||||
|
rowType: "headerComponent",
|
||||||
|
zone: "header",
|
||||||
|
component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const section of ordered(tree.sections)) {
|
||||||
|
rows.push({
|
||||||
|
rowKey: `group:${section.id}`,
|
||||||
|
rowType: "groupHeader",
|
||||||
|
zone: "group",
|
||||||
|
section,
|
||||||
|
});
|
||||||
|
for (const component of ordered(section.components)) {
|
||||||
|
rows.push({
|
||||||
|
rowKey: `group-component:${component.id}`,
|
||||||
|
rowType: "groupComponent",
|
||||||
|
zone: "group",
|
||||||
|
section,
|
||||||
|
component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const circuit of section.circuits) {
|
||||||
|
rows.push({
|
||||||
|
rowKey: `circuit-block:${circuit.id}`,
|
||||||
|
rowType: "circuitBlock",
|
||||||
|
zone: "group",
|
||||||
|
section,
|
||||||
|
circuit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
rows.push({
|
||||||
|
rowKey: `group-placeholder:${section.id}`,
|
||||||
|
rowType: "groupPlaceholder",
|
||||||
|
zone: "group",
|
||||||
|
section,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const component of ordered(tree.footerComponents)) {
|
||||||
|
rows.push({
|
||||||
|
rowKey: `footer-component:${component.id}`,
|
||||||
|
rowType: "footerComponent",
|
||||||
|
zone: "footer",
|
||||||
|
component,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ordered<T extends { id: string; sortOrder: number }>(
|
||||||
|
values: readonly T[]
|
||||||
|
) {
|
||||||
|
return [...values].sort(
|
||||||
|
(left, right) =>
|
||||||
|
left.sortOrder - right.sortOrder ||
|
||||||
|
left.id.localeCompare(right.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
CircuitTreeDeviceRowDto,
|
CircuitTreeDeviceRowDto,
|
||||||
CircuitTreeSectionDto,
|
CircuitTreeSectionDto,
|
||||||
} from "../src/frontend/types.js";
|
} from "../src/frontend/types.js";
|
||||||
|
import "./circuit-structure-projection.test.js";
|
||||||
|
|
||||||
function makeDevice(
|
function makeDevice(
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import { buildCircuitStructureProjection } from "../src/frontend/utils/circuit-structure-projection.js";
|
||||||
|
import type {
|
||||||
|
CircuitTreeCircuitDto,
|
||||||
|
CircuitTreeComponentDto,
|
||||||
|
CircuitTreeSectionDto,
|
||||||
|
} from "../src/frontend/types.js";
|
||||||
|
|
||||||
|
function component(
|
||||||
|
id: string,
|
||||||
|
placement: CircuitTreeComponentDto["placement"],
|
||||||
|
sortOrder: number,
|
||||||
|
sectionId?: string
|
||||||
|
): CircuitTreeComponentDto {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId,
|
||||||
|
equipmentIdentifier: `-${id}`,
|
||||||
|
name: id,
|
||||||
|
role:
|
||||||
|
placement === "header"
|
||||||
|
? "main_switch"
|
||||||
|
: placement === "footer"
|
||||||
|
? "auxiliary"
|
||||||
|
: "group_residual_current_protection",
|
||||||
|
placement,
|
||||||
|
sortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function circuit(
|
||||||
|
id: string,
|
||||||
|
sectionId: string
|
||||||
|
): CircuitTreeCircuitDto {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
circuitListId: "list-1",
|
||||||
|
sectionId,
|
||||||
|
equipmentIdentifier: `-${id}`,
|
||||||
|
sortOrder: 10,
|
||||||
|
isReserve: true,
|
||||||
|
circuitTotalPower: 0,
|
||||||
|
deviceRows: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function section(
|
||||||
|
id: string,
|
||||||
|
sortOrder: number
|
||||||
|
): CircuitTreeSectionDto {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
key: id,
|
||||||
|
displayName: id,
|
||||||
|
prefix: "-1F1.",
|
||||||
|
sortOrder,
|
||||||
|
category: "lighting",
|
||||||
|
groupNumber: sortOrder / 10,
|
||||||
|
sectionTotalPower: 0,
|
||||||
|
components: [
|
||||||
|
component(`${id}-rcd-late`, "group", 20, id),
|
||||||
|
component(`${id}-rcd-first`, "group", 10, id),
|
||||||
|
],
|
||||||
|
circuits: [circuit(`${id}-circuit`, id)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("circuit structure projection", () => {
|
||||||
|
it("projects fixed header, complete groups and auxiliary footer in stable order", () => {
|
||||||
|
const rows = buildCircuitStructureProjection({
|
||||||
|
headerComponents: [
|
||||||
|
component("surge", "header", 20),
|
||||||
|
component("main", "header", 10),
|
||||||
|
],
|
||||||
|
sections: [section("group-2", 20), section("group-1", 10)],
|
||||||
|
footerComponents: [
|
||||||
|
component("actor-2", "footer", 20),
|
||||||
|
component("actor-1", "footer", 10),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
rows.map((row) => row.rowKey),
|
||||||
|
[
|
||||||
|
"header-component:main",
|
||||||
|
"header-component:surge",
|
||||||
|
"group:group-1",
|
||||||
|
"group-component:group-1-rcd-first",
|
||||||
|
"group-component:group-1-rcd-late",
|
||||||
|
"circuit-block:group-1-circuit",
|
||||||
|
"group-placeholder:group-1",
|
||||||
|
"group:group-2",
|
||||||
|
"group-component:group-2-rcd-first",
|
||||||
|
"group-component:group-2-rcd-late",
|
||||||
|
"circuit-block:group-2-circuit",
|
||||||
|
"group-placeholder:group-2",
|
||||||
|
"footer-component:actor-1",
|
||||||
|
"footer-component:actor-2",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps structural rows around an empty group", () => {
|
||||||
|
const empty = section("group-empty", 10);
|
||||||
|
empty.components = [];
|
||||||
|
empty.circuits = [];
|
||||||
|
assert.deepEqual(
|
||||||
|
buildCircuitStructureProjection({
|
||||||
|
headerComponents: [],
|
||||||
|
sections: [empty],
|
||||||
|
footerComponents: [],
|
||||||
|
}).map((row) => row.rowType),
|
||||||
|
["groupHeader", "groupPlaceholder"]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps circuit device rows inside their circuit block", () => {
|
||||||
|
const grouped = section("group-1", 10);
|
||||||
|
grouped.circuits[0].deviceRows = [
|
||||||
|
{
|
||||||
|
id: "device-1",
|
||||||
|
sortOrder: 10,
|
||||||
|
name: "Leuchte",
|
||||||
|
displayName: "Leuchte",
|
||||||
|
quantity: 1,
|
||||||
|
powerPerUnit: 0.1,
|
||||||
|
simultaneityFactor: 1,
|
||||||
|
rowTotalPower: 0.1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const block = buildCircuitStructureProjection({
|
||||||
|
headerComponents: [],
|
||||||
|
sections: [grouped],
|
||||||
|
footerComponents: [],
|
||||||
|
}).find((row) => row.rowType === "circuitBlock");
|
||||||
|
assert.equal(
|
||||||
|
block?.rowType === "circuitBlock"
|
||||||
|
? block.circuit.deviceRows[0].id
|
||||||
|
: null,
|
||||||
|
"device-1"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user