Fix module drag + move delete button into edit modal
Module drag broken:
listeners were on a 12px grip strip only; dragging anywhere else on
the block had no effect. Moved {...listeners} {...attributes} to the
outer container so the whole module face is the drag source.
Port buttons now stop pointerdown propagation so clicking a port does
not accidentally start a drag. Resize handle also stops pointerdown
propagation before forwarding to its own handler.
Removed the now-redundant GripVertical strip.
Delete button covering ports 23-24:
Removed the absolute-positioned Trash2 button from ModuleBlock face.
Delete is now inside ModuleEditPanel with an inline confirm flow:
- 'Delete module' link in the modal footer (left side)
- Clicking shows 'Remove this module? [Delete] [Cancel]' inline
- On confirm: calls API, removeModuleLocal, closes modal
ConfirmDialog import and related state also removed from ModuleBlock.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, type FormEvent } from 'react';
|
import { useState, useEffect, type FormEvent } from 'react';
|
||||||
|
import { Trash2 } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { Module } from '../../types';
|
import type { Module } from '../../types';
|
||||||
import { Modal } from '../ui/Modal';
|
import { Modal } from '../ui/Modal';
|
||||||
@@ -15,7 +16,7 @@ interface ModuleEditPanelProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps) {
|
export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps) {
|
||||||
const { updateModuleLocal } = useRackStore();
|
const { updateModuleLocal, removeModuleLocal } = useRackStore();
|
||||||
const [name, setName] = useState(module.name);
|
const [name, setName] = useState(module.name);
|
||||||
const [ipAddress, setIpAddress] = useState(module.ipAddress ?? '');
|
const [ipAddress, setIpAddress] = useState(module.ipAddress ?? '');
|
||||||
const [manufacturer, setManufacturer] = useState(module.manufacturer ?? '');
|
const [manufacturer, setManufacturer] = useState(module.manufacturer ?? '');
|
||||||
@@ -23,9 +24,12 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
|||||||
const [notes, setNotes] = useState(module.notes ?? '');
|
const [notes, setNotes] = useState(module.notes ?? '');
|
||||||
const [uSize, setUSize] = useState(module.uSize);
|
const [uSize, setUSize] = useState(module.uSize);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) {
|
if (open) {
|
||||||
|
setConfirmingDelete(false);
|
||||||
setName(module.name);
|
setName(module.name);
|
||||||
setIpAddress(module.ipAddress ?? '');
|
setIpAddress(module.ipAddress ?? '');
|
||||||
setManufacturer(module.manufacturer ?? '');
|
setManufacturer(module.manufacturer ?? '');
|
||||||
@@ -35,6 +39,21 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
|||||||
}
|
}
|
||||||
}, [open, module]);
|
}, [open, module]);
|
||||||
|
|
||||||
|
async function handleDelete() {
|
||||||
|
setDeleting(true);
|
||||||
|
try {
|
||||||
|
await apiClient.modules.delete(module.id);
|
||||||
|
removeModuleLocal(module.id);
|
||||||
|
toast.success(`${module.name} removed`);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : 'Delete failed');
|
||||||
|
} finally {
|
||||||
|
setDeleting(false);
|
||||||
|
setConfirmingDelete(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubmit(e: FormEvent) {
|
async function handleSubmit(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -132,14 +151,52 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end gap-3 pt-1">
|
<div className="flex items-center justify-between gap-3 pt-1 border-t border-slate-700 mt-1">
|
||||||
<Button variant="secondary" size="sm" type="button" onClick={onClose} disabled={loading}>
|
{/* Delete — left side with inline confirm */}
|
||||||
|
{confirmingDelete ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-red-400">Remove this module?</span>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
loading={deleting}
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setConfirmingDelete(false)}
|
||||||
|
disabled={deleting}
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button size="sm" type="submit" loading={loading} disabled={!name.trim()}>
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setConfirmingDelete(true)}
|
||||||
|
disabled={loading}
|
||||||
|
className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-red-400 transition-colors disabled:opacity-40"
|
||||||
|
>
|
||||||
|
<Trash2 size={13} />
|
||||||
|
Delete module
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Save / Cancel — right side */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="secondary" size="sm" type="button" onClick={onClose} disabled={loading || deleting}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" type="submit" loading={loading} disabled={!name.trim() || deleting}>
|
||||||
Save Changes
|
Save Changes
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef } from 'react';
|
||||||
import { useDraggable } from '@dnd-kit/core';
|
import { useDraggable } from '@dnd-kit/core';
|
||||||
import { Trash2, GripVertical, GripHorizontal } from 'lucide-react';
|
import { GripHorizontal } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { Module } from '../../types';
|
import type { Module } from '../../types';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
import { MODULE_TYPE_COLORS, U_HEIGHT_PX, PORTS_PER_ROW } from '../../lib/constants';
|
import { MODULE_TYPE_COLORS, U_HEIGHT_PX, PORTS_PER_ROW } from '../../lib/constants';
|
||||||
import { ConfirmDialog } from '../ui/ConfirmDialog';
|
|
||||||
import { ModuleEditPanel } from '../modals/ModuleEditPanel';
|
import { ModuleEditPanel } from '../modals/ModuleEditPanel';
|
||||||
import { PortConfigModal } from '../modals/PortConfigModal';
|
import { PortConfigModal } from '../modals/PortConfigModal';
|
||||||
import { useRackStore } from '../../store/useRackStore';
|
import { useRackStore } from '../../store/useRackStore';
|
||||||
@@ -16,11 +15,9 @@ interface ModuleBlockProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ModuleBlock({ module }: ModuleBlockProps) {
|
export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||||
const { racks, removeModuleLocal, updateModuleLocal } = useRackStore();
|
const { racks, updateModuleLocal } = useRackStore();
|
||||||
const [hovered, setHovered] = useState(false);
|
const [hovered, setHovered] = useState(false);
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
|
||||||
const [deletingLoading, setDeletingLoading] = useState(false);
|
|
||||||
const [portModalOpen, setPortModalOpen] = useState(false);
|
const [portModalOpen, setPortModalOpen] = useState(false);
|
||||||
const [selectedPortId, setSelectedPortId] = useState<string | null>(null);
|
const [selectedPortId, setSelectedPortId] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -107,20 +104,6 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete() {
|
|
||||||
setDeletingLoading(true);
|
|
||||||
try {
|
|
||||||
await apiClient.modules.delete(module.id);
|
|
||||||
removeModuleLocal(module.id);
|
|
||||||
toast.success(`${module.name} removed`);
|
|
||||||
} catch (e) {
|
|
||||||
toast.error(e instanceof Error ? e.message : 'Delete failed');
|
|
||||||
} finally {
|
|
||||||
setDeletingLoading(false);
|
|
||||||
setConfirmDeleteOpen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function openPort(portId: string) {
|
function openPort(portId: string) {
|
||||||
setSelectedPortId(portId);
|
setSelectedPortId(portId);
|
||||||
setPortModalOpen(true);
|
setPortModalOpen(true);
|
||||||
@@ -130,11 +113,13 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
|
{...listeners}
|
||||||
|
{...attributes}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative w-full border-l-4 select-none overflow-hidden transition-opacity',
|
'relative w-full border-l-4 select-none overflow-hidden transition-opacity',
|
||||||
colors.bg,
|
colors.bg,
|
||||||
colors.border,
|
colors.border,
|
||||||
isDragging ? 'opacity-0' : 'cursor-pointer',
|
isDragging ? 'opacity-0' : 'cursor-grab active:cursor-grabbing',
|
||||||
!isDragging && hovered && 'brightness-110',
|
!isDragging && hovered && 'brightness-110',
|
||||||
previewUSize !== null && 'ring-1 ring-white/30'
|
previewUSize !== null && 'ring-1 ring-white/30'
|
||||||
)}
|
)}
|
||||||
@@ -148,23 +133,9 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
aria-label={`Edit ${module.name}`}
|
aria-label={`Edit ${module.name}`}
|
||||||
onKeyDown={(e) => e.key === 'Enter' && setEditOpen(true)}
|
onKeyDown={(e) => e.key === 'Enter' && setEditOpen(true)}
|
||||||
>
|
>
|
||||||
{/* Drag handle — slim left strip */}
|
|
||||||
<div
|
|
||||||
{...listeners}
|
|
||||||
{...attributes}
|
|
||||||
className="absolute left-0 top-0 bottom-3 w-3 flex items-center justify-center cursor-grab active:cursor-grabbing text-white/20 hover:text-white/60 transition-colors z-10 touch-none"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
aria-label={`Drag ${module.name}`}
|
|
||||||
>
|
|
||||||
<GripVertical size={9} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Port grid — primary face content */}
|
{/* Port grid — primary face content */}
|
||||||
{hasPorts && previewUSize === null ? (
|
{hasPorts && previewUSize === null ? (
|
||||||
<div
|
<div className="flex flex-col gap-[3px] px-2 pt-[5px]">
|
||||||
className="flex flex-col gap-[3px] pl-4 pr-6 pt-[5px]"
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
{visibleRows.map((row, rowIdx) => (
|
{visibleRows.map((row, rowIdx) => (
|
||||||
<div key={rowIdx} className="flex gap-[3px]">
|
<div key={rowIdx} className="flex gap-[3px]">
|
||||||
{row.map((port) => {
|
{row.map((port) => {
|
||||||
@@ -172,7 +143,8 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={port.id}
|
key={port.id}
|
||||||
onClick={() => openPort(port.id)}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onClick={(e) => { e.stopPropagation(); openPort(port.id); }}
|
||||||
aria-label={`Port ${port.portNumber}`}
|
aria-label={`Port ${port.portNumber}`}
|
||||||
title={`Port ${port.portNumber}${port.label ? ` · ${port.label}` : ''}`}
|
title={`Port ${port.portNumber}${port.label ? ` · ${port.label}` : ''}`}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -191,9 +163,8 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
/* No ports or resizing — show nothing (color communicates type) */
|
|
||||||
previewUSize === null && (
|
previewUSize === null && (
|
||||||
<div className="pl-4 pt-1.5 text-[10px] text-white/30 italic select-none">no ports</div>
|
<div className="px-2 pt-1.5 text-[10px] text-white/30 italic select-none">no ports</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -206,20 +177,6 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Delete button — hover only */}
|
|
||||||
{hovered && previewUSize === null && (
|
|
||||||
<button
|
|
||||||
className="absolute top-0.5 right-0.5 p-0.5 rounded bg-red-900/80 hover:bg-red-600 text-white transition-colors z-10"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setConfirmDeleteOpen(true);
|
|
||||||
}}
|
|
||||||
aria-label={`Delete ${module.name}`}
|
|
||||||
>
|
|
||||||
<Trash2 size={10} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Resize handle — bottom edge */}
|
{/* Resize handle — bottom edge */}
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -228,7 +185,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
hovered || previewUSize !== null ? 'opacity-100' : 'opacity-0 hover:opacity-100',
|
hovered || previewUSize !== null ? 'opacity-100' : 'opacity-0 hover:opacity-100',
|
||||||
'transition-opacity'
|
'transition-opacity'
|
||||||
)}
|
)}
|
||||||
onPointerDown={handleResizePointerDown}
|
onPointerDown={(e) => { e.stopPropagation(); handleResizePointerDown(e); }}
|
||||||
onPointerMove={handleResizePointerMove}
|
onPointerMove={handleResizePointerMove}
|
||||||
onPointerUp={handleResizePointerUp}
|
onPointerUp={handleResizePointerUp}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
@@ -241,16 +198,6 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
|||||||
|
|
||||||
<ModuleEditPanel module={module} open={editOpen} onClose={() => setEditOpen(false)} />
|
<ModuleEditPanel module={module} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
open={confirmDeleteOpen}
|
|
||||||
onClose={() => setConfirmDeleteOpen(false)}
|
|
||||||
onConfirm={handleDelete}
|
|
||||||
title="Remove Module"
|
|
||||||
message={`Remove "${module.name}" from the rack? This will also delete all associated port configuration.`}
|
|
||||||
confirmLabel="Remove"
|
|
||||||
loading={deletingLoading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedPortId && (
|
{selectedPortId && (
|
||||||
<PortConfigModal
|
<PortConfigModal
|
||||||
portId={selectedPortId}
|
portId={selectedPortId}
|
||||||
|
|||||||
Reference in New Issue
Block a user