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 { Trash2 } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { Module } from '../../types';
|
||||
import { Modal } from '../ui/Modal';
|
||||
@@ -15,7 +16,7 @@ interface ModuleEditPanelProps {
|
||||
}
|
||||
|
||||
export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps) {
|
||||
const { updateModuleLocal } = useRackStore();
|
||||
const { updateModuleLocal, removeModuleLocal } = useRackStore();
|
||||
const [name, setName] = useState(module.name);
|
||||
const [ipAddress, setIpAddress] = useState(module.ipAddress ?? '');
|
||||
const [manufacturer, setManufacturer] = useState(module.manufacturer ?? '');
|
||||
@@ -23,9 +24,12 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
||||
const [notes, setNotes] = useState(module.notes ?? '');
|
||||
const [uSize, setUSize] = useState(module.uSize);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setConfirmingDelete(false);
|
||||
setName(module.name);
|
||||
setIpAddress(module.ipAddress ?? '');
|
||||
setManufacturer(module.manufacturer ?? '');
|
||||
@@ -35,6 +39,21 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
||||
}
|
||||
}, [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) {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
@@ -132,13 +151,51 @@ export function ModuleEditPanel({ module, open, onClose }: ModuleEditPanelProps)
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-1">
|
||||
<Button variant="secondary" size="sm" type="button" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button size="sm" type="submit" loading={loading} disabled={!name.trim()}>
|
||||
Save Changes
|
||||
</Button>
|
||||
<div className="flex items-center justify-between gap-3 pt-1 border-t border-slate-700 mt-1">
|
||||
{/* 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
|
||||
</Button>
|
||||
</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
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { useDraggable } from '@dnd-kit/core';
|
||||
import { Trash2, GripVertical, GripHorizontal } from 'lucide-react';
|
||||
import { GripHorizontal } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import type { Module } from '../../types';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { MODULE_TYPE_COLORS, U_HEIGHT_PX, PORTS_PER_ROW } from '../../lib/constants';
|
||||
import { ConfirmDialog } from '../ui/ConfirmDialog';
|
||||
import { ModuleEditPanel } from '../modals/ModuleEditPanel';
|
||||
import { PortConfigModal } from '../modals/PortConfigModal';
|
||||
import { useRackStore } from '../../store/useRackStore';
|
||||
@@ -16,11 +15,9 @@ interface ModuleBlockProps {
|
||||
}
|
||||
|
||||
export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
const { racks, removeModuleLocal, updateModuleLocal } = useRackStore();
|
||||
const { racks, updateModuleLocal } = useRackStore();
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
|
||||
const [deletingLoading, setDeletingLoading] = useState(false);
|
||||
const [portModalOpen, setPortModalOpen] = useState(false);
|
||||
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) {
|
||||
setSelectedPortId(portId);
|
||||
setPortModalOpen(true);
|
||||
@@ -130,11 +113,13 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
<>
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
className={cn(
|
||||
'relative w-full border-l-4 select-none overflow-hidden transition-opacity',
|
||||
colors.bg,
|
||||
colors.border,
|
||||
isDragging ? 'opacity-0' : 'cursor-pointer',
|
||||
isDragging ? 'opacity-0' : 'cursor-grab active:cursor-grabbing',
|
||||
!isDragging && hovered && 'brightness-110',
|
||||
previewUSize !== null && 'ring-1 ring-white/30'
|
||||
)}
|
||||
@@ -148,23 +133,9 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
aria-label={`Edit ${module.name}`}
|
||||
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 */}
|
||||
{hasPorts && previewUSize === null ? (
|
||||
<div
|
||||
className="flex flex-col gap-[3px] pl-4 pr-6 pt-[5px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex flex-col gap-[3px] px-2 pt-[5px]">
|
||||
{visibleRows.map((row, rowIdx) => (
|
||||
<div key={rowIdx} className="flex gap-[3px]">
|
||||
{row.map((port) => {
|
||||
@@ -172,7 +143,8 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
return (
|
||||
<button
|
||||
key={port.id}
|
||||
onClick={() => openPort(port.id)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onClick={(e) => { e.stopPropagation(); openPort(port.id); }}
|
||||
aria-label={`Port ${port.portNumber}`}
|
||||
title={`Port ${port.portNumber}${port.label ? ` · ${port.label}` : ''}`}
|
||||
className={cn(
|
||||
@@ -191,9 +163,8 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
/* No ports or resizing — show nothing (color communicates type) */
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* 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 */}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -228,7 +185,7 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
hovered || previewUSize !== null ? 'opacity-100' : 'opacity-0 hover:opacity-100',
|
||||
'transition-opacity'
|
||||
)}
|
||||
onPointerDown={handleResizePointerDown}
|
||||
onPointerDown={(e) => { e.stopPropagation(); handleResizePointerDown(e); }}
|
||||
onPointerMove={handleResizePointerMove}
|
||||
onPointerUp={handleResizePointerUp}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@@ -241,16 +198,6 @@ export function ModuleBlock({ module }: ModuleBlockProps) {
|
||||
|
||||
<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 && (
|
||||
<PortConfigModal
|
||||
portId={selectedPortId}
|
||||
|
||||
Reference in New Issue
Block a user