forked from jappel/leistungsbilanz-ts
Add cell copy/paste, multi-row edits and a context menu to the circuit grid
Ctrl+C/Ctrl+V copy and paste grid cells without opening an editor, using the document copy/paste events so it also works over plain HTTP in the LAN. Protection and cable summaries copy the complete circuit record; tab-separated blocks from Excel spread over visible rows and columns. With a Ctrl/Shift multi-selection, typing, pasting and the protection dialog apply to every selected row that can take the value, including device power and GZF; other cells are skipped and reported. All of these writes use the new circuit-list.bulk-update command. It carries complete circuit.update, circuit-device-row.update and circuit-protection.update commands, applies them with the single-command rules in one transaction and stores the reversed inverses, so each edit is one undo step. The single-command apply logic is extracted for reuse. A right-click menu (also context-menu key / Shift+F10) offers copy, paste, edit, protection, cable sizing, insert circuit below, add device and delete. The protection summary is now selected by click and opened by double click, Enter or F2. Existing numbers open in the inline editor with a decimal comma, so confirming an unchanged decimal no longer fails. Refs #26 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
07bc52a134
commit
17a3945813
18 changed files with 2548 additions and 391 deletions
|
|
@ -0,0 +1,111 @@
|
|||
import {
|
||||
assertCircuitDeviceRowUpdateProjectCommand,
|
||||
circuitDeviceRowUpdateCommandType,
|
||||
type CircuitDeviceRowUpdateProjectCommand,
|
||||
} from "./circuit-device-row-project-command.model.js";
|
||||
import {
|
||||
assertCircuitUpdateProjectCommand,
|
||||
circuitUpdateCommandType,
|
||||
type CircuitUpdateProjectCommand,
|
||||
} from "./circuit-project-command.model.js";
|
||||
import {
|
||||
assertCircuitProtectionUpdateProjectCommand,
|
||||
circuitProtectionUpdateCommandType,
|
||||
type CircuitProtectionUpdateProjectCommand,
|
||||
} from "./circuit-protection-project-command.model.js";
|
||||
import type { SerializedProjectCommand } from "./project-command.model.js";
|
||||
|
||||
// Several cell edits of the circuit list (multi-selection edit or paste)
|
||||
// as one atomic command and one undo step. Each update is a complete
|
||||
// single-entity command; its inverse is the reversed list of the applied
|
||||
// inverses, so Undo/Redo never re-plans the edit.
|
||||
export const circuitListBulkUpdateCommandType =
|
||||
"circuit-list.bulk-update" as const;
|
||||
export const circuitListBulkUpdateCommandSchemaVersion = 1 as const;
|
||||
export const circuitListBulkUpdateMaxUpdates = 2000;
|
||||
|
||||
export type CircuitListBulkUpdateEntry =
|
||||
| CircuitUpdateProjectCommand
|
||||
| CircuitDeviceRowUpdateProjectCommand
|
||||
| CircuitProtectionUpdateProjectCommand;
|
||||
|
||||
export interface CircuitListBulkUpdateCommandPayload {
|
||||
updates: CircuitListBulkUpdateEntry[];
|
||||
}
|
||||
|
||||
export interface CircuitListBulkUpdateProjectCommand
|
||||
extends SerializedProjectCommand<CircuitListBulkUpdateCommandPayload> {
|
||||
schemaVersion: typeof circuitListBulkUpdateCommandSchemaVersion;
|
||||
type: typeof circuitListBulkUpdateCommandType;
|
||||
}
|
||||
|
||||
export function createCircuitListBulkUpdateProjectCommand(
|
||||
updates: CircuitListBulkUpdateEntry[]
|
||||
): CircuitListBulkUpdateProjectCommand {
|
||||
const command: CircuitListBulkUpdateProjectCommand = {
|
||||
schemaVersion: circuitListBulkUpdateCommandSchemaVersion,
|
||||
type: circuitListBulkUpdateCommandType,
|
||||
payload: { updates },
|
||||
};
|
||||
assertCircuitListBulkUpdateProjectCommand(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
export function assertCircuitListBulkUpdateProjectCommand(
|
||||
command: SerializedProjectCommand<unknown>
|
||||
): asserts command is CircuitListBulkUpdateProjectCommand {
|
||||
if (
|
||||
command.schemaVersion !== circuitListBulkUpdateCommandSchemaVersion ||
|
||||
command.type !== circuitListBulkUpdateCommandType ||
|
||||
!isPlainObject(command.payload) ||
|
||||
Object.keys(command.payload).length !== 1
|
||||
) {
|
||||
throw new Error("Unsupported circuit-list bulk update command.");
|
||||
}
|
||||
const updates = command.payload.updates;
|
||||
if (
|
||||
!Array.isArray(updates) ||
|
||||
updates.length === 0 ||
|
||||
updates.length > circuitListBulkUpdateMaxUpdates
|
||||
) {
|
||||
throw new Error(
|
||||
`Circuit-list bulk update requires 1 to ${circuitListBulkUpdateMaxUpdates} updates.`
|
||||
);
|
||||
}
|
||||
const seenTargets = new Set<string>();
|
||||
for (const update of updates) {
|
||||
if (!isPlainObject(update)) {
|
||||
throw new Error("Circuit-list bulk update contains an invalid update.");
|
||||
}
|
||||
const target = getBulkUpdateTargetKey(
|
||||
update as unknown as SerializedProjectCommand<unknown>
|
||||
);
|
||||
if (seenTargets.has(target)) {
|
||||
throw new Error("Circuit-list bulk update changes one target twice.");
|
||||
}
|
||||
seenTargets.add(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Validates one nested update and returns its unique type/entity key.
|
||||
function getBulkUpdateTargetKey(
|
||||
update: SerializedProjectCommand<unknown>
|
||||
): string {
|
||||
if (update.type === circuitUpdateCommandType) {
|
||||
assertCircuitUpdateProjectCommand(update);
|
||||
return `${update.type}:${update.payload.circuitId}`;
|
||||
}
|
||||
if (update.type === circuitDeviceRowUpdateCommandType) {
|
||||
assertCircuitDeviceRowUpdateProjectCommand(update);
|
||||
return `${update.type}:${update.payload.rowId}`;
|
||||
}
|
||||
if (update.type === circuitProtectionUpdateCommandType) {
|
||||
assertCircuitProtectionUpdateProjectCommand(update);
|
||||
return `${update.type}:${update.payload.circuitId}`;
|
||||
}
|
||||
throw new Error("Circuit-list bulk update contains an unsupported update.");
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import type { CircuitListBulkUpdateProjectCommand } from "../models/circuit-list-bulk-update-project-command.model.js";
|
||||
import type {
|
||||
AppendedProjectRevision,
|
||||
ProjectRevisionSource,
|
||||
} from "./project-revision.store.js";
|
||||
|
||||
export interface ExecuteCircuitListBulkUpdateCommandInput {
|
||||
projectId: string;
|
||||
expectedRevision: number;
|
||||
source: ProjectRevisionSource;
|
||||
description?: string;
|
||||
actorId?: string;
|
||||
historyTargetChangeSetId?: string;
|
||||
command: CircuitListBulkUpdateProjectCommand;
|
||||
}
|
||||
|
||||
export interface CircuitListBulkUpdateProjectCommandStore {
|
||||
execute(input: ExecuteCircuitListBulkUpdateCommandInput): {
|
||||
revision: AppendedProjectRevision;
|
||||
inverse: CircuitListBulkUpdateProjectCommand;
|
||||
};
|
||||
}
|
||||
|
|
@ -214,6 +214,11 @@ import {
|
|||
estimateBalanceUpdateCommandType,
|
||||
} from "../../estimate-balance/domain/estimate-balance-command.model.js";
|
||||
import type { EstimateBalanceProjectCommandStore } from "../../estimate-balance/domain/estimate-balance-project-command.store.js";
|
||||
import {
|
||||
assertCircuitListBulkUpdateProjectCommand,
|
||||
circuitListBulkUpdateCommandType,
|
||||
} from "../models/circuit-list-bulk-update-project-command.model.js";
|
||||
import type { CircuitListBulkUpdateProjectCommandStore } from "../ports/circuit-list-bulk-update-project-command.store.js";
|
||||
|
||||
interface DispatchProjectCommandInput {
|
||||
projectId: string;
|
||||
|
|
@ -257,7 +262,8 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||
private readonly externalInitialImportStore?: ExternalInitialImportProjectCommandStore,
|
||||
private readonly externalObjectPlanningStore?: ExternalObjectPlanningProjectCommandStore,
|
||||
private readonly externalFollowUpImportStore?: ExternalFollowUpImportProjectCommandStore,
|
||||
private readonly externalObjectDeleteMissingStore?: ExternalObjectDeleteMissingProjectCommandStore
|
||||
private readonly externalObjectDeleteMissingStore?: ExternalObjectDeleteMissingProjectCommandStore,
|
||||
private readonly circuitListBulkUpdateStore?: CircuitListBulkUpdateProjectCommandStore
|
||||
) {}
|
||||
|
||||
executeUser(
|
||||
|
|
@ -532,6 +538,16 @@ export class ProjectCommandService implements ProjectCommandExecutor {
|
|||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case circuitListBulkUpdateCommandType: {
|
||||
assertCircuitListBulkUpdateProjectCommand(input.command);
|
||||
if (!this.circuitListBulkUpdateStore) {
|
||||
throw new Error("Circuit-list bulk update store is not available.");
|
||||
}
|
||||
return this.circuitListBulkUpdateStore.execute({
|
||||
...input,
|
||||
command: input.command,
|
||||
}).revision;
|
||||
}
|
||||
case projectBuildingInsertCommandType: {
|
||||
assertProjectBuildingInsertProjectCommand(input.command);
|
||||
return this.projectLocationStructureStore.execute({ ...input, command: input.command }).revision;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue