feat(dashboard): make widget groups reorderable via drag-and-drop

This commit is contained in:
Matthias Hochmeister
2026-04-13 15:15:50 +02:00
parent dd5cd71fd1
commit b275d4baa5
2 changed files with 136 additions and 34 deletions

View File

@@ -1,37 +1,60 @@
import React from 'react';
import { Box, Typography, IconButton } from '@mui/material';
import { Delete as DeleteIcon } from '@mui/icons-material';
import { Delete as DeleteIcon, DragIndicator as DragIndicatorIcon } from '@mui/icons-material';
import { useDroppable } from '@dnd-kit/core';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
interface WidgetGroupProps {
title: string;
children: React.ReactNode;
gridColumn?: string;
groupId?: string;
sortableId?: string;
editMode?: boolean;
onDelete?: () => void;
}
function WidgetGroup({ title, children, gridColumn, groupId, editMode, onDelete }: WidgetGroupProps) {
const { setNodeRef, isOver } = useDroppable({
function WidgetGroup({ title, children, groupId, sortableId, editMode, onDelete }: WidgetGroupProps) {
const { setNodeRef: setDropRef, isOver } = useDroppable({
id: groupId ? `group-${groupId}` : 'group-default',
disabled: !editMode,
});
const {
setNodeRef: setSortableRef,
attributes,
listeners,
transform,
transition,
isDragging,
} = useSortable({
id: sortableId ?? '',
disabled: !editMode || !sortableId,
});
// Count non-null children to hide empty groups
const validChildren = React.Children.toArray(children).filter(Boolean);
if (validChildren.length === 0 && !editMode) return null;
const sortableStyle = sortableId ? {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : 1,
zIndex: isDragging ? 1 : undefined,
} : {};
return (
<Box
ref={setNodeRef}
ref={setSortableRef}
style={sortableStyle}
sx={{
position: 'relative',
borderRadius: 2,
p: 2.5,
pt: 3.5,
gridColumn,
gridColumn: '1 / -1',
bgcolor: isOver && editMode ? 'rgba(25, 118, 210, 0.04)' : 'rgba(0, 0, 0, 0.02)',
border: '1px solid',
borderColor: isOver && editMode ? 'primary.light' : 'rgba(0, 0, 0, 0.04)',
@@ -40,6 +63,7 @@ function WidgetGroup({ title, children, gridColumn, groupId, editMode, onDelete
}}
>
<Box
ref={setDropRef}
sx={{
position: 'absolute',
top: -9,
@@ -53,6 +77,16 @@ function WidgetGroup({ title, children, gridColumn, groupId, editMode, onDelete
borderRadius: 1,
}}
>
{editMode && sortableId && (
<IconButton
size="small"
{...attributes}
{...listeners}
sx={{ p: 0, mr: 0.25, color: 'text.disabled', cursor: 'grab', '&:active': { cursor: 'grabbing' } }}
>
<DragIndicatorIcon sx={{ fontSize: 14 }} />
</IconButton>
)}
<Typography
sx={{
fontSize: '0.6875rem',