Confirm cross-section device moves
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
import {
|
||||
findDuplicateEquipmentIdentifierCircuitIds,
|
||||
hasEquipmentIdentifierConflict,
|
||||
requiresCrossSectionMoveConfirmation,
|
||||
resolveGridDeleteIntent,
|
||||
} from "../utils/circuit-grid-safety";
|
||||
import {
|
||||
@@ -143,8 +144,8 @@ type ProjectDeviceDropIntent =
|
||||
| { kind: "add-to-circuit"; circuitId: string; sectionId: string; valid: boolean };
|
||||
|
||||
type DeviceRowMoveDropIntent =
|
||||
| { kind: "move-to-circuit"; circuitId: string; sectionId: string }
|
||||
| { kind: "move-to-new-circuit"; sectionId: string };
|
||||
| { kind: "move-to-circuit"; circuitId: string; sectionId: string; requiresConfirmation: boolean }
|
||||
| { kind: "move-to-new-circuit"; sectionId: string; requiresConfirmation: boolean };
|
||||
|
||||
type CircuitReorderDropIntent =
|
||||
| { kind: "before-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
@@ -2140,6 +2141,36 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDeviceRowSourceSectionIds(rowIds: string[]) {
|
||||
const sectionIds = new Set<string>();
|
||||
for (const rowId of rowIds) {
|
||||
const circuitId = findDeviceRowCircuitId(rowId);
|
||||
const sectionId = circuitId ? findCircuitSectionId(circuitId) : null;
|
||||
if (sectionId) {
|
||||
sectionIds.add(sectionId);
|
||||
}
|
||||
}
|
||||
return [...sectionIds];
|
||||
}
|
||||
|
||||
function confirmCrossSectionDeviceMove(rowIds: string[], targetSectionId: string) {
|
||||
const sourceSectionIds = getDeviceRowSourceSectionIds(rowIds);
|
||||
if (!requiresCrossSectionMoveConfirmation(sourceSectionIds, targetSectionId)) {
|
||||
return true;
|
||||
}
|
||||
const sourceNames = sourceSectionIds
|
||||
.map((sectionId) => data?.sections.find((section) => section.id === sectionId)?.displayName ?? sectionId)
|
||||
.join(", ");
|
||||
const targetName =
|
||||
data?.sections.find((section) => section.id === targetSectionId)?.displayName ?? targetSectionId;
|
||||
return confirm(
|
||||
`Move ${rowIds.length} device row(s) from ${sourceNames} to ${targetName}?\n\n` +
|
||||
"This crosses a section boundary. Phase type, category, project-device links and local values remain unchanged. " +
|
||||
"The target section's technical and numbering classification may therefore need manual review.\n\n" +
|
||||
"Existing circuit identifiers will not be renumbered."
|
||||
);
|
||||
}
|
||||
|
||||
// Applies same-section circuit reorder as explicit id ordering.
|
||||
// No implicit renumbering is performed here.
|
||||
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitIds: string[]) {
|
||||
@@ -2274,12 +2305,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.kind === "move-to-circuit" && rowIds.length === 1 && intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
if (!confirmCrossSectionDeviceMove(rowIds, intent.sectionId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Device-row drag supports bulk selection, but target intent is always explicit:
|
||||
// move into existing circuit or create new circuit in a target section.
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (rowIds.length === 1 && intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
const groupedBySource = new Map<string, string[]>();
|
||||
for (const rowId of rowIds) {
|
||||
const source = sourceByRowId.get(rowId)!;
|
||||
@@ -2748,7 +2783,13 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
// Renumbering is intentionally blocked while sort/filter overlays are active so users
|
||||
// cannot renumber against a transformed view that has not been persisted yet.
|
||||
const hasActiveSortOrFilter = isSortedView || hasActiveFilters;
|
||||
const draggingDeviceCount = draggingDeviceRowIds.length > 0 ? draggingDeviceRowIds.length : draggingDeviceRowId ? 1 : 0;
|
||||
const activeDraggedDeviceRowIds =
|
||||
draggingDeviceRowIds.length > 0
|
||||
? draggingDeviceRowIds
|
||||
: draggingDeviceRowId
|
||||
? [draggingDeviceRowId]
|
||||
: [];
|
||||
const draggingDeviceCount = activeDraggedDeviceRowIds.length;
|
||||
const activeDraggedCircuitIds =
|
||||
draggingCircuitIds.length > 0 ? draggingCircuitIds : draggingCircuitId ? [draggingCircuitId] : [];
|
||||
const draggingCircuitCount = activeDraggedCircuitIds.length;
|
||||
@@ -3167,10 +3208,26 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
((dropIntent?.kind === "new-circuit" && dropIntent.sectionId === row.sectionId && dropIntent.valid) ||
|
||||
(deviceMoveIntent?.kind === "move-to-new-circuit" && deviceMoveIntent.sectionId === row.sectionId) ||
|
||||
(deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.sectionId === row.sectionId &&
|
||||
!deviceMoveIntent.requiresConfirmation) ||
|
||||
(circuitReorderIntent?.kind === "section-end" && circuitReorderIntent.sectionId === row.sectionId))
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
deviceMoveIntent.sectionId === row.sectionId &&
|
||||
deviceMoveIntent.requiresConfirmation
|
||||
? "drop-target-confirm"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
deviceMoveIntent?.kind === "move-to-circuit" &&
|
||||
deviceMoveIntent.circuitId === row.circuit.id &&
|
||||
deviceMoveIntent.requiresConfirmation
|
||||
? "drop-target-confirm"
|
||||
: ""
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
dropIntent?.kind === "new-circuit" &&
|
||||
@@ -3291,20 +3348,33 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceCount > 0) {
|
||||
const requiresConfirmation = requiresCrossSectionMoveConfirmation(
|
||||
getDeviceRowSourceSectionIds(activeDraggedDeviceRowIds),
|
||||
row.sectionId
|
||||
);
|
||||
if (row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDeviceMoveIntent({ kind: "move-to-new-circuit", sectionId: row.sectionId });
|
||||
setDeviceMoveIntent({
|
||||
kind: "move-to-new-circuit",
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
const sourceCircuitIds = (draggingDeviceRowIds.length > 0 ? draggingDeviceRowIds : draggingDeviceRowId ? [draggingDeviceRowId] : [])
|
||||
const sourceCircuitIds = activeDraggedDeviceRowIds
|
||||
.map((id) => findDeviceRowCircuitId(id))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
if (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 });
|
||||
setDeviceMoveIntent({
|
||||
kind: "move-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3400,8 +3470,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceCount > 0) {
|
||||
const requiresConfirmation = requiresCrossSectionMoveConfirmation(
|
||||
getDeviceRowSourceSectionIds(activeDraggedDeviceRowIds),
|
||||
row.sectionId
|
||||
);
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleDeviceRowDropWithIntent(event, { kind: "move-to-new-circuit", sectionId: row.sectionId });
|
||||
void handleDeviceRowDropWithIntent(event, {
|
||||
kind: "move-to-new-circuit",
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
@@ -3409,6 +3487,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
kind: "move-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
requiresConfirmation,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3570,7 +3649,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
(dropIntent?.kind === "add-to-circuit" &&
|
||||
dropIntent.circuitId === row.circuit?.id &&
|
||||
dropIntent.valid) ||
|
||||
(deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id)
|
||||
(deviceMoveIntent?.kind === "move-to-circuit" &&
|
||||
deviceMoveIntent.circuitId === row.circuit?.id &&
|
||||
!deviceMoveIntent.requiresConfirmation)
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
} ${
|
||||
@@ -3579,6 +3660,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
!dropIntent.valid
|
||||
? "drop-target-invalid"
|
||||
: ""
|
||||
} ${
|
||||
deviceMoveIntent?.kind === "move-to-circuit" &&
|
||||
deviceMoveIntent.circuitId === row.circuit?.id &&
|
||||
deviceMoveIntent.requiresConfirmation
|
||||
? "drop-target-confirm"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
||||
@@ -3617,10 +3704,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
{deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
row.rowType === "placeholder" &&
|
||||
deviceMoveIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">{`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to new circuit`}</span>
|
||||
<span className="drop-hint">
|
||||
{`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to new circuit${deviceMoveIntent.requiresConfirmation ? " (confirmation required)" : ""}`}
|
||||
</span>
|
||||
) : null}
|
||||
{deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id ? (
|
||||
<span className="drop-hint">{`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to this circuit`}</span>
|
||||
<span className="drop-hint">
|
||||
{`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to this circuit${deviceMoveIntent.requiresConfirmation ? " (confirmation required)" : ""}`}
|
||||
</span>
|
||||
) : null}
|
||||
{circuitReorderIntent?.kind === "section-end" &&
|
||||
row.rowType === "placeholder" &&
|
||||
|
||||
@@ -72,3 +72,10 @@ export function findDuplicateEquipmentIdentifierCircuitIds(
|
||||
}
|
||||
return [...idsByIdentifier.values()].filter((ids) => ids.length > 1).flat();
|
||||
}
|
||||
|
||||
export function requiresCrossSectionMoveConfirmation(
|
||||
sourceSectionIds: string[],
|
||||
targetSectionId: string
|
||||
): boolean {
|
||||
return sourceSectionIds.some((sectionId) => sectionId !== targetSectionId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user