fix(dashboard): a refused archive, delete or copy says why instead of only that it failed

A member without the right to archive a trip clicked Archive and read
"Failed to archive trip", while the server had answered why. The archive,
restore, delete and copy toasts now show the server's reason, as creating
and editing a trip already did; a failure without one keeps the old line.
The buttons stay where they are.

Refs #2604
This commit is contained in:
Maurice
2026-10-12 15:46:17 +02:00
parent 125649ce1b
commit a0331bf53b
2 changed files with 43 additions and 12 deletions
@@ -207,7 +207,7 @@ describe('useDashboard', () => {
});
it('FE-HOOK-DASH-014: a failing delete toasts and keeps the trip', async () => {
server.use(http.delete('/api/trips/:id', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
server.use(http.delete('/api/trips/:id', () => HttpResponse.json({}, { status: 500 })));
const { result } = await mountLoaded();
act(() => { result.current.setDeleteTrip(PARIS); });
@@ -230,7 +230,7 @@ describe('useDashboard', () => {
});
it('FE-HOOK-DASH-016: a failing archive only toasts', async () => {
server.use(http.put('/api/trips/:id', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
server.use(http.put('/api/trips/:id', () => HttpResponse.json({}, { status: 500 })));
const { result } = await mountLoaded();
await act(async () => { await result.current.handleArchive(101); });
@@ -238,6 +238,35 @@ describe('useDashboard', () => {
expect(toastCalls).toContainEqual(['Failed to archive trip', 'error']);
});
it('FE-HOOK-DASH-016b: an archive the server refuses says why (#2604)', async () => {
server.use(http.put('/api/trips/:id', () => HttpResponse.json({ error: 'No permission to archive/unarchive this trip' }, { status: 403 })));
const { result } = await mountLoaded();
await act(async () => { await result.current.handleArchive(101); });
await act(async () => { await result.current.handleUnarchive(103); });
expect(toastCalls.filter(c => c[1] === 'error')).toEqual([
['No permission to archive/unarchive this trip', 'error'],
['No permission to archive/unarchive this trip', 'error'],
]);
});
it('FE-HOOK-DASH-016c: a delete or copy the server refuses says why (#2604)', async () => {
server.use(
http.delete('/api/trips/:id', () => HttpResponse.json({ error: 'No permission to delete this trip' }, { status: 403 })),
http.post('/api/trips/:id/copy', () => HttpResponse.json({ error: 'No permission to create trips' }, { status: 403 })),
);
const { result } = await mountLoaded();
act(() => { result.current.setDeleteTrip(PARIS); });
await act(async () => { await result.current.confirmDelete(); });
act(() => { result.current.setCopyTrip(PARIS); });
await act(async () => { await result.current.confirmCopy(); });
expect(toastCalls).toContainEqual(['No permission to delete this trip', 'error']);
expect(toastCalls).toContainEqual(['No permission to create trips', 'error']);
});
it('FE-HOOK-DASH-017: restoring moves the trip back into the active list', async () => {
server.use(http.put('/api/trips/:id', () => HttpResponse.json({ trip: { ...ROME, is_archived: 0 } })));
const { result } = await mountLoaded();
@@ -252,7 +281,7 @@ describe('useDashboard', () => {
});
it('FE-HOOK-DASH-018: a failing restore only toasts', async () => {
server.use(http.put('/api/trips/:id', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
server.use(http.put('/api/trips/:id', () => HttpResponse.json({}, { status: 500 })));
const { result } = await mountLoaded();
await act(async () => { await result.current.handleUnarchive(103); });
@@ -284,7 +313,7 @@ describe('useDashboard', () => {
});
it('FE-HOOK-DASH-021: a failing copy only toasts', async () => {
server.use(http.post('/api/trips/:id/copy', () => HttpResponse.json({ error: 'nope' }, { status: 500 })));
server.use(http.post('/api/trips/:id/copy', () => HttpResponse.json({}, { status: 500 })));
const { result } = await mountLoaded();
act(() => { result.current.setCopyTrip(PARIS); });
+10 -8
View File
@@ -140,6 +140,8 @@ export function useDashboard() {
}
}
// A refusal says why (no right to archive or delete this trip, say) rather
// than only that it failed; the generic line stays for a failure without one.
const confirmDelete = async () => {
if (!deleteTrip) return
try {
@@ -147,8 +149,8 @@ export function useDashboard() {
setTrips(prev => prev.filter(t => t.id !== deleteTrip.id))
setArchivedTrips(prev => prev.filter(t => t.id !== deleteTrip.id))
toast.success(t('dashboard.toast.deleted'))
} catch {
toast.error(t('dashboard.toast.deleteError'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('dashboard.toast.deleteError')))
}
setDeleteTrip(null)
}
@@ -159,8 +161,8 @@ export function useDashboard() {
setTrips(prev => prev.filter(t => t.id !== id))
setArchivedTrips(prev => sortTrips([data.trip, ...prev]))
toast.success(t('dashboard.toast.archived'))
} catch {
toast.error(t('dashboard.toast.archiveError'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('dashboard.toast.archiveError')))
}
}
@@ -170,8 +172,8 @@ export function useDashboard() {
setArchivedTrips(prev => prev.filter(t => t.id !== id))
setTrips(prev => sortTrips([data.trip, ...prev]))
toast.success(t('dashboard.toast.restored'))
} catch {
toast.error(t('dashboard.toast.restoreError'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('dashboard.toast.restoreError')))
}
}
@@ -181,8 +183,8 @@ export function useDashboard() {
const data = await tripsApi.copy(copyTrip.id, { title: `${copyTrip.title} (${t('dashboard.copySuffix')})` })
setTrips(prev => sortTrips([data.trip, ...prev]))
toast.success(t('dashboard.toast.copied'))
} catch {
toast.error(t('dashboard.toast.copyError'))
} catch (err: unknown) {
toast.error(getApiErrorMessage(err, t('dashboard.toast.copyError')))
}
setCopyTrip(null)
}