feat: move dashboard edit button to bottom with label, replace menu order arrows with drag-and-drop

This commit is contained in:
Matthias Hochmeister
2026-03-28 15:40:03 +01:00
parent 0a912e60b5
commit 443f3569bd
2 changed files with 92 additions and 57 deletions

View File

@@ -20,9 +20,25 @@ import {
ListItem,
ListItemText,
} from '@mui/material';
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode, Widgets, Cloud, LinkOff, Forum, Person, OpenInNew, ArrowUpward, ArrowDownward, Sort, Restore } from '@mui/icons-material';
import { Settings as SettingsIcon, Notifications, Palette, Language, SettingsBrightness, LightMode, DarkMode, Widgets, Cloud, LinkOff, Forum, Person, OpenInNew, Sort, Restore, DragIndicator } from '@mui/icons-material';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import {
DndContext,
PointerSensor,
useSensor,
useSensors,
closestCenter,
} from '@dnd-kit/core';
import type { DragEndEvent } from '@dnd-kit/core';
import {
SortableContext,
verticalListSortingStrategy,
arrayMove,
useSortable,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { useNavigate } from 'react-router-dom';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useThemeMode } from '../contexts/ThemeContext';
import { preferencesApi } from '../services/settings';
@@ -48,9 +64,37 @@ const ORDERABLE_NAV_ITEMS = [
{ text: 'Wissen', path: '/wissen', permission: 'wissen:view' },
{ text: 'Bestellungen', path: '/bestellungen', permission: 'bestellungen:view' },
{ text: 'Interne Bestellungen', path: '/ausruestungsanfrage', permission: 'ausruestungsanfrage:view' },
{ text: 'Checklisten', path: '/checklisten', permission: 'checklisten:view' },
{ text: 'Issues', path: '/issues', permission: 'issues:view_own' },
];
function SortableNavItem({ id, text }: { id: string; text: string }) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id });
return (
<ListItem
ref={setNodeRef}
disablePadding
sx={{
py: 0.25,
opacity: isDragging ? 0.4 : 1,
transform: CSS.Transform.toString(transform),
transition,
}}
>
<IconButton
size="small"
{...attributes}
{...listeners}
sx={{ cursor: 'grab', touchAction: 'none', mr: 0.5 }}
disableRipple
>
<DragIndicator fontSize="small" />
</IconButton>
<ListItemText primary={text} primaryTypographyProps={{ variant: 'body2' }} />
</ListItem>
);
}
function Settings() {
const { themeMode, setThemeMode } = useThemeMode();
const queryClient = useQueryClient();
@@ -96,13 +140,23 @@ function Settings() {
return 0;
});
const moveNavItem = (fromIdx: number, toIdx: number) => {
if (toIdx < 0 || toIdx >= orderedNavItems.length) return;
const newOrder = [...orderedNavItems];
const [moved] = newOrder.splice(fromIdx, 1);
newOrder.splice(toIdx, 0, moved);
const [localNavItems, setLocalNavItems] = useState(orderedNavItems);
useEffect(() => { setLocalNavItems(orderedNavItems); }, [menuOrder.join(',')]);
const navSensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
);
const handleNavDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIdx = localNavItems.findIndex((i) => i.path === active.id);
const newIdx = localNavItems.findIndex((i) => i.path === over.id);
if (oldIdx === -1 || newIdx === -1) return;
const newItems = arrayMove(localNavItems, oldIdx, newIdx);
setLocalNavItems(newItems);
const current = preferences ?? {};
mutation.mutate({ ...current, menuOrder: newOrder.map((i) => i.path) });
mutation.mutate({ ...current, menuOrder: newItems.map((i) => i.path) });
};
const resetMenuOrder = () => {
@@ -270,41 +324,22 @@ function Settings() {
<CircularProgress size={24} />
</Box>
) : (
<List disablePadding>
{orderedNavItems.map((item, idx) => (
<ListItem
key={item.path}
disablePadding
sx={{ py: 0.25 }}
secondaryAction={
<Box>
<IconButton
size="small"
onClick={() => moveNavItem(idx, idx - 1)}
disabled={idx === 0 || mutation.isPending}
aria-label="Nach oben"
>
<ArrowUpward fontSize="small" />
</IconButton>
<IconButton
size="small"
onClick={() => moveNavItem(idx, idx + 1)}
disabled={idx === orderedNavItems.length - 1 || mutation.isPending}
aria-label="Nach unten"
>
<ArrowDownward fontSize="small" />
</IconButton>
</Box>
}
>
<ListItemText
primary={item.text}
primaryTypographyProps={{ variant: 'body2' }}
sx={{ pr: 8 }}
/>
</ListItem>
))}
</List>
<DndContext
sensors={navSensors}
collisionDetection={closestCenter}
onDragEnd={handleNavDragEnd}
>
<SortableContext
items={localNavItems.map((i) => i.path)}
strategy={verticalListSortingStrategy}
>
<List disablePadding>
{localNavItems.map((item) => (
<SortableNavItem key={item.path} id={item.path} text={item.text} />
))}
</List>
</SortableContext>
</DndContext>
)}
</CardContent>
</Card>