Drag and drop working
This commit is contained in:
@@ -217,6 +217,14 @@ body {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tree-grid .circuit-drag-handle {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.tree-grid .circuit-dragging-block td {
|
||||
opacity: 0.55;
|
||||
}
|
||||
|
||||
.tree-grid .cell-selected {
|
||||
outline: 2px solid #4c7dd9;
|
||||
outline-offset: -2px;
|
||||
@@ -248,6 +256,62 @@ body {
|
||||
background: #eef4ff !important;
|
||||
}
|
||||
|
||||
.tree-grid .drop-target-invalid {
|
||||
box-shadow: inset 0 0 0 2px #d97706;
|
||||
background: #fff7ed !important;
|
||||
}
|
||||
|
||||
.tree-grid tr.circuit-insert-before td,
|
||||
.tree-grid tr.circuit-insert-after td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree-grid tr.circuit-insert-before td::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
top: -2px;
|
||||
border-top: 4px solid #2563eb;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-grid tr.circuit-insert-before td:first-child::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
top: -7px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 7px solid transparent;
|
||||
border-bottom: 7px solid transparent;
|
||||
border-left: 10px solid #2563eb;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-grid tr.circuit-insert-after td::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
bottom: -2px;
|
||||
border-bottom: 4px solid #2563eb;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.tree-grid tr.circuit-insert-after td:first-child::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -7px;
|
||||
bottom: -7px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 7px solid transparent;
|
||||
border-bottom: 7px solid transparent;
|
||||
border-left: 10px solid #2563eb;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.drop-hint {
|
||||
font-size: 0.75rem;
|
||||
color: #1f4ea3;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CreateCircuitDeviceRowInput,
|
||||
CreateCircuitInput,
|
||||
MoveCircuitDeviceRowInput,
|
||||
ReorderSectionCircuitsInput,
|
||||
UpdateCircuitDeviceRowInput,
|
||||
UpdateCircuitInput,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
@@ -408,4 +409,48 @@ export class CircuitWriteService {
|
||||
|
||||
return this.circuitRepository.listBySection(sectionId);
|
||||
}
|
||||
|
||||
async reorderCircuitsInSection(sectionId: string, input: ReorderSectionCircuitsInput) {
|
||||
const section = await this.circuitSectionRepository.findById(sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid section id.");
|
||||
}
|
||||
const sectionCircuits = await this.circuitRepository.listBySection(sectionId);
|
||||
const sectionIds = new Set(sectionCircuits.map((circuit) => circuit.id));
|
||||
if (sectionCircuits.length !== input.orderedCircuitIds.length) {
|
||||
throw new Error("orderedCircuitIds must include all circuits of the section.");
|
||||
}
|
||||
for (const circuitId of input.orderedCircuitIds) {
|
||||
if (!sectionIds.has(circuitId)) {
|
||||
throw new Error("Circuit id does not belong to section.");
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < input.orderedCircuitIds.length; index += 1) {
|
||||
const circuitId = input.orderedCircuitIds[index];
|
||||
const circuit = sectionCircuits.find((entry) => entry.id === circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
await this.circuitRepository.update(circuit.id, {
|
||||
sectionId: circuit.sectionId,
|
||||
equipmentIdentifier: circuit.equipmentIdentifier,
|
||||
displayName: circuit.displayName ?? undefined,
|
||||
sortOrder: (index + 1) * 10,
|
||||
protectionType: circuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: circuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: circuit.protectionCharacteristic ?? undefined,
|
||||
cableType: circuit.cableType ?? undefined,
|
||||
cableCrossSection: circuit.cableCrossSection ?? undefined,
|
||||
cableLength: circuit.cableLength ?? undefined,
|
||||
rcdAssignment: circuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: circuit.terminalDesignation ?? undefined,
|
||||
voltage: circuit.voltage ?? undefined,
|
||||
status: circuit.status ?? undefined,
|
||||
isReserve: Boolean(circuit.isReserve),
|
||||
remark: circuit.remark ?? undefined,
|
||||
});
|
||||
}
|
||||
return this.circuitRepository.listBySection(sectionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -148,6 +148,13 @@ export function renumberCircuitSection(sectionId: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function reorderSectionCircuits(sectionId: string, orderedCircuitIds: string[]) {
|
||||
return request(`/api/circuit-sections/${sectionId}/circuits/reorder`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ orderedCircuitIds }),
|
||||
});
|
||||
}
|
||||
|
||||
export function listFloors(projectId: string) {
|
||||
return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Request, Response } from "express";
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
import { reorderSectionCircuitsSchema } from "../../shared/validation/circuit.schemas.js";
|
||||
|
||||
const circuitWriteService = new CircuitWriteService();
|
||||
|
||||
@@ -17,3 +18,21 @@ export async function renumberCircuitSection(req: Request, res: Response) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function reorderSectionCircuits(req: Request, res: Response) {
|
||||
const { sectionId } = req.params;
|
||||
if (typeof sectionId !== "string") {
|
||||
return res.status(400).json({ error: "Invalid sectionId" });
|
||||
}
|
||||
|
||||
const parsed = reorderSectionCircuitsSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const updatedCircuits = await circuitWriteService.reorderCircuitsInSection(sectionId, parsed.data);
|
||||
return res.json({ sectionId, circuits: updatedCircuits });
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to reorder circuits." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from "express";
|
||||
import { renumberCircuitSection } from "../controllers/circuit-section.controller.js";
|
||||
import { renumberCircuitSection, reorderSectionCircuits } from "../controllers/circuit-section.controller.js";
|
||||
|
||||
export const circuitSectionRouter = Router();
|
||||
|
||||
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
|
||||
|
||||
circuitSectionRouter.patch("/circuit-sections/:sectionId/circuits/reorder", reorderSectionCircuits);
|
||||
|
||||
@@ -62,8 +62,13 @@ export const moveCircuitDeviceRowSchema = z
|
||||
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." }
|
||||
);
|
||||
|
||||
export const reorderSectionCircuitsSchema = z.object({
|
||||
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
||||
});
|
||||
|
||||
export type CreateCircuitInput = z.infer<typeof createCircuitSchema>;
|
||||
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
|
||||
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
|
||||
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
|
||||
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
|
||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||
|
||||
@@ -300,4 +300,34 @@ describe("circuit write service rules", () => {
|
||||
assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8");
|
||||
assert.equal(createdCircuitPayload?.isReserve, false);
|
||||
});
|
||||
|
||||
it("reorders circuits inside one section without renumbering identifiers", async () => {
|
||||
const updates: Array<{ id: string; sortOrder: number; equipmentIdentifier: string }> = [];
|
||||
const service = new CircuitWriteService({
|
||||
circuitSectionRepository: {
|
||||
async findById() {
|
||||
return { id: "s1", circuitListId: "l1" } as never;
|
||||
},
|
||||
} as never,
|
||||
circuitRepository: {
|
||||
async listBySection() {
|
||||
return [
|
||||
{ id: "c1", sectionId: "s1", equipmentIdentifier: "-2F7", sortOrder: 10, isReserve: 0 },
|
||||
{ id: "c2", sectionId: "s1", equipmentIdentifier: "-2F9", sortOrder: 20, isReserve: 0 },
|
||||
{ id: "c3", sectionId: "s1", equipmentIdentifier: "-2F5", sortOrder: 30, isReserve: 1 },
|
||||
] as never[];
|
||||
},
|
||||
async update(id: string, payload: { sortOrder: number; equipmentIdentifier: string }) {
|
||||
updates.push({ id, sortOrder: payload.sortOrder, equipmentIdentifier: payload.equipmentIdentifier });
|
||||
},
|
||||
} as never,
|
||||
});
|
||||
|
||||
await service.reorderCircuitsInSection("s1", { orderedCircuitIds: ["c3", "c1", "c2"] });
|
||||
assert.deepEqual(updates, [
|
||||
{ id: "c3", sortOrder: 10, equipmentIdentifier: "-2F5" },
|
||||
{ id: "c1", sortOrder: 20, equipmentIdentifier: "-2F7" },
|
||||
{ id: "c2", sortOrder: 30, equipmentIdentifier: "-2F9" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user