Started multiline manipulations
This commit is contained in:
@@ -347,6 +347,10 @@ body {
|
||||
background: #f8fbff;
|
||||
}
|
||||
|
||||
.tree-grid tr.row-selected td {
|
||||
background: #eaf1ff;
|
||||
}
|
||||
|
||||
.tree-grid .placeholder-row td {
|
||||
background: #f7f7f7;
|
||||
color: #6b7280;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
CreateCircuitDeviceRowInput,
|
||||
CreateCircuitInput,
|
||||
MoveCircuitDeviceRowInput,
|
||||
MoveCircuitDeviceRowsBulkInput,
|
||||
ReorderSectionCircuitsInput,
|
||||
UpdateSectionEquipmentIdentifiersInput,
|
||||
UpdateCircuitDeviceRowInput,
|
||||
@@ -365,6 +366,128 @@ export class CircuitWriteService {
|
||||
return this.deviceRowRepository.findById(rowId);
|
||||
}
|
||||
|
||||
async moveDeviceRowsBulk(input: MoveCircuitDeviceRowsBulkInput) {
|
||||
const uniqueRowIds = [...new Set(input.rowIds)];
|
||||
if (uniqueRowIds.length === 0) {
|
||||
throw new Error("No device rows provided.");
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
for (const rowId of uniqueRowIds) {
|
||||
const row = await this.deviceRowRepository.findById(rowId);
|
||||
if (!row) {
|
||||
throw new Error("Invalid device row id.");
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
const sourceCircuits = new Map<string, Awaited<ReturnType<CircuitRepository["findById"]>>>();
|
||||
for (const row of rows) {
|
||||
if (!sourceCircuits.has(row.circuitId)) {
|
||||
const circuit = await this.circuitRepository.findById(row.circuitId);
|
||||
if (!circuit) {
|
||||
throw new Error("Invalid circuit id.");
|
||||
}
|
||||
sourceCircuits.set(row.circuitId, circuit);
|
||||
}
|
||||
}
|
||||
|
||||
const referenceSourceCircuit = sourceCircuits.get(rows[0].circuitId)!;
|
||||
let targetCircuit = input.targetCircuitId
|
||||
? await this.circuitRepository.findById(input.targetCircuitId)
|
||||
: null;
|
||||
if (input.targetCircuitId && !targetCircuit) {
|
||||
throw new Error("Invalid target circuit id.");
|
||||
}
|
||||
|
||||
if (!targetCircuit) {
|
||||
if (!input.targetSectionId || !input.createNewCircuit) {
|
||||
throw new Error("Invalid move target.");
|
||||
}
|
||||
const section = await this.assertSectionInList(input.targetSectionId, referenceSourceCircuit.circuitListId);
|
||||
const nextIdentifier = await this.numberingService.getNextIdentifier(section.id);
|
||||
const sectionCircuits = await this.circuitRepository.listBySection(section.id);
|
||||
const nextSortOrder =
|
||||
sectionCircuits.length > 0 ? Math.max(...sectionCircuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||
const createdId = await this.circuitRepository.create({
|
||||
circuitListId: referenceSourceCircuit.circuitListId,
|
||||
sectionId: section.id,
|
||||
equipmentIdentifier: nextIdentifier,
|
||||
displayName: "New circuit",
|
||||
sortOrder: nextSortOrder,
|
||||
isReserve: false,
|
||||
});
|
||||
targetCircuit = await this.circuitRepository.findById(createdId);
|
||||
if (!targetCircuit) {
|
||||
throw new Error("Failed to create target circuit.");
|
||||
}
|
||||
}
|
||||
|
||||
for (const sourceCircuit of sourceCircuits.values()) {
|
||||
if (sourceCircuit.circuitListId !== targetCircuit.circuitListId) {
|
||||
throw new Error("All moved rows must belong to same circuit list as target.");
|
||||
}
|
||||
}
|
||||
|
||||
const targetCount = await this.deviceRowRepository.countByCircuit(targetCircuit.id);
|
||||
let offset = 1;
|
||||
for (const row of rows) {
|
||||
await this.deviceRowRepository.moveToCircuit(row.id, targetCircuit.id, (targetCount + offset) * 10);
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
for (const sourceCircuit of sourceCircuits.values()) {
|
||||
const remaining = await this.deviceRowRepository.countByCircuit(sourceCircuit.id);
|
||||
if (remaining === 0) {
|
||||
await this.circuitRepository.update(sourceCircuit.id, {
|
||||
sectionId: sourceCircuit.sectionId,
|
||||
equipmentIdentifier: sourceCircuit.equipmentIdentifier,
|
||||
displayName: sourceCircuit.displayName ?? undefined,
|
||||
sortOrder: sourceCircuit.sortOrder,
|
||||
protectionType: sourceCircuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: sourceCircuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: sourceCircuit.protectionCharacteristic ?? undefined,
|
||||
cableType: sourceCircuit.cableType ?? undefined,
|
||||
cableCrossSection: sourceCircuit.cableCrossSection ?? undefined,
|
||||
cableLength: sourceCircuit.cableLength ?? undefined,
|
||||
rcdAssignment: sourceCircuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: sourceCircuit.terminalDesignation ?? undefined,
|
||||
voltage: sourceCircuit.voltage ?? undefined,
|
||||
status: sourceCircuit.status ?? undefined,
|
||||
isReserve: true,
|
||||
remark: sourceCircuit.remark ?? undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Boolean(targetCircuit.isReserve)) {
|
||||
await this.circuitRepository.update(targetCircuit.id, {
|
||||
sectionId: targetCircuit.sectionId,
|
||||
equipmentIdentifier: targetCircuit.equipmentIdentifier,
|
||||
displayName: targetCircuit.displayName ?? undefined,
|
||||
sortOrder: targetCircuit.sortOrder,
|
||||
protectionType: targetCircuit.protectionType ?? undefined,
|
||||
protectionRatedCurrent: targetCircuit.protectionRatedCurrent ?? undefined,
|
||||
protectionCharacteristic: targetCircuit.protectionCharacteristic ?? undefined,
|
||||
cableType: targetCircuit.cableType ?? undefined,
|
||||
cableCrossSection: targetCircuit.cableCrossSection ?? undefined,
|
||||
cableLength: targetCircuit.cableLength ?? undefined,
|
||||
rcdAssignment: targetCircuit.rcdAssignment ?? undefined,
|
||||
terminalDesignation: targetCircuit.terminalDesignation ?? undefined,
|
||||
voltage: targetCircuit.voltage ?? undefined,
|
||||
status: targetCircuit.status ?? undefined,
|
||||
isReserve: false,
|
||||
remark: targetCircuit.remark ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
movedRowIds: rows.map((row) => row.id),
|
||||
targetCircuitId: targetCircuit.id,
|
||||
createdCircuitId: input.targetCircuitId ? undefined : targetCircuit.id,
|
||||
};
|
||||
}
|
||||
|
||||
async getNextIdentifier(sectionId: string) {
|
||||
return this.numberingService.getNextIdentifier(sectionId);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getCircuitTree,
|
||||
getNextCircuitIdentifier,
|
||||
listProjectDevices,
|
||||
moveCircuitDeviceRowsBulk,
|
||||
moveCircuitDeviceRowById,
|
||||
reorderSectionCircuits,
|
||||
renumberCircuitSection,
|
||||
@@ -467,6 +468,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedCell, setSelectedCell] = useState<SelectedCell | null>(null);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||||
const [anchorRowKey, setAnchorRowKey] = useState<string | null>(null);
|
||||
const [editingCell, setEditingCell] = useState<EditingCell | null>(null);
|
||||
const [activeSectionId, setActiveSectionId] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -478,8 +481,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [draggingProjectDeviceId, setDraggingProjectDeviceId] = useState<string | null>(null);
|
||||
const [dropIntent, setDropIntent] = useState<ProjectDeviceDropIntent | null>(null);
|
||||
const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null);
|
||||
const [draggingDeviceRowIds, setDraggingDeviceRowIds] = useState<string[]>([]);
|
||||
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | null>(null);
|
||||
const [draggingCircuitId, setDraggingCircuitId] = useState<string | null>(null);
|
||||
const [draggingCircuitIds, setDraggingCircuitIds] = useState<string[]>([]);
|
||||
const [circuitReorderIntent, setCircuitReorderIntent] = useState<CircuitReorderDropIntent | null>(null);
|
||||
const [undoStack, setUndoStack] = useState<HistoryCommand[]>([]);
|
||||
const [redoStack, setRedoStack] = useState<HistoryCommand[]>([]);
|
||||
@@ -494,6 +499,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [columnDropTargetKey, setColumnDropTargetKey] = useState<CellKey | null>(null);
|
||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||
const pendingSelectedDeviceRowIdsAfterReload = useRef<string[] | null>(null);
|
||||
const pendingSelectedCircuitIdsAfterReload = useRef<string[] | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const focusTokenRef = useRef(1);
|
||||
@@ -803,6 +810,19 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
() => visibleRows.filter((row) => rowCellMap.has(row.rowKey)).map((row) => row.rowKey),
|
||||
[visibleRows, rowCellMap]
|
||||
);
|
||||
const selectableRowOrder = useMemo(
|
||||
() =>
|
||||
visibleRows
|
||||
.filter(
|
||||
(row) =>
|
||||
row.rowType === "circuitCompact" ||
|
||||
row.rowType === "circuitSummary" ||
|
||||
row.rowType === "deviceRow" ||
|
||||
row.rowType === "reserveCircuit"
|
||||
)
|
||||
.map((row) => row.rowKey),
|
||||
[visibleRows]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedCell && editableCells.length > 0) {
|
||||
@@ -819,6 +839,64 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
}, [editableCells, selectedCell]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRowKeys((current) => current.filter((rowKey) => selectableRowOrder.includes(rowKey)));
|
||||
setAnchorRowKey((current) => (current && selectableRowOrder.includes(current) ? current : null));
|
||||
}, [selectableRowOrder]);
|
||||
|
||||
function isSelectableRowType(rowType: RowType) {
|
||||
return (
|
||||
rowType === "circuitCompact" ||
|
||||
rowType === "circuitSummary" ||
|
||||
rowType === "deviceRow" ||
|
||||
rowType === "reserveCircuit"
|
||||
);
|
||||
}
|
||||
|
||||
function selectRowRange(targetRowKey: string) {
|
||||
const anchor = anchorRowKey ?? selectedRowKeys[selectedRowKeys.length - 1] ?? targetRowKey;
|
||||
const start = selectableRowOrder.indexOf(anchor);
|
||||
const end = selectableRowOrder.indexOf(targetRowKey);
|
||||
if (start < 0 || end < 0) {
|
||||
setSelectedRowKeys([targetRowKey]);
|
||||
setAnchorRowKey(targetRowKey);
|
||||
return;
|
||||
}
|
||||
const from = Math.min(start, end);
|
||||
const to = Math.max(start, end);
|
||||
setSelectedRowKeys(selectableRowOrder.slice(from, to + 1));
|
||||
setAnchorRowKey(anchor);
|
||||
}
|
||||
|
||||
function handleRowSelectionClick(
|
||||
row: VisibleGridRow,
|
||||
cellKey: CellKey,
|
||||
options?: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean }
|
||||
) {
|
||||
if (isSelectableRowType(row.rowType)) {
|
||||
const ctrlOrMeta = Boolean(options?.ctrlKey || options?.metaKey);
|
||||
if (options?.shiftKey) {
|
||||
selectRowRange(row.rowKey);
|
||||
} else if (ctrlOrMeta) {
|
||||
setSelectedRowKeys((current) => {
|
||||
if (current.includes(row.rowKey)) {
|
||||
return current.filter((key) => key !== row.rowKey);
|
||||
}
|
||||
return [...current, row.rowKey];
|
||||
});
|
||||
setAnchorRowKey(row.rowKey);
|
||||
} else {
|
||||
setSelectedRowKeys([row.rowKey]);
|
||||
setAnchorRowKey(row.rowKey);
|
||||
}
|
||||
} else if (!options?.ctrlKey && !options?.metaKey && !options?.shiftKey) {
|
||||
setSelectedRowKeys([]);
|
||||
setAnchorRowKey(null);
|
||||
}
|
||||
setSelectedCell({ rowKey: row.rowKey, cellKey });
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}
|
||||
|
||||
function buildSelectionIntent(cell: SelectedCell): SelectionIntent | null {
|
||||
const row = findRow(cell.rowKey);
|
||||
if (!row) {
|
||||
@@ -1102,6 +1180,46 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
}, [data, editableCells, visibleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = pendingSelectedDeviceRowIdsAfterReload.current;
|
||||
if (!pending || pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
const rowKeys: string[] = [];
|
||||
for (const row of visibleRows) {
|
||||
if ((row.rowType === "deviceRow" || row.rowType === "circuitCompact") && row.device?.id && pending.includes(row.device.id)) {
|
||||
rowKeys.push(row.rowKey);
|
||||
}
|
||||
}
|
||||
if (rowKeys.length > 0) {
|
||||
setSelectedRowKeys(rowKeys);
|
||||
setAnchorRowKey(rowKeys[0]);
|
||||
}
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = null;
|
||||
}, [visibleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
const pending = pendingSelectedCircuitIdsAfterReload.current;
|
||||
if (!pending || pending.length === 0) {
|
||||
return;
|
||||
}
|
||||
const rowKeys: string[] = [];
|
||||
for (const row of visibleRows) {
|
||||
if (
|
||||
(row.rowType === "circuitCompact" || row.rowType === "circuitSummary" || row.rowType === "reserveCircuit") &&
|
||||
row.circuit?.id &&
|
||||
pending.includes(row.circuit.id)
|
||||
) {
|
||||
rowKeys.push(row.rowKey);
|
||||
}
|
||||
}
|
||||
if (rowKeys.length > 0) {
|
||||
setSelectedRowKeys(rowKeys);
|
||||
setAnchorRowKey(rowKeys[0]);
|
||||
}
|
||||
pendingSelectedCircuitIdsAfterReload.current = null;
|
||||
}, [visibleRows]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingFocus) {
|
||||
return;
|
||||
@@ -1745,6 +1863,38 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return event.dataTransfer.getData("application/x-circuit-device-row-id") || null;
|
||||
}
|
||||
|
||||
function parseDraggedDeviceRowIds(event: DragEvent<HTMLElement>) {
|
||||
const raw = event.dataTransfer.getData("application/x-circuit-device-row-ids");
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseDraggedCircuitIds(event: DragEvent<HTMLElement>) {
|
||||
const raw = event.dataTransfer.getData("application/x-circuit-ids");
|
||||
if (!raw) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
return parsed.filter((entry): entry is string => typeof entry === "string" && entry.length > 0);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function findDeviceRowCircuitId(deviceRowId: string) {
|
||||
for (const section of data?.sections ?? []) {
|
||||
for (const circuit of section.circuits) {
|
||||
@@ -1756,6 +1906,46 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
function getSelectedEligibleDeviceRowIds(primaryRowId?: string | null) {
|
||||
const selectedSet = new Set(selectedRowKeys);
|
||||
const ids: string[] = [];
|
||||
for (const row of visibleRows) {
|
||||
if (!selectedSet.has(row.rowKey)) {
|
||||
continue;
|
||||
}
|
||||
if ((row.rowType === "deviceRow" || row.rowType === "circuitCompact") && row.device?.id) {
|
||||
ids.push(row.device.id);
|
||||
}
|
||||
}
|
||||
if (ids.length > 1 && primaryRowId && ids.includes(primaryRowId)) {
|
||||
return ids;
|
||||
}
|
||||
return primaryRowId ? [primaryRowId] : ids;
|
||||
}
|
||||
|
||||
function getSelectedEligibleCircuitIds(primaryCircuitId?: string | null) {
|
||||
const selectedSet = new Set(selectedRowKeys);
|
||||
const ids: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const row of visibleRows) {
|
||||
if (!selectedSet.has(row.rowKey)) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
(row.rowType === "circuitCompact" || row.rowType === "circuitSummary" || row.rowType === "reserveCircuit") &&
|
||||
row.circuit?.id &&
|
||||
!seen.has(row.circuit.id)
|
||||
) {
|
||||
seen.add(row.circuit.id);
|
||||
ids.push(row.circuit.id);
|
||||
}
|
||||
}
|
||||
if (ids.length > 1 && primaryCircuitId && ids.includes(primaryCircuitId)) {
|
||||
return ids;
|
||||
}
|
||||
return primaryCircuitId ? [primaryCircuitId] : ids;
|
||||
}
|
||||
|
||||
function findCircuitSectionId(circuitId: string) {
|
||||
for (const section of data?.sections ?? []) {
|
||||
if (section.circuits.some((circuit) => circuit.id === circuitId)) {
|
||||
@@ -1765,28 +1955,28 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return null;
|
||||
}
|
||||
|
||||
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitId: string) {
|
||||
async function applyCircuitReorder(intent: CircuitReorderDropIntent, sourceCircuitIds: 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) {
|
||||
const block = sourceCircuitIds.filter((id) => ids.includes(id));
|
||||
if (block.length === 0) {
|
||||
throw new Error("Invalid source circuit.");
|
||||
}
|
||||
const nextIds = [...ids];
|
||||
nextIds.splice(fromIndex, 1);
|
||||
const blockSet = new Set(block);
|
||||
const nextIds = ids.filter((id) => !blockSet.has(id));
|
||||
|
||||
if (intent.kind === "section-end") {
|
||||
nextIds.push(sourceCircuitId);
|
||||
nextIds.push(...block);
|
||||
} 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);
|
||||
nextIds.splice(insertIndex, 0, ...block);
|
||||
}
|
||||
await reorderSectionCircuits(section.id, nextIds);
|
||||
}
|
||||
@@ -1797,6 +1987,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const draggedId = parseDraggedProjectDeviceId(event) ?? draggingProjectDeviceId;
|
||||
setDropIntent(null);
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDraggingCircuitIds([]);
|
||||
if (!draggedId) {
|
||||
setError("Missing dragged project device.");
|
||||
return;
|
||||
@@ -1858,32 +2050,57 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const rowId = parseDraggedDeviceRowId(event) ?? draggingDeviceRowId;
|
||||
const draggedRowIds = parseDraggedDeviceRowIds(event);
|
||||
const singleRowId = parseDraggedDeviceRowId(event) ?? draggingDeviceRowId;
|
||||
const rowIds = draggedRowIds.length > 0 ? draggedRowIds : singleRowId ? [singleRowId] : [];
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingDeviceRowId(null);
|
||||
if (!rowId) {
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDraggingCircuitIds([]);
|
||||
if (rowIds.length === 0) {
|
||||
setError("Missing dragged device row.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||
const sourceByRowId = new Map<string, string>();
|
||||
for (const rowId of rowIds) {
|
||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||
if (!sourceCircuitId) {
|
||||
setError("Invalid dragged device row.");
|
||||
return;
|
||||
}
|
||||
sourceByRowId.set(rowId, sourceCircuitId);
|
||||
}
|
||||
const sourceCircuitId = sourceByRowId.get(rowIds[0]);
|
||||
if (!sourceCircuitId) {
|
||||
setError("Invalid dragged device row.");
|
||||
setError("Invalid dragged device row source.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (intent.circuitId === sourceCircuitId) {
|
||||
if (rowIds.length === 1 && intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
const groupedBySource = new Map<string, string[]>();
|
||||
for (const rowId of rowIds) {
|
||||
const source = sourceByRowId.get(rowId)!;
|
||||
if (!groupedBySource.has(source)) {
|
||||
groupedBySource.set(source, []);
|
||||
}
|
||||
groupedBySource.get(source)!.push(rowId);
|
||||
}
|
||||
await runCommand({
|
||||
label: "Move device row",
|
||||
label: rowIds.length > 1 ? `Move ${rowIds.length} device rows` : "Move device row",
|
||||
redo: async () => {
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
||||
await moveCircuitDeviceRowsBulk({ rowIds, targetCircuitId: intent.circuitId });
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||
return null;
|
||||
},
|
||||
undo: async () => {
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: sourceCircuitId });
|
||||
for (const [source, ids] of groupedBySource) {
|
||||
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
|
||||
}
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -1891,21 +2108,34 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
|
||||
let createdCircuitId: string | null = null;
|
||||
const groupedBySource = new Map<string, string[]>();
|
||||
for (const rowId of rowIds) {
|
||||
const source = sourceByRowId.get(rowId)!;
|
||||
if (!groupedBySource.has(source)) {
|
||||
groupedBySource.set(source, []);
|
||||
}
|
||||
groupedBySource.get(source)!.push(rowId);
|
||||
}
|
||||
await runCommand({
|
||||
label: "Move device row to new circuit",
|
||||
label: rowIds.length > 1 ? `Move ${rowIds.length} device rows to new circuit` : "Move device row to new circuit",
|
||||
redo: async () => {
|
||||
const moved = (await moveCircuitDeviceRowById(rowId, {
|
||||
const moved = await moveCircuitDeviceRowsBulk({
|
||||
rowIds,
|
||||
targetSectionId: intent.sectionId,
|
||||
createNewCircuit: true,
|
||||
})) as { circuitId?: string };
|
||||
createdCircuitId = moved.circuitId ?? null;
|
||||
});
|
||||
createdCircuitId = moved.createdCircuitId ?? null;
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||
return null;
|
||||
},
|
||||
undo: async () => {
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: sourceCircuitId });
|
||||
for (const [source, ids] of groupedBySource) {
|
||||
await moveCircuitDeviceRowsBulk({ rowIds: ids, targetCircuitId: source });
|
||||
}
|
||||
if (createdCircuitId) {
|
||||
await deleteCircuitById(createdCircuitId);
|
||||
}
|
||||
pendingSelectedDeviceRowIdsAfterReload.current = rowIds;
|
||||
return null;
|
||||
},
|
||||
});
|
||||
@@ -1914,10 +2144,13 @@ 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;
|
||||
const draggedCircuitIds = parseDraggedCircuitIds(event);
|
||||
const singleCircuitId = event.dataTransfer.getData("application/x-circuit-id") || draggingCircuitId;
|
||||
const sourceCircuitIds = draggedCircuitIds.length > 0 ? draggedCircuitIds : singleCircuitId ? [singleCircuitId] : [];
|
||||
setCircuitReorderIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
if (!sourceCircuitId) {
|
||||
setDraggingCircuitIds([]);
|
||||
if (sourceCircuitIds.length === 0) {
|
||||
setError("Missing dragged circuit.");
|
||||
return;
|
||||
}
|
||||
@@ -1930,27 +2163,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setError("Invalid section id.");
|
||||
return;
|
||||
}
|
||||
const sectionCircuitIds = new Set(section.circuits.map((circuit) => circuit.id));
|
||||
if (sourceCircuitIds.some((id) => !sectionCircuitIds.has(id))) {
|
||||
setError("Cross-section circuit move is not allowed in this phase.");
|
||||
return;
|
||||
}
|
||||
const beforeOrder = section.circuits.map((circuit) => circuit.id);
|
||||
const primaryCircuitId = sourceCircuitIds[0];
|
||||
await runCommand({
|
||||
label: "Reorder circuits",
|
||||
label: sourceCircuitIds.length > 1 ? `Reorder ${sourceCircuitIds.length} circuits` : "Reorder circuits",
|
||||
redo: async () => {
|
||||
await applyCircuitReorder(intent, sourceCircuitId);
|
||||
await applyCircuitReorder(intent, sourceCircuitIds);
|
||||
pendingSelectedCircuitIdsAfterReload.current = sourceCircuitIds;
|
||||
return {
|
||||
rowKey: `circuitSummary:${sourceCircuitId}`,
|
||||
rowKey: `circuitSummary:${primaryCircuitId}`,
|
||||
cellKey: "equipmentIdentifier",
|
||||
rowType: "circuitSummary",
|
||||
sectionId: intent.sectionId,
|
||||
circuitId: sourceCircuitId,
|
||||
circuitId: primaryCircuitId,
|
||||
};
|
||||
},
|
||||
undo: async () => {
|
||||
await reorderSectionCircuits(intent.sectionId, beforeOrder);
|
||||
pendingSelectedCircuitIdsAfterReload.current = sourceCircuitIds;
|
||||
return {
|
||||
rowKey: `circuitSummary:${sourceCircuitId}`,
|
||||
rowKey: `circuitSummary:${primaryCircuitId}`,
|
||||
cellKey: "equipmentIdentifier",
|
||||
rowType: "circuitSummary",
|
||||
sectionId: intent.sectionId,
|
||||
circuitId: sourceCircuitId,
|
||||
circuitId: primaryCircuitId,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -2077,6 +2318,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
if (selectedRowKeys.length > 0) {
|
||||
event.preventDefault();
|
||||
setSelectedRowKeys([]);
|
||||
setAnchorRowKey(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (event.ctrlKey && event.key.toLowerCase() === "z") {
|
||||
event.preventDefault();
|
||||
if (event.shiftKey) {
|
||||
@@ -2229,6 +2478,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
|
||||
const isSortedView = Boolean(sortState);
|
||||
const hasActiveSortOrFilter = isSortedView || hasActiveFilters;
|
||||
const draggingDeviceCount = draggingDeviceRowIds.length > 0 ? draggingDeviceRowIds.length : draggingDeviceRowId ? 1 : 0;
|
||||
const activeDraggedCircuitIds =
|
||||
draggingCircuitIds.length > 0 ? draggingCircuitIds : draggingCircuitId ? [draggingCircuitId] : [];
|
||||
const draggingCircuitCount = activeDraggedCircuitIds.length;
|
||||
|
||||
return (
|
||||
<div className="tree-editor-shell">
|
||||
@@ -2335,7 +2588,9 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
setSelectedProjectDeviceId(device.id);
|
||||
setDraggingProjectDeviceId(device.id);
|
||||
setDraggingDeviceRowId(null);
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDraggingCircuitId(null);
|
||||
setDraggingCircuitIds([]);
|
||||
setDeviceMoveIntent(null);
|
||||
setCircuitReorderIntent(null);
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
@@ -2344,6 +2599,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
onDragEnd={() => {
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDraggingCircuitIds([]);
|
||||
}}
|
||||
>
|
||||
<strong>{device.displayName || device.name}</strong>
|
||||
@@ -2401,6 +2658,12 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
tabIndex={0}
|
||||
onFocus={handleContainerFocus}
|
||||
onKeyDown={handleContainerKeyDown}
|
||||
onMouseDown={(event) => {
|
||||
if (event.target === event.currentTarget) {
|
||||
setSelectedRowKeys([]);
|
||||
setAnchorRowKey(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<table className="tree-grid">
|
||||
<thead>
|
||||
@@ -2490,7 +2753,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? "drop-target-active" : ""
|
||||
}`}
|
||||
onDragOver={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
if (draggingCircuitCount > 0) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "none";
|
||||
setCircuitReorderIntent({
|
||||
@@ -2520,7 +2783,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
void handleDropWithIntent(event, { kind: "new-circuit", sectionId: section.id });
|
||||
return;
|
||||
}
|
||||
if (draggingCircuitId) {
|
||||
if (draggingCircuitCount > 0) {
|
||||
void handleCircuitReorderDrop(event, {
|
||||
kind: "section-end",
|
||||
sectionId: section.id,
|
||||
@@ -2613,26 +2876,31 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
? "drop-target-invalid"
|
||||
: ""
|
||||
} ${
|
||||
row.circuit?.id && draggingCircuitId && row.circuit.id === draggingCircuitId ? "circuit-dragging-block" : ""
|
||||
row.circuit?.id &&
|
||||
((draggingCircuitIds.length > 0 && draggingCircuitIds.includes(row.circuit.id)) ||
|
||||
(draggingCircuitIds.length === 0 && draggingCircuitId && row.circuit.id === draggingCircuitId))
|
||||
? "circuit-dragging-block"
|
||||
: ""
|
||||
} ${selectedRowKeys.includes(row.rowKey) ? "row-selected" : ""}
|
||||
}`}
|
||||
onClick={() => setActiveSectionId(row.sectionId)}
|
||||
onDragOver={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
if (draggingCircuitCount > 0) {
|
||||
const sourceSectionIds = activeDraggedCircuitIds
|
||||
.map((id) => findCircuitSectionId(id))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
const valid = sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId);
|
||||
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) {
|
||||
if (activeDraggedCircuitIds.includes(row.circuit.id)) {
|
||||
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();
|
||||
@@ -2660,7 +2928,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceRowId) {
|
||||
if (draggingDeviceCount > 0) {
|
||||
if (row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
@@ -2668,8 +2936,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
const sourceCircuitId = findDeviceRowCircuitId(draggingDeviceRowId);
|
||||
if (sourceCircuitId && sourceCircuitId !== row.circuit.id) {
|
||||
const sourceCircuitIds = (draggingDeviceRowIds.length > 0 ? draggingDeviceRowIds : draggingDeviceRowId ? [draggingDeviceRowId] : [])
|
||||
.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 });
|
||||
@@ -2719,28 +2989,32 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
});
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (draggingCircuitId) {
|
||||
if (draggingCircuitCount > 0) {
|
||||
if (row.rowType === "placeholder") {
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
const sourceSectionIds = activeDraggedCircuitIds
|
||||
.map((id) => findCircuitSectionId(id))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
void handleCircuitReorderDrop(event, {
|
||||
kind: "section-end",
|
||||
sectionId: row.sectionId,
|
||||
valid: sourceSectionId === row.sectionId,
|
||||
valid: sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
if (row.circuit.id === draggingCircuitId) {
|
||||
if (activeDraggedCircuitIds.includes(row.circuit.id)) {
|
||||
return;
|
||||
}
|
||||
const sourceSectionId = findCircuitSectionId(draggingCircuitId);
|
||||
const sourceSectionIds = activeDraggedCircuitIds
|
||||
.map((id) => findCircuitSectionId(id))
|
||||
.filter((id): id is string => Boolean(id));
|
||||
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,
|
||||
valid: sourceSectionIds.length > 0 && sourceSectionIds.every((sectionId) => sectionId === row.sectionId),
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -2759,7 +3033,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceRowId) {
|
||||
if (draggingDeviceCount > 0) {
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleDeviceRowDropWithIntent(event, { kind: "move-to-new-circuit", sectionId: row.sectionId });
|
||||
return;
|
||||
@@ -2794,7 +3068,11 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
? "circuit-drag-handle"
|
||||
: ""
|
||||
} ${
|
||||
draggingDeviceRowId && row.device?.id === draggingDeviceRowId ? "device-dragging" : ""
|
||||
row.device?.id &&
|
||||
((draggingDeviceRowIds.length > 0 && draggingDeviceRowIds.includes(row.device.id)) ||
|
||||
(draggingDeviceRowIds.length === 0 && draggingDeviceRowId === row.device.id))
|
||||
? "device-dragging"
|
||||
: ""
|
||||
}`}
|
||||
draggable={
|
||||
!isEditing &&
|
||||
@@ -2815,14 +3093,18 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
row.rowType === "circuitSummary" ||
|
||||
row.rowType === "reserveCircuit")
|
||||
) {
|
||||
const circuitIds = getSelectedEligibleCircuitIds(row.circuit.id);
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingDeviceRowId(null);
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingCircuitId(row.circuit.id);
|
||||
setDraggingCircuitIds(circuitIds);
|
||||
setCircuitReorderIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-id", row.circuit.id);
|
||||
event.dataTransfer.setData("application/x-circuit-ids", JSON.stringify(circuitIds));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -2830,26 +3112,35 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
column.key === "displayName" &&
|
||||
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
) {
|
||||
const rowIds = getSelectedEligibleDeviceRowIds(row.device.id);
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
setDraggingCircuitIds([]);
|
||||
setCircuitReorderIntent(null);
|
||||
setDraggingDeviceRowId(row.device.id);
|
||||
setDraggingDeviceRowIds(rowIds);
|
||||
setDeviceMoveIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id);
|
||||
event.dataTransfer.setData("application/x-circuit-device-row-ids", JSON.stringify(rowIds));
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDraggingDeviceRowId(null);
|
||||
setDraggingDeviceRowIds([]);
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingCircuitId(null);
|
||||
setDraggingCircuitIds([]);
|
||||
setCircuitReorderIntent(null);
|
||||
}}
|
||||
onClick={() => {
|
||||
onClick={(event) => {
|
||||
if (cell.editable) {
|
||||
setSelectedCell({ rowKey: row.rowKey, cellKey: column.key });
|
||||
requestAnimationFrame(() => containerRef.current?.focus());
|
||||
handleRowSelectionClick(row, column.key, {
|
||||
ctrlKey: event.ctrlKey,
|
||||
metaKey: event.metaKey,
|
||||
shiftKey: event.shiftKey,
|
||||
});
|
||||
}
|
||||
}}
|
||||
onDoubleClick={() => {
|
||||
@@ -2935,16 +3226,18 @@ 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 device to new circuit</span>
|
||||
<span className="drop-hint">{`move ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} to new circuit`}</span>
|
||||
) : null}
|
||||
{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 ${draggingDeviceCount || 1} device${draggingDeviceCount === 1 ? "" : "s"} 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"}
|
||||
{circuitReorderIntent.valid
|
||||
? `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} to section end`
|
||||
: "cross-section move not allowed"}
|
||||
</span>
|
||||
) : null}
|
||||
{circuitReorderIntent &&
|
||||
@@ -2953,8 +3246,8 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<span className="drop-hint">
|
||||
{circuitReorderIntent.valid
|
||||
? circuitReorderIntent.kind === "before-circuit"
|
||||
? "move circuit before this circuit"
|
||||
: "move circuit after this circuit"
|
||||
? `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} before this circuit`
|
||||
: `move ${draggingCircuitCount || 1} circuit${draggingCircuitCount === 1 ? "" : "s"} after this circuit`
|
||||
: "cross-section move not allowed"}
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
@@ -142,6 +142,21 @@ export function moveCircuitDeviceRowById(
|
||||
});
|
||||
}
|
||||
|
||||
export function moveCircuitDeviceRowsBulk(input: {
|
||||
rowIds: string[];
|
||||
targetCircuitId?: string;
|
||||
targetSectionId?: string;
|
||||
createNewCircuit?: boolean;
|
||||
}) {
|
||||
return request<{ movedRowIds: string[]; targetCircuitId: string; createdCircuitId?: string }>(
|
||||
"/api/circuit-device-rows/move-bulk",
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(input),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function renumberCircuitSection(sectionId: string) {
|
||||
return request(`/api/circuit-sections/${sectionId}/renumber`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
|
||||
import { CircuitWriteService } from "../../domain/services/circuit-write.service.js";
|
||||
import {
|
||||
createCircuitDeviceRowSchema,
|
||||
moveCircuitDeviceRowsBulkSchema,
|
||||
moveCircuitDeviceRowSchema,
|
||||
updateCircuitDeviceRowSchema,
|
||||
} from "../../shared/validation/circuit.schemas.js";
|
||||
@@ -78,3 +79,17 @@ export async function moveCircuitDeviceRow(req: Request, res: Response) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to move device row." });
|
||||
}
|
||||
}
|
||||
|
||||
export async function moveCircuitDeviceRowsBulk(req: Request, res: Response) {
|
||||
const parsed = moveCircuitDeviceRowsBulkSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: parsed.error.flatten() });
|
||||
}
|
||||
|
||||
try {
|
||||
const moved = await circuitWriteService.moveDeviceRowsBulk(parsed.data);
|
||||
return res.json(moved);
|
||||
} catch (error) {
|
||||
return res.status(400).json({ error: error instanceof Error ? error.message : "Failed to move device rows." });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
deleteCircuitDeviceRow,
|
||||
moveCircuitDeviceRowsBulk,
|
||||
moveCircuitDeviceRow,
|
||||
updateCircuitDeviceRow,
|
||||
} from "../controllers/circuit-device-row.controller.js";
|
||||
|
||||
export const circuitDeviceRowRouter = Router();
|
||||
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/move-bulk", moveCircuitDeviceRowsBulk);
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId", updateCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.patch("/circuit-device-rows/:rowId/move", moveCircuitDeviceRow);
|
||||
circuitDeviceRowRouter.delete("/circuit-device-rows/:rowId", deleteCircuitDeviceRow);
|
||||
|
||||
@@ -62,6 +62,20 @@ export const moveCircuitDeviceRowSchema = z
|
||||
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." }
|
||||
);
|
||||
|
||||
export const moveCircuitDeviceRowsBulkSchema = z
|
||||
.object({
|
||||
rowIds: z.array(z.string().min(1)).min(1),
|
||||
targetCircuitId: z.string().min(1).optional(),
|
||||
targetSectionId: z.string().min(1).optional(),
|
||||
createNewCircuit: z.boolean().optional(),
|
||||
})
|
||||
.refine(
|
||||
(value) =>
|
||||
Boolean(value.targetCircuitId) ||
|
||||
(Boolean(value.targetSectionId) && value.createNewCircuit === true),
|
||||
{ message: "Either targetCircuitId or targetSectionId+createNewCircuit=true is required." }
|
||||
);
|
||||
|
||||
export const reorderSectionCircuitsSchema = z.object({
|
||||
orderedCircuitIds: z.array(z.string().min(1)).min(1),
|
||||
});
|
||||
@@ -82,5 +96,6 @@ 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 MoveCircuitDeviceRowsBulkInput = z.infer<typeof moveCircuitDeviceRowsBulkSchema>;
|
||||
export type ReorderSectionCircuitsInput = z.infer<typeof reorderSectionCircuitsSchema>;
|
||||
export type UpdateSectionEquipmentIdentifiersInput = z.infer<typeof updateSectionEquipmentIdentifiersSchema>;
|
||||
|
||||
Reference in New Issue
Block a user