Add circuit group reordering
This commit is contained in:
@@ -314,6 +314,9 @@ Empty circuit-group creation, display-name updates and deletion use
|
|||||||
Their snapshots keep category, positive group number and derived prefix
|
Their snapshots keep category, positive group number and derived prefix
|
||||||
consistent. General deletion rejects groups containing circuits or group
|
consistent. General deletion rejects groups containing circuits or group
|
||||||
components; populated deletion remains a separate confirmed command.
|
components; populated deletion remains a separate confirmed command.
|
||||||
|
`circuit-group.reorder` requires a complete expected/target sort assignment for
|
||||||
|
every group in one circuit list. It changes only sort positions and never
|
||||||
|
renumbers groups, prefixes or circuits.
|
||||||
Distribution-board floor assignment and a project-enabled supply type use
|
Distribution-board floor assignment and a project-enabled supply type use
|
||||||
`distribution-board.update`; both values are snapshot/export fields and one
|
`distribution-board.update`; both values are snapshot/export fields and one
|
||||||
persistent undo step.
|
persistent undo step.
|
||||||
|
|||||||
@@ -305,6 +305,11 @@ BMK-Präfix; allgemeine Updates ändern ausschließlich den Anzeigenamen.
|
|||||||
Löschen verlangt einen exakt unveränderten Zustand ohne Stromkreise und ohne
|
Löschen verlangt einen exakt unveränderten Zustand ohne Stromkreise und ohne
|
||||||
Gruppenkomponenten. Das bestätigte Löschen befüllter Unterbäume bleibt ein
|
Gruppenkomponenten. Das bestätigte Löschen befüllter Unterbäume bleibt ein
|
||||||
eigener späterer Command.
|
eigener späterer Command.
|
||||||
|
`circuit-group.reorder` sortiert Gruppen als vollständige Stromkreislisten-
|
||||||
|
Zuordnung. Jede vorhandene Gruppe muss mit erwarteter und neuer Position
|
||||||
|
enthalten sein. Der Command verändert ausschließlich `sortOrder`; Nummern,
|
||||||
|
Präfixe und Stromkreis-BMKs bleiben stabil. Die gesamte Sortierung bildet eine
|
||||||
|
Revision und einen Undo/Redo-Schritt.
|
||||||
`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
|
||||||
@@ -377,8 +382,8 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät.
|
|||||||
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden
|
||||||
Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für
|
Snapshot-Schema 7. Persistente Insert/Delete/Update-Commands für
|
||||||
veränderliche Verteilerkomponenten sowie CRUD-Commands für leere Gruppen
|
veränderliche Verteilerkomponenten sowie CRUD-Commands für leere Gruppen
|
||||||
sind integriert; Gruppensortierung, befüllte Unterbäume und UI folgen in
|
sowie vollständige Gruppensortierung sind integriert; befüllte Unterbäume
|
||||||
abgegrenzten Arbeitspaketen.
|
und UI folgen in abgegrenzten Arbeitspaketen.
|
||||||
|
|
||||||
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und
|
||||||
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ requirements and intended sequencing, not proof of implementation.
|
|||||||
protection and auxiliary components.
|
protection and auxiliary components.
|
||||||
- [x] Phase C2a2: component update and reorder commands.
|
- [x] Phase C2a2: component update and reorder commands.
|
||||||
- [x] Phase C2b1: empty-group insert/update/delete commands.
|
- [x] Phase C2b1: empty-group insert/update/delete commands.
|
||||||
- [ ] Phase C2b2: complete group reorder command.
|
- [x] Phase C2b2: complete group reorder command.
|
||||||
- [ ] Phase D: group numbering, moves and destructive operations.
|
- [ ] Phase D: group numbering, moves and destructive operations.
|
||||||
- [ ] 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.
|
||||||
|
|||||||
@@ -593,9 +593,7 @@ Acceptance:
|
|||||||
|
|
||||||
### C. Persistent Commands and Board Defaults
|
### C. Persistent Commands and Board Defaults
|
||||||
|
|
||||||
Status: In progress. New-board defaults C1, component management C2a and
|
Status: Complete.
|
||||||
empty-group CRUD C2b1 are complete. Complete group reordering C2b2 remains
|
|
||||||
pending.
|
|
||||||
|
|
||||||
- extend `distribution-board.insert` with fixed components and three groups
|
- extend `distribution-board.insert` with fixed components and three groups
|
||||||
- implement component and group CRUD commands
|
- implement component and group CRUD commands
|
||||||
@@ -647,6 +645,15 @@ Implemented in C2b1:
|
|||||||
- project/list ownership, stale state, Undo/Redo and late rollback are covered
|
- project/list ownership, stale state, Undo/Redo and late rollback are covered
|
||||||
at the shared transaction boundary
|
at the shared transaction boundary
|
||||||
|
|
||||||
|
Implemented in C2b2:
|
||||||
|
|
||||||
|
- `circuit-group.reorder` requires every group in the circuit list with its
|
||||||
|
exact expected and target `sortOrder`
|
||||||
|
- sorting changes no group number, prefix or circuit equipment identifier
|
||||||
|
- incomplete or stale assignments are rejected before history is recorded
|
||||||
|
- the complete reorder is one project revision and one persistent Undo/Redo
|
||||||
|
step; late failures roll back every position
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- new boards contain the agreed fixed structure
|
- new boards contain the agreed fixed structure
|
||||||
|
|||||||
@@ -2,13 +2,17 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import {
|
import {
|
||||||
assertCircuitGroupDeleteProjectCommand,
|
assertCircuitGroupDeleteProjectCommand,
|
||||||
assertCircuitGroupInsertProjectCommand,
|
assertCircuitGroupInsertProjectCommand,
|
||||||
|
assertCircuitGroupReorderProjectCommand,
|
||||||
assertCircuitGroupUpdateProjectCommand,
|
assertCircuitGroupUpdateProjectCommand,
|
||||||
circuitGroupDeleteCommandType,
|
circuitGroupDeleteCommandType,
|
||||||
circuitGroupInsertCommandType,
|
circuitGroupInsertCommandType,
|
||||||
|
circuitGroupReorderCommandType,
|
||||||
circuitGroupUpdateCommandType,
|
circuitGroupUpdateCommandType,
|
||||||
createCircuitGroupDeleteProjectCommand,
|
createCircuitGroupDeleteProjectCommand,
|
||||||
createCircuitGroupInsertProjectCommand,
|
createCircuitGroupInsertProjectCommand,
|
||||||
|
createCircuitGroupReorderProjectCommand,
|
||||||
createCircuitGroupUpdateProjectCommand,
|
createCircuitGroupUpdateProjectCommand,
|
||||||
|
type CircuitGroupReorderAssignment,
|
||||||
type CircuitGroupSnapshot,
|
type CircuitGroupSnapshot,
|
||||||
type CircuitGroupStructureProjectCommand,
|
type CircuitGroupStructureProjectCommand,
|
||||||
} from "../../domain/models/circuit-group-structure-project-command.model.js";
|
} from "../../domain/models/circuit-group-structure-project-command.model.js";
|
||||||
@@ -68,6 +72,19 @@ export class CircuitGroupStructureProjectCommandRepository
|
|||||||
command.payload.expected
|
command.payload.expected
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (command.type === circuitGroupReorderCommandType) {
|
||||||
|
assertCircuitGroupReorderProjectCommand(command);
|
||||||
|
const inverseAssignments = this.reorder(
|
||||||
|
database,
|
||||||
|
projectId,
|
||||||
|
command.payload.circuitListId,
|
||||||
|
command.payload.assignments
|
||||||
|
);
|
||||||
|
return createCircuitGroupReorderProjectCommand(
|
||||||
|
command.payload.circuitListId,
|
||||||
|
inverseAssignments
|
||||||
|
);
|
||||||
|
}
|
||||||
throw new Error("Unsupported circuit-group structure command.");
|
throw new Error("Unsupported circuit-group structure command.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +185,61 @@ export class CircuitGroupStructureProjectCommandRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private reorder(
|
||||||
|
database: AppDatabase,
|
||||||
|
projectId: string,
|
||||||
|
circuitListId: string,
|
||||||
|
assignments: CircuitGroupReorderAssignment[]
|
||||||
|
) {
|
||||||
|
this.assertListOwnership(database, projectId, circuitListId);
|
||||||
|
const persisted = database
|
||||||
|
.select({
|
||||||
|
id: circuitSections.id,
|
||||||
|
sortOrder: circuitSections.sortOrder,
|
||||||
|
})
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
||||||
|
.all();
|
||||||
|
const persistedById = new Map(
|
||||||
|
persisted.map((group) => [group.id, group])
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
persisted.length !== assignments.length ||
|
||||||
|
assignments.some((assignment) => {
|
||||||
|
const group = persistedById.get(assignment.groupId);
|
||||||
|
return !group || group.sortOrder !== assignment.expectedSortOrder;
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuit-group reorder must include every unchanged group in the list."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
if (assignment.expectedSortOrder === assignment.targetSortOrder) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const updated = database
|
||||||
|
.update(circuitSections)
|
||||||
|
.set({ sortOrder: assignment.targetSortOrder })
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(circuitSections.id, assignment.groupId),
|
||||||
|
eq(circuitSections.circuitListId, circuitListId),
|
||||||
|
eq(circuitSections.sortOrder, assignment.expectedSortOrder)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.run();
|
||||||
|
if (updated.changes !== 1) {
|
||||||
|
throw new Error("Circuit group changed during reorder.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return assignments.map((assignment) => ({
|
||||||
|
groupId: assignment.groupId,
|
||||||
|
expectedSortOrder: assignment.targetSortOrder,
|
||||||
|
targetSortOrder: assignment.expectedSortOrder,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
private getCurrent(database: AppDatabase, id: string) {
|
private getCurrent(database: AppDatabase, id: string) {
|
||||||
return database
|
return database
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { SerializedProjectCommand } from "./project-command.model.js";
|
|||||||
export const circuitGroupInsertCommandType = "circuit-group.insert" as const;
|
export const circuitGroupInsertCommandType = "circuit-group.insert" as const;
|
||||||
export const circuitGroupDeleteCommandType = "circuit-group.delete" as const;
|
export const circuitGroupDeleteCommandType = "circuit-group.delete" as const;
|
||||||
export const circuitGroupUpdateCommandType = "circuit-group.update" as const;
|
export const circuitGroupUpdateCommandType = "circuit-group.update" as const;
|
||||||
|
export const circuitGroupReorderCommandType = "circuit-group.reorder" as const;
|
||||||
export const circuitGroupStructureCommandSchemaVersion = 1 as const;
|
export const circuitGroupStructureCommandSchemaVersion = 1 as const;
|
||||||
|
|
||||||
export interface CircuitGroupSnapshot {
|
export interface CircuitGroupSnapshot {
|
||||||
@@ -30,6 +31,17 @@ interface UpdatePayload {
|
|||||||
target: CircuitGroupSnapshot;
|
target: CircuitGroupSnapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitGroupReorderAssignment {
|
||||||
|
groupId: string;
|
||||||
|
expectedSortOrder: number;
|
||||||
|
targetSortOrder: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReorderPayload {
|
||||||
|
circuitListId: string;
|
||||||
|
assignments: CircuitGroupReorderAssignment[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CircuitGroupInsertProjectCommand
|
export interface CircuitGroupInsertProjectCommand
|
||||||
extends SerializedProjectCommand<SnapshotPayload> {
|
extends SerializedProjectCommand<SnapshotPayload> {
|
||||||
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
||||||
@@ -48,10 +60,17 @@ export interface CircuitGroupUpdateProjectCommand
|
|||||||
type: typeof circuitGroupUpdateCommandType;
|
type: typeof circuitGroupUpdateCommandType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CircuitGroupReorderProjectCommand
|
||||||
|
extends SerializedProjectCommand<ReorderPayload> {
|
||||||
|
schemaVersion: typeof circuitGroupStructureCommandSchemaVersion;
|
||||||
|
type: typeof circuitGroupReorderCommandType;
|
||||||
|
}
|
||||||
|
|
||||||
export type CircuitGroupStructureProjectCommand =
|
export type CircuitGroupStructureProjectCommand =
|
||||||
| CircuitGroupInsertProjectCommand
|
| CircuitGroupInsertProjectCommand
|
||||||
| CircuitGroupDeleteProjectCommand
|
| CircuitGroupDeleteProjectCommand
|
||||||
| CircuitGroupUpdateProjectCommand;
|
| CircuitGroupUpdateProjectCommand
|
||||||
|
| CircuitGroupReorderProjectCommand;
|
||||||
|
|
||||||
export function createCircuitGroupInsertProjectCommand(
|
export function createCircuitGroupInsertProjectCommand(
|
||||||
snapshot: CircuitGroupSnapshot
|
snapshot: CircuitGroupSnapshot
|
||||||
@@ -90,6 +109,19 @@ export function createCircuitGroupUpdateProjectCommand(
|
|||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createCircuitGroupReorderProjectCommand(
|
||||||
|
circuitListId: string,
|
||||||
|
assignments: CircuitGroupReorderAssignment[]
|
||||||
|
): CircuitGroupReorderProjectCommand {
|
||||||
|
const command: CircuitGroupReorderProjectCommand = {
|
||||||
|
schemaVersion: circuitGroupStructureCommandSchemaVersion,
|
||||||
|
type: circuitGroupReorderCommandType,
|
||||||
|
payload: { circuitListId, assignments },
|
||||||
|
};
|
||||||
|
assertCircuitGroupReorderProjectCommand(command);
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
export function assertCircuitGroupInsertProjectCommand(
|
export function assertCircuitGroupInsertProjectCommand(
|
||||||
command: SerializedProjectCommand<unknown>
|
command: SerializedProjectCommand<unknown>
|
||||||
): asserts command is CircuitGroupInsertProjectCommand {
|
): asserts command is CircuitGroupInsertProjectCommand {
|
||||||
@@ -132,6 +164,44 @@ export function assertCircuitGroupUpdateProjectCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function assertCircuitGroupReorderProjectCommand(
|
||||||
|
command: SerializedProjectCommand<unknown>
|
||||||
|
): asserts command is CircuitGroupReorderProjectCommand {
|
||||||
|
if (
|
||||||
|
command.schemaVersion !== circuitGroupStructureCommandSchemaVersion ||
|
||||||
|
command.type !== circuitGroupReorderCommandType ||
|
||||||
|
!isPlainObject(command.payload) ||
|
||||||
|
Object.keys(command.payload).length !== 2 ||
|
||||||
|
typeof command.payload.circuitListId !== "string" ||
|
||||||
|
!command.payload.circuitListId.trim() ||
|
||||||
|
!Array.isArray(command.payload.assignments) ||
|
||||||
|
command.payload.assignments.length === 0
|
||||||
|
) {
|
||||||
|
throw new Error("Unsupported circuit-group reorder command.");
|
||||||
|
}
|
||||||
|
const groupIds = new Set<string>();
|
||||||
|
let hasChange = false;
|
||||||
|
for (const assignment of command.payload.assignments) {
|
||||||
|
if (
|
||||||
|
!isPlainObject(assignment) ||
|
||||||
|
typeof assignment.groupId !== "string" ||
|
||||||
|
!assignment.groupId.trim() ||
|
||||||
|
!Number.isFinite(assignment.expectedSortOrder) ||
|
||||||
|
!Number.isFinite(assignment.targetSortOrder) ||
|
||||||
|
groupIds.has(assignment.groupId)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"Circuit-group reorder contains an invalid or duplicate assignment."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
hasChange ||= assignment.expectedSortOrder !== assignment.targetSortOrder;
|
||||||
|
groupIds.add(assignment.groupId);
|
||||||
|
}
|
||||||
|
if (!hasChange) {
|
||||||
|
throw new Error("Circuit-group reorder must change at least one position.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function assertSnapshotCommand(
|
function assertSnapshotCommand(
|
||||||
command: SerializedProjectCommand<unknown>,
|
command: SerializedProjectCommand<unknown>,
|
||||||
type:
|
type:
|
||||||
|
|||||||
@@ -114,9 +114,11 @@ import type {
|
|||||||
import {
|
import {
|
||||||
assertCircuitGroupDeleteProjectCommand,
|
assertCircuitGroupDeleteProjectCommand,
|
||||||
assertCircuitGroupInsertProjectCommand,
|
assertCircuitGroupInsertProjectCommand,
|
||||||
|
assertCircuitGroupReorderProjectCommand,
|
||||||
assertCircuitGroupUpdateProjectCommand,
|
assertCircuitGroupUpdateProjectCommand,
|
||||||
circuitGroupDeleteCommandType,
|
circuitGroupDeleteCommandType,
|
||||||
circuitGroupInsertCommandType,
|
circuitGroupInsertCommandType,
|
||||||
|
circuitGroupReorderCommandType,
|
||||||
circuitGroupUpdateCommandType,
|
circuitGroupUpdateCommandType,
|
||||||
} from "../models/circuit-group-structure-project-command.model.js";
|
} from "../models/circuit-group-structure-project-command.model.js";
|
||||||
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
|
import type { ProjectLocationStructureProjectCommandStore } from "../ports/project-location-structure-project-command.store.js";
|
||||||
@@ -364,6 +366,13 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||||||
command: input.command,
|
command: input.command,
|
||||||
}).revision;
|
}).revision;
|
||||||
}
|
}
|
||||||
|
case circuitGroupReorderCommandType: {
|
||||||
|
assertCircuitGroupReorderProjectCommand(input.command);
|
||||||
|
return this.circuitGroupStructureStore.execute({
|
||||||
|
...input,
|
||||||
|
command: input.command,
|
||||||
|
}).revision;
|
||||||
|
}
|
||||||
case projectFloorInsertCommandType: {
|
case projectFloorInsertCommandType: {
|
||||||
assertProjectFloorInsertProjectCommand(input.command);
|
assertProjectFloorInsertProjectCommand(input.command);
|
||||||
return this.projectLocationStructureStore.execute({
|
return this.projectLocationStructureStore.execute({
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const commandTypeLabels: Record<string, string> = {
|
|||||||
"circuit-group.insert": "Stromkreisgruppe angelegt",
|
"circuit-group.insert": "Stromkreisgruppe angelegt",
|
||||||
"circuit-group.delete": "Stromkreisgruppe entfernt",
|
"circuit-group.delete": "Stromkreisgruppe entfernt",
|
||||||
"circuit-group.update": "Stromkreisgruppe bearbeitet",
|
"circuit-group.update": "Stromkreisgruppe bearbeitet",
|
||||||
|
"circuit-group.reorder": "Stromkreisgruppen sortiert",
|
||||||
"project-floor.insert": "Geschoss angelegt",
|
"project-floor.insert": "Geschoss angelegt",
|
||||||
"project-floor.delete": "Geschoss entfernt",
|
"project-floor.delete": "Geschoss entfernt",
|
||||||
"project-room.insert": "Raum angelegt",
|
"project-room.insert": "Raum angelegt",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { projects } from "../src/db/schema/projects.js";
|
|||||||
import {
|
import {
|
||||||
createCircuitGroupDeleteProjectCommand,
|
createCircuitGroupDeleteProjectCommand,
|
||||||
createCircuitGroupInsertProjectCommand,
|
createCircuitGroupInsertProjectCommand,
|
||||||
|
createCircuitGroupReorderProjectCommand,
|
||||||
createCircuitGroupUpdateProjectCommand,
|
createCircuitGroupUpdateProjectCommand,
|
||||||
type CircuitGroupSnapshot,
|
type CircuitGroupSnapshot,
|
||||||
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
|
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
|
||||||
@@ -293,4 +294,137 @@ describe("circuit-group structure project command", () => {
|
|||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reorders every group atomically without changing group identity", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const circuitListId = getProjectList(context, "project-1").id;
|
||||||
|
const groups = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
||||||
|
.all();
|
||||||
|
const expectedPrefixes = new Map(
|
||||||
|
groups.map((group) => [group.id, group.prefix])
|
||||||
|
);
|
||||||
|
const assignments = groups.map((group, index) => ({
|
||||||
|
groupId: group.id,
|
||||||
|
expectedSortOrder: group.sortOrder,
|
||||||
|
targetSortOrder: (groups.length - index) * 10,
|
||||||
|
}));
|
||||||
|
const repository =
|
||||||
|
new CircuitGroupStructureProjectCommandRepository(context.db);
|
||||||
|
const reordered = repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitGroupReorderProjectCommand(
|
||||||
|
circuitListId,
|
||||||
|
assignments
|
||||||
|
),
|
||||||
|
});
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
const group = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.id, assignment.groupId))
|
||||||
|
.get();
|
||||||
|
assert.equal(group?.sortOrder, assignment.targetSortOrder);
|
||||||
|
assert.equal(
|
||||||
|
group?.prefix,
|
||||||
|
expectedPrefixes.get(assignment.groupId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 1,
|
||||||
|
source: "undo",
|
||||||
|
historyTargetChangeSetId: new ProjectHistoryRepository(
|
||||||
|
context.db
|
||||||
|
).getNextCommand("project-1", "undo")?.changeSetId,
|
||||||
|
command: reordered.inverse,
|
||||||
|
});
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.id, assignment.groupId))
|
||||||
|
.get()?.sortOrder,
|
||||||
|
assignment.expectedSortOrder
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects incomplete reorders and rolls back late failures", () => {
|
||||||
|
const context = createTestDatabase();
|
||||||
|
try {
|
||||||
|
const circuitListId = getProjectList(context, "project-1").id;
|
||||||
|
const groups = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
||||||
|
.all();
|
||||||
|
const assignments = groups.map((group, index) => ({
|
||||||
|
groupId: group.id,
|
||||||
|
expectedSortOrder: group.sortOrder,
|
||||||
|
targetSortOrder: (groups.length - index) * 10,
|
||||||
|
}));
|
||||||
|
const repository =
|
||||||
|
new CircuitGroupStructureProjectCommandRepository(context.db);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitGroupReorderProjectCommand(
|
||||||
|
circuitListId,
|
||||||
|
assignments.slice(1)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/include every unchanged group/
|
||||||
|
);
|
||||||
|
context.sqlite.exec(`
|
||||||
|
CREATE TRIGGER fail_group_reorder_history
|
||||||
|
BEFORE INSERT ON project_history_stack_entries
|
||||||
|
BEGIN
|
||||||
|
SELECT RAISE(ABORT, 'forced group reorder history failure');
|
||||||
|
END;
|
||||||
|
`);
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
repository.execute({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 0,
|
||||||
|
source: "user",
|
||||||
|
command: createCircuitGroupReorderProjectCommand(
|
||||||
|
circuitListId,
|
||||||
|
assignments
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
/forced group reorder history failure/
|
||||||
|
);
|
||||||
|
for (const assignment of assignments) {
|
||||||
|
assert.equal(
|
||||||
|
context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.id, assignment.groupId))
|
||||||
|
.get()?.sortOrder,
|
||||||
|
assignment.expectedSortOrder
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert.equal(
|
||||||
|
context.db.select().from(projectRevisions).all().length,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import { createCircuitSectionRenumberProjectCommand } from "../src/domain/models
|
|||||||
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js";
|
import { createCircuitInsertProjectCommand } from "../src/domain/models/circuit-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
createCircuitGroupInsertProjectCommand,
|
createCircuitGroupInsertProjectCommand,
|
||||||
|
createCircuitGroupReorderProjectCommand,
|
||||||
createCircuitGroupUpdateProjectCommand,
|
createCircuitGroupUpdateProjectCommand,
|
||||||
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
|
} from "../src/domain/models/circuit-group-structure-project-command.model.js";
|
||||||
import {
|
import {
|
||||||
@@ -1270,6 +1271,26 @@ describe("project command service", () => {
|
|||||||
.get()?.displayName,
|
.get()?.displayName,
|
||||||
expected.displayName
|
expected.displayName
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groups = context.db
|
||||||
|
.select()
|
||||||
|
.from(circuitSections)
|
||||||
|
.where(eq(circuitSections.circuitListId, circuitListId))
|
||||||
|
.all();
|
||||||
|
const reorderAssignments = groups.map((group, index) => ({
|
||||||
|
groupId: group.id,
|
||||||
|
expectedSortOrder: group.sortOrder,
|
||||||
|
targetSortOrder: (groups.length - index) * 10,
|
||||||
|
}));
|
||||||
|
const reordered = createService(context).executeUser({
|
||||||
|
projectId: "project-1",
|
||||||
|
expectedRevision: 3,
|
||||||
|
command: createCircuitGroupReorderProjectCommand(
|
||||||
|
circuitListId,
|
||||||
|
reorderAssignments
|
||||||
|
),
|
||||||
|
});
|
||||||
|
assert.equal(reordered.history.currentRevision, 4);
|
||||||
} finally {
|
} finally {
|
||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,12 @@ describe("project version history presentation", () => {
|
|||||||
),
|
),
|
||||||
"Stromkreisgruppe bearbeitet"
|
"Stromkreisgruppe bearbeitet"
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
getProjectRevisionDescription(
|
||||||
|
revision(11, { commandType: "circuit-group.reorder" })
|
||||||
|
),
|
||||||
|
"Stromkreisgruppen sortiert"
|
||||||
|
);
|
||||||
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
|
assert.equal(getProjectSnapshotKindLabel("named"), "Benannt");
|
||||||
assert.equal(
|
assert.equal(
|
||||||
getProjectSnapshotKindLabel("automatic"),
|
getProjectSnapshotKindLabel("automatic"),
|
||||||
|
|||||||
Reference in New Issue
Block a user