feat(rack): add shift-click context modal for connections with color and edge type configurability
This commit is contained in:
@@ -200,8 +200,10 @@ const edges = {
|
|||||||
// ---- Connections ----
|
// ---- Connections ----
|
||||||
|
|
||||||
const connections = {
|
const connections = {
|
||||||
create: (data: { fromPortId: string; toPortId: string; color?: string; label?: string }) =>
|
create: (data: { fromPortId: string; toPortId: string; color?: string; label?: string; edgeType?: string }) =>
|
||||||
post<{ id: string }>('/connections', data),
|
post<{ id: string }>('/connections', data),
|
||||||
|
update: (id: string, data: Partial<{ color: string; label: string; edgeType: string }>) =>
|
||||||
|
put<{ id: string }>(`/connections/${id}`, data),
|
||||||
delete: (id: string) => del<null>(`/connections/${id}`),
|
delete: (id: string) => del<null>(`/connections/${id}`),
|
||||||
deleteByPorts: (p1: string, p2: string) => del<null>(`/connections/ports/${p1}/${p2}`),
|
deleteByPorts: (p1: string, p2: string) => del<null>(`/connections/ports/${p1}/${p2}`),
|
||||||
};
|
};
|
||||||
|
|||||||
181
client/src/components/modals/ConnectionConfigModal.tsx
Normal file
181
client/src/components/modals/ConnectionConfigModal.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import { useState, useEffect, useMemo, type FormEvent } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Modal } from '../ui/Modal';
|
||||||
|
import { Button } from '../ui/Button';
|
||||||
|
import { useRackStore } from '../../store/useRackStore';
|
||||||
|
import type { Connection } from '../../types';
|
||||||
|
|
||||||
|
interface ConnectionConfigModalProps {
|
||||||
|
connectionId: string | null;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EDGE_STYLES = [
|
||||||
|
{ value: 'bezier', label: 'Curved (Bezier)' },
|
||||||
|
{ value: 'straight', label: 'Straight Line' },
|
||||||
|
{ value: 'step', label: 'Stepped / Orthogonal' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PRESET_COLORS = [
|
||||||
|
'#3b82f6', // Blue
|
||||||
|
'#10b981', // Emerald
|
||||||
|
'#8b5cf6', // Violet
|
||||||
|
'#ef4444', // Red
|
||||||
|
'#f59e0b', // Amber
|
||||||
|
'#ec4899', // Pink
|
||||||
|
'#64748b', // Slate
|
||||||
|
'#ffffff', // White
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ConnectionConfigModal({ connectionId, open, onClose }: ConnectionConfigModalProps) {
|
||||||
|
const { racks, setCablingFromPortId, updateConnection, deleteConnection } = useRackStore();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
// Synchronously find the connection from the global store
|
||||||
|
const connection = useMemo(() => {
|
||||||
|
if (!connectionId) return null;
|
||||||
|
for (const rack of racks) {
|
||||||
|
for (const mod of rack.modules) {
|
||||||
|
for (const port of mod.ports) {
|
||||||
|
const found = port.sourceConnections?.find((c) => c.id === connectionId);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [racks, connectionId]);
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [color, setColor] = useState('#3b82f6');
|
||||||
|
const [edgeType, setEdgeType] = useState('bezier');
|
||||||
|
const [label, setLabel] = useState('');
|
||||||
|
|
||||||
|
// Reset form state when connection is found or changed
|
||||||
|
useEffect(() => {
|
||||||
|
if (connection && open) {
|
||||||
|
setColor(connection.color || '#3b82f6');
|
||||||
|
setEdgeType(connection.edgeType || 'bezier');
|
||||||
|
setLabel(connection.label || '');
|
||||||
|
}
|
||||||
|
}, [connection, open]);
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!connectionId) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await updateConnection(connectionId, { color, edgeType, label });
|
||||||
|
toast.success('Connection updated');
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Update failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
if (!connectionId) return;
|
||||||
|
if (!confirm('Are you sure you want to remove this connection?')) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await deleteConnection(connectionId);
|
||||||
|
toast.success('Connection removed');
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(err instanceof Error ? err.message : 'Delete failed');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!open || !connection) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => !loading && onClose()} title="Edit Connection">
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Customize the cable style and color.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form id="connection-form" onSubmit={handleSubmit} className="flex flex-col gap-5">
|
||||||
|
{/* Label Name */}
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<label className="text-sm font-semibold text-slate-300">Cable Label (Optional)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={label}
|
||||||
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
|
placeholder="e.g. Uplink to Core"
|
||||||
|
className="px-3 py-2 rounded-md bg-slate-900 border border-slate-700 text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Color Picker */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-semibold text-slate-300">Cable Color</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{PRESET_COLORS.map((c) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setColor(c)}
|
||||||
|
style={{ backgroundColor: c }}
|
||||||
|
className={`w-8 h-8 rounded-full border-2 transition-all hover:scale-110 ${
|
||||||
|
color === c ? 'border-white scale-110 shadow-lg' : 'border-transparent'
|
||||||
|
}`}
|
||||||
|
aria-label={`Select color ${c}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="relative w-8 h-8 rounded-full overflow-hidden border-2 border-slate-700">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={color}
|
||||||
|
onChange={(e) => setColor(e.target.value)}
|
||||||
|
className="absolute -top-2 -left-2 w-12 h-12 cursor-pointer"
|
||||||
|
title="Custom color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Edge Style */}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-semibold text-slate-300">Curve Style</label>
|
||||||
|
<div className="grid grid-cols-3 gap-2">
|
||||||
|
{EDGE_STYLES.map((style) => (
|
||||||
|
<button
|
||||||
|
key={style.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setEdgeType(style.value)}
|
||||||
|
className={`px-3 py-2 rounded-md border text-xs font-medium transition-all ${
|
||||||
|
edgeType === style.value
|
||||||
|
? 'bg-blue-500/10 border-blue-500 text-blue-400'
|
||||||
|
: 'bg-slate-900 border-slate-700 text-slate-400 hover:border-slate-500 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{style.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t border-slate-800">
|
||||||
|
<Button type="button" variant="ghost" className="text-red-400 hover:text-red-300" onClick={handleDelete} disabled={loading}>
|
||||||
|
Delete Connection
|
||||||
|
</Button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button type="button" variant="ghost" onClick={onClose} disabled={loading}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" form="connection-form" variant="primary" loading={loading}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useState, useMemo, useCallback } from 'react';
|
import { useEffect, useState, useMemo, useCallback } from 'react';
|
||||||
import { useRackStore } from '../../store/useRackStore';
|
import { useRackStore } from '../../store/useRackStore';
|
||||||
|
import { cn } from '../../lib/utils';
|
||||||
|
|
||||||
export function ConnectionLayer() {
|
export function ConnectionLayer() {
|
||||||
const { racks, cablingFromPortId } = useRackStore();
|
const { racks, cablingFromPortId, setActiveConfigConnectionId } = useRackStore();
|
||||||
const [coords, setCoords] = useState<Record<string, { x: number; y: number }>>({});
|
const [coords, setCoords] = useState<Record<string, { x: number; y: number }>>({});
|
||||||
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
|
||||||
|
|
||||||
@@ -78,7 +79,7 @@ export function ConnectionLayer() {
|
|||||||
}, [cablingFromPortId]);
|
}, [cablingFromPortId]);
|
||||||
|
|
||||||
const connections = useMemo(() => {
|
const connections = useMemo(() => {
|
||||||
const conns: { id: string; from: string; to: string; color?: string; fromRackId: string; toRackId: string }[] = [];
|
const conns: { id: string; from: string; to: string; color?: string; edgeType?: string }[] = [];
|
||||||
racks.forEach((rack) => {
|
racks.forEach((rack) => {
|
||||||
rack.modules.forEach((mod) => {
|
rack.modules.forEach((mod) => {
|
||||||
mod.ports.forEach((port) => {
|
mod.ports.forEach((port) => {
|
||||||
@@ -88,8 +89,7 @@ export function ConnectionLayer() {
|
|||||||
from: c.fromPortId,
|
from: c.fromPortId,
|
||||||
to: c.toPortId,
|
to: c.toPortId,
|
||||||
color: c.color,
|
color: c.color,
|
||||||
fromRackId: rack.id,
|
edgeType: c.edgeType,
|
||||||
toRackId: '' // We don't easily know the destination rack without searching
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -118,16 +118,25 @@ export function ConnectionLayer() {
|
|||||||
const end = coords[conn.to];
|
const end = coords[conn.to];
|
||||||
if (!start || !end) return null;
|
if (!start || !end) return null;
|
||||||
|
|
||||||
// Calculate a slight curve. If ports are close, use a tighter curve.
|
let d = '';
|
||||||
|
if (conn.edgeType === 'straight') {
|
||||||
|
d = `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
|
||||||
|
} else if (conn.edgeType === 'step') {
|
||||||
|
const midX = start.x + (end.x - start.x) / 2;
|
||||||
|
d = `M ${start.x} ${start.y} L ${midX} ${start.y} L ${midX} ${end.y} L ${end.x} ${end.y}`;
|
||||||
|
} else {
|
||||||
|
// default bezier
|
||||||
const dx = Math.abs(end.x - start.x);
|
const dx = Math.abs(end.x - start.x);
|
||||||
const dy = Math.abs(end.y - start.y);
|
const dy = Math.abs(end.y - start.y);
|
||||||
const distance = Math.sqrt(dx*dx + dy*dy);
|
const distance = Math.sqrt(dx*dx + dy*dy);
|
||||||
const curvature = Math.min(100, distance / 3);
|
const curvature = Math.min(100, distance / 3);
|
||||||
|
d = `M ${start.x} ${start.y} C ${start.x + curvature} ${start.y}, ${end.x - curvature} ${end.y}, ${end.x} ${end.y}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={conn.id} className="connection-group">
|
<g key={conn.id} className="connection-group">
|
||||||
<path
|
<path
|
||||||
d={`M ${start.x} ${start.y} C ${start.x + curvature} ${start.y}, ${end.x - curvature} ${end.y}, ${end.x} ${end.y}`}
|
d={d}
|
||||||
stroke={conn.color || '#3b82f6'}
|
stroke={conn.color || '#3b82f6'}
|
||||||
strokeWidth="2.5"
|
strokeWidth="2.5"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -136,11 +145,17 @@ export function ConnectionLayer() {
|
|||||||
/>
|
/>
|
||||||
{/* Thicker transparent helper for easier identification if we ever add hover interactions */}
|
{/* Thicker transparent helper for easier identification if we ever add hover interactions */}
|
||||||
<path
|
<path
|
||||||
d={`M ${start.x} ${start.y} C ${start.x + curvature} ${start.y}, ${end.x - curvature} ${end.y}, ${end.x} ${end.y}`}
|
d={d}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth="10"
|
strokeWidth="10"
|
||||||
fill="none"
|
fill="none"
|
||||||
className="pointer-events-auto cursor-help"
|
className="pointer-events-auto cursor-pointer"
|
||||||
|
onClick={(e) => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
e.stopPropagation();
|
||||||
|
setActiveConfigConnectionId(conn.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { DevicePalette } from './DevicePalette';
|
|||||||
import { ConnectionLayer } from './ConnectionLayer';
|
import { ConnectionLayer } from './ConnectionLayer';
|
||||||
import { AddModuleModal } from '../modals/AddModuleModal';
|
import { AddModuleModal } from '../modals/AddModuleModal';
|
||||||
import { PortConfigModal } from '../modals/PortConfigModal';
|
import { PortConfigModal } from '../modals/PortConfigModal';
|
||||||
|
import { ConnectionConfigModal } from '../modals/ConnectionConfigModal';
|
||||||
import { RackSkeleton } from '../ui/Skeleton';
|
import { RackSkeleton } from '../ui/Skeleton';
|
||||||
import type { ModuleType } from '../../types';
|
import type { ModuleType } from '../../types';
|
||||||
import { MODULE_TYPE_COLORS, MODULE_TYPE_LABELS } from '../../lib/constants';
|
import { MODULE_TYPE_COLORS, MODULE_TYPE_LABELS } from '../../lib/constants';
|
||||||
@@ -86,7 +87,7 @@ const POINTER_SENSOR_OPTIONS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function RackPlanner() {
|
export function RackPlanner() {
|
||||||
const { racks, loading, fetchRacks, moveModule, activeConfigPortId, setActiveConfigPortId } = useRackStore();
|
const { racks, loading, fetchRacks, moveModule, activeConfigPortId, setActiveConfigPortId, activeConfigConnectionId, setActiveConfigConnectionId } = useRackStore();
|
||||||
const canvasRef = useRef<HTMLDivElement>(null);
|
const canvasRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Drag state
|
// Drag state
|
||||||
@@ -304,6 +305,14 @@ export function RackPlanner() {
|
|||||||
onClose={() => setActiveConfigPortId(null)}
|
onClose={() => setActiveConfigPortId(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{activeConfigConnectionId && (
|
||||||
|
<ConnectionConfigModal
|
||||||
|
open={!!activeConfigConnectionId}
|
||||||
|
connectionId={activeConfigConnectionId}
|
||||||
|
onClose={() => setActiveConfigConnectionId(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,14 @@ interface RackState {
|
|||||||
cablingFromPortId: string | null;
|
cablingFromPortId: string | null;
|
||||||
setCablingFromPortId: (id: string | null) => void;
|
setCablingFromPortId: (id: string | null) => void;
|
||||||
createConnection: (fromPortId: string, toPortId: string) => Promise<void>;
|
createConnection: (fromPortId: string, toPortId: string) => Promise<void>;
|
||||||
|
updateConnection: (id: string, data: Partial<{ color: string; label: string; edgeType: string }>) => Promise<void>;
|
||||||
deleteConnection: (id: string) => Promise<void>;
|
deleteConnection: (id: string) => Promise<void>;
|
||||||
// Port Config Global Modal
|
// Port Config Global Modal
|
||||||
activeConfigPortId: string | null;
|
activeConfigPortId: string | null;
|
||||||
setActiveConfigPortId: (id: string | null) => void;
|
setActiveConfigPortId: (id: string | null) => void;
|
||||||
|
// Connection Config Global Modal
|
||||||
|
activeConfigConnectionId: string | null;
|
||||||
|
setActiveConfigConnectionId: (id: string | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRackStore = create<RackState>((set, get) => ({
|
export const useRackStore = create<RackState>((set, get) => ({
|
||||||
@@ -132,6 +136,15 @@ export const useRackStore = create<RackState>((set, get) => ({
|
|||||||
set({ racks });
|
set({ racks });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateConnection: async (id, data) => {
|
||||||
|
await apiClient.connections.update(id, data);
|
||||||
|
const racks = await apiClient.racks.list();
|
||||||
|
set({ racks });
|
||||||
|
},
|
||||||
|
|
||||||
activeConfigPortId: null,
|
activeConfigPortId: null,
|
||||||
setActiveConfigPortId: (id) => set({ activeConfigPortId: id }),
|
setActiveConfigPortId: (id) => set({ activeConfigPortId: id }),
|
||||||
|
|
||||||
|
activeConfigConnectionId: null,
|
||||||
|
setActiveConfigConnectionId: (id) => set({ activeConfigConnectionId: id }),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export interface Connection {
|
|||||||
toPortId: string;
|
toPortId: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
edgeType?: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Connection" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"fromPortId" TEXT NOT NULL,
|
||||||
|
"toPortId" TEXT NOT NULL,
|
||||||
|
"color" TEXT,
|
||||||
|
"label" TEXT,
|
||||||
|
"edgeType" TEXT NOT NULL DEFAULT 'bezier',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "Connection_fromPortId_fkey" FOREIGN KEY ("fromPortId") REFERENCES "Port" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "Connection_toPortId_fkey" FOREIGN KEY ("toPortId") REFERENCES "Port" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Connection" ("color", "createdAt", "fromPortId", "id", "label", "toPortId") SELECT "color", "createdAt", "fromPortId", "id", "label", "toPortId" FROM "Connection";
|
||||||
|
DROP TABLE "Connection";
|
||||||
|
ALTER TABLE "new_Connection" RENAME TO "Connection";
|
||||||
|
CREATE UNIQUE INDEX "Connection_fromPortId_toPortId_key" ON "Connection"("fromPortId", "toPortId");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -67,6 +67,7 @@ model Connection {
|
|||||||
toPort Port @relation("TargetPort", fields: [toPortId], references: [id], onDelete: Cascade)
|
toPort Port @relation("TargetPort", fields: [toPortId], references: [id], onDelete: Cascade)
|
||||||
color String? // Optional custom cable color
|
color String? // Optional custom cable color
|
||||||
label String? // Optional cable label (e.g. "Cable #104")
|
label String? // Optional cable label (e.g. "Cable #104")
|
||||||
|
edgeType String @default("bezier") // bezier | straight | step
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@unique([fromPortId, toPortId])
|
@@unique([fromPortId, toPortId])
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ const router = Router();
|
|||||||
// POST /api/connections
|
// POST /api/connections
|
||||||
router.post('/', async (req, res, next) => {
|
router.post('/', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const { fromPortId, toPortId, color, label } = req.body;
|
const { fromPortId, toPortId, color, label, edgeType } = req.body;
|
||||||
const conn = await connService.createConnection({ fromPortId, toPortId, color, label });
|
const conn = await connService.createConnection({ fromPortId, toPortId, color, label, edgeType });
|
||||||
res.status(201).json(conn);
|
res.status(201).json(conn);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PUT /api/connections/:id
|
||||||
|
router.put('/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const { color, label, edgeType } = req.body;
|
||||||
|
const conn = await connService.updateConnection(req.params.id, { color, label, edgeType });
|
||||||
|
res.json(conn);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// DELETE /api/connections/:id
|
// DELETE /api/connections/:id
|
||||||
router.delete('/:id', async (req, res, next) => {
|
router.delete('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { prisma } from '../lib/prisma';
|
import { prisma } from '../lib/prisma';
|
||||||
import { AppError } from '../types/index';
|
import { AppError } from '../types/index';
|
||||||
|
|
||||||
export async function createConnection(data: { fromPortId: string; toPortId: string; color?: string; label?: string }) {
|
export async function createConnection(data: { fromPortId: string; toPortId: string; color?: string; label?: string; edgeType?: string }) {
|
||||||
// Check if both ports exist
|
// Check if both ports exist
|
||||||
const [from, to] = await Promise.all([
|
const [from, to] = await Promise.all([
|
||||||
prisma.port.findUnique({ where: { id: data.fromPortId } }),
|
prisma.port.findUnique({ where: { id: data.fromPortId } }),
|
||||||
@@ -22,6 +22,13 @@ export async function deleteConnection(id: string) {
|
|||||||
return prisma.connection.delete({ where: { id } });
|
return prisma.connection.delete({ where: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateConnection(id: string, data: Partial<{ color: string; label: string; edgeType: string }>) {
|
||||||
|
return prisma.connection.update({
|
||||||
|
where: { id },
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteByPorts(portId1: string, portId2: string) {
|
export async function deleteByPorts(portId1: string, portId2: string) {
|
||||||
return prisma.connection.deleteMany({
|
return prisma.connection.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user