feat: move dashboard edit button to bottom with label, replace menu order arrows with drag-and-drop
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user