diff --git a/client/src/pages/dashboard/useDashboard.test.tsx b/client/src/pages/dashboard/useDashboard.test.tsx index f3e2d44c8..01f9591b2 100644 --- a/client/src/pages/dashboard/useDashboard.test.tsx +++ b/client/src/pages/dashboard/useDashboard.test.tsx @@ -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); }); diff --git a/client/src/pages/dashboard/useDashboard.ts b/client/src/pages/dashboard/useDashboard.ts index b3fb8967e..9bc7f2acd 100644 --- a/client/src/pages/dashboard/useDashboard.ts +++ b/client/src/pages/dashboard/useDashboard.ts @@ -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) }