feat(admin): reorder handelingen with up/down arrows
This commit is contained in:
@@ -47,3 +47,17 @@ export function useDeleteActivity() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useReorderActivities() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (ids: number[]) =>
|
||||
apiFetch<Activity[]>('/api/activities/reorder', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ids }),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['activities'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,8 +13,27 @@ vi.mock('../lib/api', () => ({
|
||||
const mockApiFetch = vi.mocked(apiFetch);
|
||||
|
||||
const existing: Activity[] = [
|
||||
{ id: 1, name: 'Frezen', insole_types: ['Kurk', 'Berk'], created_at: '2026-06-17T00:00:00.000Z' },
|
||||
{ id: 2, name: 'Lijmen', insole_types: ['3D'], created_at: '2026-06-17T00:00:00.000Z' },
|
||||
{
|
||||
id: 1,
|
||||
name: 'Frezen',
|
||||
insole_types: ['Kurk', 'Berk'],
|
||||
created_at: '2026-06-17T00:00:00.000Z',
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Lijmen',
|
||||
insole_types: ['3D'],
|
||||
created_at: '2026-06-17T00:00:00.000Z',
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Polijsten',
|
||||
insole_types: ['Kurk'],
|
||||
created_at: '2026-06-17T00:00:00.000Z',
|
||||
sort_order: 2,
|
||||
},
|
||||
];
|
||||
|
||||
function renderActivities() {
|
||||
@@ -103,4 +122,45 @@ describe('Activities', () => {
|
||||
|
||||
expect(mockApiFetch).not.toHaveBeenCalledWith('/api/activities/1', { method: 'DELETE' });
|
||||
});
|
||||
|
||||
it('moving the second row up PUTs /api/activities/reorder with the swapped ids', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderActivities();
|
||||
await screen.findByText('Lijmen');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Verplaats Lijmen omhoog' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiFetch).toHaveBeenCalledWith('/api/activities/reorder', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ids: [2, 1, 3] }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('moving the second row down PUTs /api/activities/reorder with the swapped ids', async () => {
|
||||
const user = userEvent.setup();
|
||||
renderActivities();
|
||||
await screen.findByText('Lijmen');
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Verplaats Lijmen omlaag' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApiFetch).toHaveBeenCalledWith('/api/activities/reorder', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ ids: [1, 3, 2] }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('disables up on the first row and down on the last row', async () => {
|
||||
renderActivities();
|
||||
await screen.findByText('Frezen');
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Verplaats Frezen omhoog' })).toBeDisabled();
|
||||
expect(screen.getByRole('button', { name: 'Verplaats Polijsten omlaag' })).toBeDisabled();
|
||||
// The opposite ends are enabled.
|
||||
expect(screen.getByRole('button', { name: 'Verplaats Frezen omlaag' })).toBeEnabled();
|
||||
expect(screen.getByRole('button', { name: 'Verplaats Polijsten omhoog' })).toBeEnabled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
useActivities,
|
||||
useCreateActivity,
|
||||
useDeleteActivity,
|
||||
useReorderActivities,
|
||||
useUpdateActivity,
|
||||
} from '../api/activities';
|
||||
|
||||
@@ -71,6 +72,7 @@ export default function Activities() {
|
||||
const createActivity = useCreateActivity();
|
||||
const updateActivity = useUpdateActivity();
|
||||
const deleteActivity = useDeleteActivity();
|
||||
const reorderActivities = useReorderActivities();
|
||||
|
||||
// Add form state.
|
||||
const [newName, setNewName] = useState('');
|
||||
@@ -127,6 +129,14 @@ export default function Activities() {
|
||||
if (ok) deleteActivity.mutate(id);
|
||||
}
|
||||
|
||||
function handleMove(index: number, direction: -1 | 1) {
|
||||
const target = index + direction;
|
||||
if (target < 0 || target >= activities.length) return;
|
||||
const ids = activities.map((a) => a.id);
|
||||
[ids[index], ids[target]] = [ids[target], ids[index]];
|
||||
reorderActivities.mutate(ids);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="screen">
|
||||
<h1 className="screen-title">Handelingen</h1>
|
||||
@@ -164,7 +174,7 @@ export default function Activities() {
|
||||
<p className="muted">Nog geen stappen. Voeg er een toe hierboven.</p>
|
||||
) : (
|
||||
<ul className="activity-list">
|
||||
{activities.map((activity) => (
|
||||
{activities.map((activity, index) => (
|
||||
<li key={activity.id} className="activity-card">
|
||||
{editingId === activity.id ? (
|
||||
<>
|
||||
@@ -202,6 +212,24 @@ export default function Activities() {
|
||||
<div className="activity-row">
|
||||
<span className="activity-name">{activity.name}</span>
|
||||
<div className="row-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn icon-move"
|
||||
aria-label={`Verplaats ${activity.name} omhoog`}
|
||||
disabled={index === 0 || reorderActivities.isPending}
|
||||
onClick={() => handleMove(index, -1)}
|
||||
>
|
||||
↑
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn icon-move"
|
||||
aria-label={`Verplaats ${activity.name} omlaag`}
|
||||
disabled={index === activities.length - 1 || reorderActivities.isPending}
|
||||
onClick={() => handleMove(index, 1)}
|
||||
>
|
||||
↓
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="icon-btn icon-edit"
|
||||
|
||||
Reference in New Issue
Block a user