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 mockApiFetch = vi.mocked(apiFetch);
|
||||||
|
|
||||||
const existing: Activity[] = [
|
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() {
|
function renderActivities() {
|
||||||
@@ -103,4 +122,45 @@ describe('Activities', () => {
|
|||||||
|
|
||||||
expect(mockApiFetch).not.toHaveBeenCalledWith('/api/activities/1', { method: 'DELETE' });
|
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,
|
useActivities,
|
||||||
useCreateActivity,
|
useCreateActivity,
|
||||||
useDeleteActivity,
|
useDeleteActivity,
|
||||||
|
useReorderActivities,
|
||||||
useUpdateActivity,
|
useUpdateActivity,
|
||||||
} from '../api/activities';
|
} from '../api/activities';
|
||||||
|
|
||||||
@@ -71,6 +72,7 @@ export default function Activities() {
|
|||||||
const createActivity = useCreateActivity();
|
const createActivity = useCreateActivity();
|
||||||
const updateActivity = useUpdateActivity();
|
const updateActivity = useUpdateActivity();
|
||||||
const deleteActivity = useDeleteActivity();
|
const deleteActivity = useDeleteActivity();
|
||||||
|
const reorderActivities = useReorderActivities();
|
||||||
|
|
||||||
// Add form state.
|
// Add form state.
|
||||||
const [newName, setNewName] = useState('');
|
const [newName, setNewName] = useState('');
|
||||||
@@ -127,6 +129,14 @@ export default function Activities() {
|
|||||||
if (ok) deleteActivity.mutate(id);
|
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 (
|
return (
|
||||||
<div className="screen">
|
<div className="screen">
|
||||||
<h1 className="screen-title">Handelingen</h1>
|
<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>
|
<p className="muted">Nog geen stappen. Voeg er een toe hierboven.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="activity-list">
|
<ul className="activity-list">
|
||||||
{activities.map((activity) => (
|
{activities.map((activity, index) => (
|
||||||
<li key={activity.id} className="activity-card">
|
<li key={activity.id} className="activity-card">
|
||||||
{editingId === activity.id ? (
|
{editingId === activity.id ? (
|
||||||
<>
|
<>
|
||||||
@@ -202,6 +212,24 @@ export default function Activities() {
|
|||||||
<div className="activity-row">
|
<div className="activity-row">
|
||||||
<span className="activity-name">{activity.name}</span>
|
<span className="activity-name">{activity.name}</span>
|
||||||
<div className="row-actions">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="icon-btn icon-edit"
|
className="icon-btn icon-edit"
|
||||||
|
|||||||
Reference in New Issue
Block a user