Drag and drop working

This commit is contained in:
2026-05-04 23:27:13 +02:00
parent 897e506b74
commit 75435475fc
8 changed files with 448 additions and 16 deletions
+64
View File
@@ -217,6 +217,14 @@ body {
opacity: 0.55; opacity: 0.55;
} }
.tree-grid .circuit-drag-handle {
cursor: grab;
}
.tree-grid .circuit-dragging-block td {
opacity: 0.55;
}
.tree-grid .cell-selected { .tree-grid .cell-selected {
outline: 2px solid #4c7dd9; outline: 2px solid #4c7dd9;
outline-offset: -2px; outline-offset: -2px;
@@ -248,6 +256,62 @@ body {
background: #eef4ff !important; 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 { .drop-hint {
font-size: 0.75rem; font-size: 0.75rem;
color: #1f4ea3; color: #1f4ea3;
@@ -7,6 +7,7 @@ import type {
CreateCircuitDeviceRowInput, CreateCircuitDeviceRowInput,
CreateCircuitInput, CreateCircuitInput,
MoveCircuitDeviceRowInput, MoveCircuitDeviceRowInput,
ReorderSectionCircuitsInput,
UpdateCircuitDeviceRowInput, UpdateCircuitDeviceRowInput,
UpdateCircuitInput, UpdateCircuitInput,
} from "../../shared/validation/circuit.schemas.js"; } from "../../shared/validation/circuit.schemas.js";
@@ -408,4 +409,48 @@ export class CircuitWriteService {
return this.circuitRepository.listBySection(sectionId); 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);
}
} }
+271 -9
View File
@@ -10,6 +10,7 @@ import {
getNextCircuitIdentifier, getNextCircuitIdentifier,
listProjectDevices, listProjectDevices,
moveCircuitDeviceRowById, moveCircuitDeviceRowById,
reorderSectionCircuits,
renumberCircuitSection, renumberCircuitSection,
updateCircuitById, updateCircuitById,
updateCircuitDeviceRowById, updateCircuitDeviceRowById,
@@ -83,6 +84,11 @@ type DeviceRowMoveDropIntent =
| { kind: "move-to-circuit"; circuitId: string; sectionId: string } | { kind: "move-to-circuit"; circuitId: string; sectionId: string }
| { kind: "move-to-new-circuit"; 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 }> = [ const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [
{ key: "equipmentIdentifier", label: "Equipment identifier" }, { key: "equipmentIdentifier", label: "Equipment identifier" },
{ key: "displayName", label: "Display name" }, { 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 [dropIntent, setDropIntent] = useState<ProjectDeviceDropIntent | null>(null);
const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null); const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null);
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | 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 [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null); const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null); const containerRef = useRef<HTMLDivElement | null>(null);
@@ -947,6 +955,41 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
return null; 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) { async function handleDropWithIntent(event: DragEvent<HTMLElement>, intent: ProjectDeviceDropIntent) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); 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) { async function handleDeleteDevice(rowId: string) {
if (!confirm("Delete this device row?")) { if (!confirm("Delete this device row?")) {
return; return;
@@ -1174,7 +1250,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
setSelectedProjectDeviceId(device.id); setSelectedProjectDeviceId(device.id);
setDraggingProjectDeviceId(device.id); setDraggingProjectDeviceId(device.id);
setDraggingDeviceRowId(null); setDraggingDeviceRowId(null);
setDraggingCircuitId(null);
setDeviceMoveIntent(null); setDeviceMoveIntent(null);
setCircuitReorderIntent(null);
event.dataTransfer.effectAllowed = "copy"; event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.setData("application/x-project-device-id", device.id); 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" : "" dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? "drop-target-active" : ""
}`} }`}
onDragOver={(event) => { onDragOver={(event) => {
if (draggingCircuitId) {
event.preventDefault();
event.dataTransfer.dropEffect = "none";
setCircuitReorderIntent({
kind: "section-end",
sectionId: section.id,
valid: false,
});
return;
}
if (!draggingProjectDeviceId) { if (!draggingProjectDeviceId) {
return; return;
} }
@@ -1272,8 +1360,23 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
if (dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id) { if (dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id) {
setDropIntent(null); 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"> <td colSpan={columns.length + 1} className="section-drop-cell">
<div className="section-content"> <div className="section-content">
@@ -1311,12 +1414,81 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
} ${ } ${
row.rowType === "placeholder" && row.rowType === "placeholder" &&
((dropIntent?.kind === "new-circuit" && dropIntent.sectionId === row.sectionId) || ((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" ? "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)} onClick={() => setActiveSectionId(row.sectionId)}
onDragOver={(event) => { 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 (draggingProjectDeviceId) {
if (row.rowType === "placeholder") { if (row.rowType === "placeholder") {
event.preventDefault(); event.preventDefault();
@@ -1373,8 +1545,49 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
} }
return current; 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) => { 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 (draggingProjectDeviceId) {
if (row.rowType === "placeholder") { if (row.rowType === "placeholder") {
void handleDropWithIntent(event, { kind: "new-circuit", sectionId: row.sectionId }); 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") Boolean(row.device) && column.key === "displayName" && (row.rowType === "deviceRow" || row.rowType === "circuitCompact")
? "device-drag-handle" ? "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" : "" draggingDeviceRowId && row.device?.id === draggingDeviceRowId ? "device-dragging" : ""
}`} }`}
draggable={ draggable={
!isEditing && !isEditing &&
Boolean(row.device) && ((Boolean(row.device) &&
column.key === "displayName" && column.key === "displayName" &&
(row.rowType === "deviceRow" || row.rowType === "circuitCompact") (row.rowType === "deviceRow" || row.rowType === "circuitCompact")) ||
(Boolean(row.circuit) &&
column.key === "equipmentIdentifier" &&
(row.rowType === "circuitCompact" ||
row.rowType === "circuitSummary" ||
row.rowType === "reserveCircuit")))
} }
onDragStart={(event) => { onDragStart={(event) => {
if ( if (
!row.device || row.circuit &&
column.key !== "displayName" || column.key === "equipmentIdentifier" &&
(row.rowType !== "deviceRow" && row.rowType !== "circuitCompact") (row.rowType === "circuitCompact" ||
row.rowType === "circuitSummary" ||
row.rowType === "reserveCircuit")
) { ) {
return;
}
setDraggingProjectDeviceId(null); setDraggingProjectDeviceId(null);
setDropIntent(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;
}
if (
row.device &&
column.key === "displayName" &&
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")
) {
setDraggingProjectDeviceId(null);
setDropIntent(null);
setDraggingCircuitId(null);
setCircuitReorderIntent(null);
setDraggingDeviceRowId(row.device.id); setDraggingDeviceRowId(row.device.id);
setDeviceMoveIntent(null); setDeviceMoveIntent(null);
event.dataTransfer.effectAllowed = "move"; event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id); event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id);
}
}} }}
onDragEnd={() => { onDragEnd={() => {
setDraggingDeviceRowId(null); setDraggingDeviceRowId(null);
setDeviceMoveIntent(null); setDeviceMoveIntent(null);
setDraggingCircuitId(null);
setCircuitReorderIntent(null);
}} }}
onClick={() => { onClick={() => {
if (cell.editable) { 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 ? ( {deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id ? (
<span className="drop-hint">move device to this circuit</span> <span className="drop-hint">move device to this circuit</span>
) : null} ) : 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> </td>
</tr> </tr>
); );
+7
View File
@@ -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) { export function listFloors(projectId: string) {
return request<FloorDto[]>(`/api/projects/${projectId}/floors`); return request<FloorDto[]>(`/api/projects/${projectId}/floors`);
} }
@@ -1,5 +1,6 @@
import type { Request, Response } from "express"; import type { Request, Response } from "express";
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js"; import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
import { reorderSectionCircuitsSchema } from "../../shared/validation/circuit.schemas.js";
const circuitWriteService = new CircuitWriteService(); 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." });
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
import { Router } from "express"; 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(); export const circuitSectionRouter = Router();
circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection); circuitSectionRouter.post("/circuit-sections/:sectionId/renumber", renumberCircuitSection);
circuitSectionRouter.patch("/circuit-sections/:sectionId/circuits/reorder", reorderSectionCircuits);
+5
View File
@@ -62,8 +62,13 @@ export const moveCircuitDeviceRowSchema = z
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." } { 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 CreateCircuitInput = z.infer<typeof createCircuitSchema>;
export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>; export type UpdateCircuitInput = z.infer<typeof updateCircuitSchema>;
export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>; export type CreateCircuitDeviceRowInput = z.infer<typeof createCircuitDeviceRowSchema>;
export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>; export type UpdateCircuitDeviceRowInput = z.infer<typeof updateCircuitDeviceRowSchema>;
export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>; export type MoveCircuitDeviceRowInput = z.infer<typeof moveCircuitDeviceRowSchema>;
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
+30
View File
@@ -300,4 +300,34 @@ describe("circuit write service rules", () => {
assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8"); assert.equal(createdCircuitPayload?.equipmentIdentifier, "-2F8");
assert.equal(createdCircuitPayload?.isReserve, false); 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" },
]);
});
}); });