Restore circuit drag placement
This commit is contained in:
@@ -83,6 +83,10 @@ Intent is separated by drag source type:
|
||||
- device row drag:
|
||||
- drop to existing circuit -> move row(s) into that circuit
|
||||
- drop to placeholder -> create new target circuit and move row(s)
|
||||
- drop on the upper or lower edge of a circuit row -> create a new target
|
||||
circuit directly before or after that circuit and move the row(s)
|
||||
- the center of a circuit row remains the explicit target for adding the
|
||||
row(s) to that existing circuit
|
||||
- crossing a section boundary requires explicit confirmation
|
||||
- phase type, category, linked project device and local row values remain unchanged
|
||||
- target creation, row assignments and reserve-state updates use one SQLite transaction
|
||||
@@ -107,6 +111,8 @@ Cross-section device moves show confirmation-required feedback before drop. The
|
||||
|
||||
- New groups are created manually in one of the three supported categories and
|
||||
receive the highest existing category group number plus one.
|
||||
- Group headings always show the stored editable display name; category labels
|
||||
do not overwrite names of the default first groups.
|
||||
- Group reorder buttons move only relative to category peers and never change a
|
||||
group number or BMK.
|
||||
- `Gruppen-BMK neu nummerieren` is an explicit confirmed category-wide action.
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
resolveProjectVoltage,
|
||||
} from "../../domain/services/project-voltage.service";
|
||||
import {
|
||||
getAdjacentInsertionSortOrder,
|
||||
getInsertionSortOrder,
|
||||
isGridInsertionRowType,
|
||||
resolveGridInsertionIntent,
|
||||
@@ -175,7 +176,13 @@ type ProjectDeviceDropIntent =
|
||||
|
||||
type DeviceRowMoveDropIntent =
|
||||
| { kind: "move-to-circuit"; circuitId: string; sectionId: string; requiresConfirmation: boolean }
|
||||
| { kind: "move-to-new-circuit"; sectionId: string; requiresConfirmation: boolean };
|
||||
| {
|
||||
kind: "move-to-new-circuit";
|
||||
sectionId: string;
|
||||
requiresConfirmation: boolean;
|
||||
targetCircuitId?: string;
|
||||
placement?: "before" | "after";
|
||||
};
|
||||
|
||||
type CircuitReorderDropIntent =
|
||||
| { kind: "before-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
@@ -2329,6 +2336,34 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
);
|
||||
}
|
||||
|
||||
function resolveDeviceRowCircuitDropIntent(
|
||||
event: DragEvent<HTMLElement>,
|
||||
circuitId: string,
|
||||
sectionId: string,
|
||||
requiresConfirmation: boolean
|
||||
): DeviceRowMoveDropIntent {
|
||||
const rect = (
|
||||
event.currentTarget as HTMLTableRowElement
|
||||
).getBoundingClientRect();
|
||||
const relativeY =
|
||||
rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
|
||||
if (relativeY <= 0.25 || relativeY >= 0.75) {
|
||||
return {
|
||||
kind: "move-to-new-circuit",
|
||||
sectionId,
|
||||
requiresConfirmation,
|
||||
targetCircuitId: circuitId,
|
||||
placement: relativeY <= 0.25 ? "before" : "after",
|
||||
};
|
||||
}
|
||||
return {
|
||||
kind: "move-to-circuit",
|
||||
circuitId,
|
||||
sectionId,
|
||||
requiresConfirmation,
|
||||
};
|
||||
}
|
||||
|
||||
// Applies same-section circuit reorder as explicit id ordering.
|
||||
// No implicit renumbering is performed here.
|
||||
function resolveCircuitReorderOrder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) {
|
||||
@@ -2512,13 +2547,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
redo: async () => {
|
||||
const next = await getNextCircuitIdentifier(intent.sectionId);
|
||||
const sortOrder =
|
||||
targetSection.circuits.length > 0
|
||||
? Math.max(
|
||||
...targetSection.circuits.map(
|
||||
(circuit) => circuit.sortOrder
|
||||
)
|
||||
) + 10
|
||||
: 10;
|
||||
intent.targetCircuitId && intent.placement
|
||||
? getAdjacentInsertionSortOrder(
|
||||
targetSection.circuits,
|
||||
intent.targetCircuitId,
|
||||
intent.placement
|
||||
)
|
||||
: getInsertionSortOrder(targetSection.circuits);
|
||||
const targetCircuit = createCircuitSnapshot({
|
||||
sectionId: intent.sectionId,
|
||||
equipmentIdentifier: next.nextIdentifier,
|
||||
@@ -3841,6 +3876,28 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
deviceMoveIntent.requiresConfirmation
|
||||
? "drop-target-confirm"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.targetCircuitId === row.circuit.id
|
||||
? deviceMoveIntent.requiresConfirmation
|
||||
? "drop-target-confirm"
|
||||
: "drop-target-active"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.targetCircuitId === row.circuit.id &&
|
||||
deviceMoveIntent.placement === "before"
|
||||
? "circuit-insert-before"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.targetCircuitId === row.circuit.id &&
|
||||
deviceMoveIntent.placement === "after"
|
||||
? "circuit-insert-after"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
deviceMoveIntent?.kind === "move-to-circuit" &&
|
||||
@@ -3985,15 +4042,25 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const sourceCircuitIds = activeDraggedDeviceRowIds
|
||||
.map((id) => findDeviceRowCircuitId(id))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
if (sourceCircuitIds.some((sourceCircuitId) => sourceCircuitId !== row.circuit!.id)) {
|
||||
const intent =
|
||||
resolveDeviceRowCircuitDropIntent(
|
||||
event,
|
||||
row.circuit.id,
|
||||
row.sectionId,
|
||||
requiresConfirmation
|
||||
);
|
||||
if (
|
||||
intent.kind === "move-to-new-circuit" ||
|
||||
sourceCircuitIds.some(
|
||||
(sourceCircuitId) =>
|
||||
sourceCircuitId !== row.circuit!.id
|
||||
)
|
||||
) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDeviceMoveIntent({
|
||||
kind: "move-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
setDeviceMoveIntent(intent);
|
||||
} else {
|
||||
setDeviceMoveIntent(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4018,6 +4085,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (current.kind === "move-to-new-circuit" && row.rowType === "placeholder" && current.sectionId === row.sectionId) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
current.kind === "move-to-new-circuit" &&
|
||||
current.targetCircuitId === row.circuit?.id
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "move-to-circuit" && current.circuitId === row.circuit?.id) {
|
||||
return null;
|
||||
}
|
||||
@@ -4096,12 +4169,15 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
void handleDeviceRowDropWithIntent(event, {
|
||||
kind: "move-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
void handleDeviceRowDropWithIntent(
|
||||
event,
|
||||
resolveDeviceRowCircuitDropIntent(
|
||||
event,
|
||||
row.circuit.id,
|
||||
row.sectionId,
|
||||
requiresConfirmation
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -4140,9 +4216,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
row.device?.id &&
|
||||
((draggingDeviceRowIds.length > 0 && draggingDeviceRowIds.includes(row.device.id)) ||
|
||||
(draggingDeviceRowIds.length === 0 && draggingDeviceRowId === row.device.id))
|
||||
? "device-dragging"
|
||||
: ""
|
||||
}`}
|
||||
? "device-dragging"
|
||||
: ""
|
||||
}`}
|
||||
title={
|
||||
column.key === "equipmentIdentifier" &&
|
||||
row.circuit &&
|
||||
(row.rowType === "circuitCompact" ||
|
||||
row.rowType === "circuitSummary" ||
|
||||
row.rowType === "reserveCircuit")
|
||||
? "Stromkreis verschieben: am BMK ziehen und zwischen Stromkreisen ablegen"
|
||||
: column.key === "displayName" &&
|
||||
row.device
|
||||
? "Gerät verschieben: in der Zeilenmitte zuordnen oder am oberen/unteren Rand einen neuen Stromkreis bilden"
|
||||
: undefined
|
||||
}
|
||||
draggable={
|
||||
!isEditing &&
|
||||
((Boolean(row.device) &&
|
||||
@@ -4384,6 +4472,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
{`${draggingDeviceCount || 1} Gerätezeile(n) in diesen Stromkreis verschieben${deviceMoveIntent.requiresConfirmation ? " (Bestätigung erforderlich)" : ""}`}
|
||||
</span>
|
||||
) : null}
|
||||
{deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.targetCircuitId ===
|
||||
row.circuit?.id ? (
|
||||
<span className="drop-hint">
|
||||
{`${draggingDeviceCount || 1} Gerätezeile(n) in einen neuen Stromkreis ${
|
||||
deviceMoveIntent.placement === "before"
|
||||
? "davor"
|
||||
: "danach"
|
||||
} verschieben${
|
||||
deviceMoveIntent.requiresConfirmation
|
||||
? " (Bestätigung erforderlich)"
|
||||
: ""
|
||||
}`}
|
||||
</span>
|
||||
) : null}
|
||||
{circuitReorderIntent?.kind === "section-end" &&
|
||||
row.rowType === "placeholder" &&
|
||||
circuitReorderIntent.sectionId === row.sectionId ? (
|
||||
|
||||
@@ -87,3 +87,26 @@ export function getInsertionSortOrder(
|
||||
const next = entries[index + 1]?.sortOrder;
|
||||
return next === undefined ? current + 10 : current + (next - current) / 2;
|
||||
}
|
||||
|
||||
export function getAdjacentInsertionSortOrder(
|
||||
orderedEntries: Array<{ id: string; sortOrder: number }>,
|
||||
targetId: string,
|
||||
placement: "before" | "after"
|
||||
): number {
|
||||
const entries = [...orderedEntries].sort(
|
||||
(left, right) =>
|
||||
left.sortOrder - right.sortOrder ||
|
||||
left.id.localeCompare(right.id)
|
||||
);
|
||||
const index = entries.findIndex((entry) => entry.id === targetId);
|
||||
if (index < 0) {
|
||||
throw new Error("Der Zielstromkreis wurde nicht gefunden.");
|
||||
}
|
||||
if (placement === "after") {
|
||||
return getInsertionSortOrder(entries, targetId);
|
||||
}
|
||||
const previous = entries[index - 1];
|
||||
return previous
|
||||
? getInsertionSortOrder(entries, previous.id)
|
||||
: entries[index].sortOrder - 10;
|
||||
}
|
||||
|
||||
@@ -144,8 +144,15 @@ const circuitSectionLabels: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function getCircuitSectionLabel(
|
||||
section: { key: string; displayName: string }
|
||||
section: {
|
||||
key: string;
|
||||
displayName: string;
|
||||
category?: string;
|
||||
}
|
||||
): string {
|
||||
if (section.category) {
|
||||
return section.displayName;
|
||||
}
|
||||
return circuitSectionLabels[section.key] ?? section.displayName;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
import {
|
||||
getAdjacentInsertionSortOrder,
|
||||
getInsertionSortOrder,
|
||||
isGridInsertionRowType,
|
||||
resolveGridInsertionIntent,
|
||||
@@ -89,5 +90,26 @@ describe("circuit grid insertion", () => {
|
||||
assert.equal(getInsertionSortOrder(entries, "c"), 40);
|
||||
assert.equal(getInsertionSortOrder(entries), 40);
|
||||
assert.equal(getInsertionSortOrder([], "missing"), 10);
|
||||
assert.equal(
|
||||
getAdjacentInsertionSortOrder(entries, "a", "before"),
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
getAdjacentInsertionSortOrder(entries, "b", "before"),
|
||||
15
|
||||
);
|
||||
assert.equal(
|
||||
getAdjacentInsertionSortOrder(entries, "b", "after"),
|
||||
25
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
getAdjacentInsertionSortOrder(
|
||||
entries,
|
||||
"missing",
|
||||
"before"
|
||||
),
|
||||
/nicht gefunden/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -101,6 +101,14 @@ describe("circuit grid model", () => {
|
||||
});
|
||||
|
||||
it("shows stable circuit sections with German labels", () => {
|
||||
assert.equal(
|
||||
getCircuitSectionLabel({
|
||||
key: "lighting",
|
||||
displayName: "Lighting",
|
||||
category: "lighting",
|
||||
}),
|
||||
"Lighting"
|
||||
);
|
||||
assert.equal(
|
||||
getCircuitSectionLabel({
|
||||
key: "lighting",
|
||||
|
||||
Reference in New Issue
Block a user