Drag and drop working
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
getNextCircuitIdentifier,
|
||||
listProjectDevices,
|
||||
moveCircuitDeviceRowById,
|
||||
reorderSectionCircuits,
|
||||
renumberCircuitSection,
|
||||
updateCircuitById,
|
||||
updateCircuitDeviceRowById,
|
||||
@@ -83,6 +84,11 @@ type DeviceRowMoveDropIntent =
|
||||
| { kind: "move-to-circuit"; circuitId: string; sectionId: string }
|
||||
| { kind: "move-to-new-circuit"; sectionId: string };
|
||||
|
||||
type CircuitReorderDropIntent =
|
||||
| { kind: "before-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
| { kind: "after-circuit"; sectionId: string; targetCircuitId: string; valid: boolean }
|
||||
| { kind: "section-end"; sectionId: string; valid: boolean };
|
||||
|
||||
const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier" },
|
||||
{ key: "displayName", label: "Display name" },
|
||||
@@ -274,6 +280,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [dropIntent, setDropIntent] = useState<ProjectDeviceDropIntent | null>(null);
|
||||
const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null);
|
||||
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | null>(null);
|
||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -947,6 +955,41 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
function findCircuitSectionId(circuitId: string) {
|
||||
for (const section of data?.sections ?? []) {
|
||||
if (section.circuits.some((circuit) => circuit.id === circuitId)) {
|
||||
return section.id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitId: string) {
|
||||
const section = data?.sections.find((entry) => entry.id === intent.sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
}
|
||||
const ids = section.circuits.map((circuit) => circuit.id);
|
||||
const fromIndex = ids.indexOf(sourceCircuitId);
|
||||
if (fromIndex < 0) {
|
||||
throw new Error("Invalid source circuit.");
|
||||
}
|
||||
const nextIds = [...ids];
|
||||
nextIds.splice(fromIndex, 1);
|
||||
|
||||
if (intent.kind === "section-end") {
|
||||
nextIds.push(sourceCircuitId);
|
||||
} else {
|
||||
const targetIndex = nextIds.indexOf(intent.targetCircuitId);
|
||||
if (targetIndex < 0) {
|
||||
throw new Error("Invalid target circuit.");
|
||||
}
|
||||
const insertIndex = intent.kind === "after-circuit" ? targetIndex + 1 : targetIndex;
|
||||
nextIds.splice(insertIndex, 0, sourceCircuitId);
|
||||
}
|
||||
await reorderSectionCircuits(section.id, nextIds);
|
||||
}
|
||||
|
||||
async function handleDropWithIntent(event: DragEvent<HTMLElement>, intent: ProjectDeviceDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -1017,6 +1060,39 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCircuitReorderDrop(event: DragEvent<HTMLElement>, intent: CircuitReorderDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const sourceCircuitId = event.dataTransfer.getData("application/x-circuit-id") || draggingCircuitId;
|
||||
setCircuitReorderIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
if (!sourceCircuitId) {
|
||||
setError("Missing dragged circuit.");
|
||||
return;
|
||||
}
|
||||
if (!intent.valid) {
|
||||
setError("Cross-section circuit move is not allowed in this phase.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
await applyCircuitReorder(intent, sourceCircuitId);
|
||||
pendingSelectionAfterReload.current = {
|
||||
rowKey: `circuitSummary:${sourceCircuitId}`,
|
||||
cellKey: "equipmentIdentifier",
|
||||
rowType: "circuitSummary",
|
||||
sectionId: intent.sectionId,
|
||||
circuitId: sourceCircuitId,
|
||||
};
|
||||
await loadTree({ showLoading: false });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteDevice(rowId: string) {
|
||||
if (!confirm("Delete this device row?")) {
|
||||
return;
|
||||
@@ -1174,7 +1250,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setSelectedProjectDeviceId(device.id);
|
||||
setDraggingProjectDeviceId(device.id);
|
||||
setDraggingDeviceRowId(null);
|
||||
setDraggingCircuitId(null);
|
||||
setDeviceMoveIntent(null);
|
||||
setCircuitReorderIntent(null);
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("application/x-project-device-id", device.id);
|
||||
}}
|
||||
@@ -1261,6 +1339,16 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? "drop-target-active" : ""
|
||||
}`}
|
||||
onDragOver={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
setCircuitReorderIntent({
|
||||
kind: "section-end",
|
||||
sectionId: section.id,
|
||||
valid: false,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!draggingProjectDeviceId) {
|
||||
return;
|
||||
}
|
||||
@@ -1272,8 +1360,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id) {
|
||||
setDropIntent(null);
|
||||
}
|
||||
if (circuitReorderIntent?.kind === "section-end" && circuitReorderIntent.sectionId === section.id) {
|
||||
setCircuitReorderIntent(null);
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (draggingProjectDeviceId) {
|
||||
void handleDropWithIntent(event, { kind: "new-circuit", sectionId: section.id });
|
||||
return;
|
||||
}
|
||||
if (draggingCircuitId) {
|
||||
void handleCircuitReorderDrop(event, {
|
||||
kind: "section-end",
|
||||
sectionId: section.id,
|
||||
valid: false,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => void handleDropWithIntent(event, { kind: "new-circuit", sectionId: section.id })}
|
||||
>
|
||||
<td colSpan={columns.length + 1} className="section-drop-cell">
|
||||
<div className="section-content">
|
||||
@@ -1311,12 +1414,81 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
((dropIntent?.kind === "new-circuit" && dropIntent.sectionId === row.sectionId) ||
|
||||
(deviceMoveIntent?.kind === "move-to-new-circuit" && deviceMoveIntent.sectionId === row.sectionId))
|
||||
(deviceMoveIntent?.kind === "move-to-new-circuit" && deviceMoveIntent.sectionId === row.sectionId) ||
|
||||
(circuitReorderIntent?.kind === "section-end" && circuitReorderIntent.sectionId === row.sectionId))
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
circuitReorderIntent?.kind === "section-end" &&
|
||||
circuitReorderIntent.sectionId === row.sectionId &&
|
||||
!circuitReorderIntent.valid
|
||||
? "drop-target-invalid"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
circuitReorderIntent &&
|
||||
(circuitReorderIntent.kind === "before-circuit" || circuitReorderIntent.kind === "after-circuit") &&
|
||||
circuitReorderIntent.targetCircuitId === row.circuit.id &&
|
||||
circuitReorderIntent.valid
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
circuitReorderIntent?.kind === "before-circuit" &&
|
||||
circuitReorderIntent.targetCircuitId === row.circuit.id &&
|
||||
circuitReorderIntent.valid
|
||||
? "circuit-insert-before"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
circuitReorderIntent?.kind === "after-circuit" &&
|
||||
circuitReorderIntent.targetCircuitId === row.circuit.id &&
|
||||
circuitReorderIntent.valid
|
||||
? "circuit-insert-after"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit &&
|
||||
circuitReorderIntent &&
|
||||
(circuitReorderIntent.kind === "before-circuit" || circuitReorderIntent.kind === "after-circuit") &&
|
||||
circuitReorderIntent.targetCircuitId === row.circuit.id &&
|
||||
!circuitReorderIntent.valid
|
||||
? "drop-target-invalid"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit?.id && draggingCircuitId && row.circuit.id === draggingCircuitId ? "circuit-dragging-block" : ""
|
||||
}`}
|
||||
onClick={() => setActiveSectionId(row.sectionId)}
|
||||
onDragOver={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
const valid = sourceSectionId === row.sectionId;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = valid ? "move" : "none";
|
||||
setCircuitReorderIntent({ kind: "section-end", sectionId: row.sectionId, valid });
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
if (row.circuit.id === draggingCircuitId) {
|
||||
setCircuitReorderIntent(null);
|
||||
return;
|
||||
}
|
||||
const valid = sourceSectionId === row.sectionId;
|
||||
const rect = (event.currentTarget as HTMLTableRowElement).getBoundingClientRect();
|
||||
const isAfter = event.clientY > rect.top + rect.height / 2;
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = valid ? "move" : "none";
|
||||
setCircuitReorderIntent({
|
||||
kind: isAfter ? "after-circuit" : "before-circuit",
|
||||
sectionId: row.sectionId,
|
||||
targetCircuitId: row.circuit.id,
|
||||
valid,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingProjectDeviceId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
@@ -1373,8 +1545,49 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
return current;
|
||||
});
|
||||
setCircuitReorderIntent((current) => {
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "section-end" && row.rowType === "placeholder" && current.sectionId === row.sectionId) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
(current.kind === "before-circuit" || current.kind === "after-circuit") &&
|
||||
current.targetCircuitId === row.circuit?.id
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
void handleCircuitReorderDrop(event, {
|
||||
kind: "section-end",
|
||||
sectionId: row.sectionId,
|
||||
valid: sourceSectionId === row.sectionId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
if (row.circuit.id === draggingCircuitId) {
|
||||
return;
|
||||
}
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
const rect = (event.currentTarget as HTMLTableRowElement).getBoundingClientRect();
|
||||
const isAfter = event.clientY > rect.top + rect.height / 2;
|
||||
void handleCircuitReorderDrop(event, {
|
||||
kind: isAfter ? "after-circuit" : "before-circuit",
|
||||
sectionId: row.sectionId,
|
||||
targetCircuitId: row.circuit.id,
|
||||
valid: sourceSectionId === row.sectionId,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingProjectDeviceId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleDropWithIntent(event, { kind: "new-circuit", sectionId: row.sectionId });
|
||||
@@ -1417,33 +1630,64 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
Boolean(row.device) && column.key === "displayName" && (row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
? "device-drag-handle"
|
||||
: ""
|
||||
} ${
|
||||
column.key === "equipmentIdentifier" &&
|
||||
Boolean(row.circuit) &&
|
||||
(row.rowType === "circuitCompact" || row.rowType === "circuitSummary" || row.rowType === "reserveCircuit")
|
||||
? "circuit-drag-handle"
|
||||
: ""
|
||||
} ${
|
||||
draggingDeviceRowId && row.device?.id === draggingDeviceRowId ? "device-dragging" : ""
|
||||
}`}
|
||||
draggable={
|
||||
!isEditing &&
|
||||
Boolean(row.device) &&
|
||||
column.key === "displayName" &&
|
||||
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
((Boolean(row.device) &&
|
||||
column.key === "displayName" &&
|
||||
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")) ||
|
||||
(Boolean(row.circuit) &&
|
||||
column.key === "equipmentIdentifier" &&
|
||||
(row.rowType === "circuitCompact" ||
|
||||
row.rowType === "circuitSummary" ||
|
||||
row.rowType === "reserveCircuit")))
|
||||
}
|
||||
onDragStart={(event) => {
|
||||
if (
|
||||
!row.device ||
|
||||
column.key !== "displayName" ||
|
||||
(row.rowType !== "deviceRow" && row.rowType !== "circuitCompact")
|
||||
row.circuit &&
|
||||
column.key === "equipmentIdentifier" &&
|
||||
(row.rowType === "circuitCompact" ||
|
||||
row.rowType === "circuitSummary" ||
|
||||
row.rowType === "reserveCircuit")
|
||||
) {
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingDeviceRowId(null);
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingCircuitId(row.circuit.id);
|
||||
setCircuitReorderIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-id", row.circuit.id);
|
||||
return;
|
||||
}
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingDeviceRowId(row.device.id);
|
||||
setDeviceMoveIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id);
|
||||
if (
|
||||
row.device &&
|
||||
column.key === "displayName" &&
|
||||
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
) {
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
setCircuitReorderIntent(null);
|
||||
setDraggingDeviceRowId(row.device.id);
|
||||
setDeviceMoveIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id);
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDraggingDeviceRowId(null);
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
setCircuitReorderIntent(null);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (cell.editable) {
|
||||
@@ -1539,6 +1783,24 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
{deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id ? (
|
||||
<span className="drop-hint">move device to this circuit</span>
|
||||
) : null}
|
||||
{circuitReorderIntent?.kind === "section-end" &&
|
||||
row.rowType === "placeholder" &&
|
||||
circuitReorderIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">
|
||||
{circuitReorderIntent.valid ? "move circuit to section end" : "cross-section move not allowed"}
|
||||
</span>
|
||||
) : null}
|
||||
{circuitReorderIntent &&
|
||||
(circuitReorderIntent.kind === "before-circuit" || circuitReorderIntent.kind === "after-circuit") &&
|
||||
circuitReorderIntent.targetCircuitId === row.circuit?.id ? (
|
||||
<span className="drop-hint">
|
||||
{circuitReorderIntent.valid
|
||||
? circuitReorderIntent.kind === "before-circuit"
|
||||
? "move circuit before this circuit"
|
||||
: "move circuit after this circuit"
|
||||
: "cross-section move not allowed"}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user