From 959d6c3714e2bbc6973655de00ea3140fea6c407 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sun, 31 May 2026 22:57:39 +0200 Subject: [PATCH] Surface the real place-search error instead of a generic toast (#1092) When a place search or detail lookup fails, the backend already forwards the upstream reason - including descriptive Google Places API messages such as 'Places API (New) has not been used in project ... or it is disabled'. The planner discarded it and always showed 'Place search failed', so a key that is mis-enabled, unbilled, or pointed at the legacy API instead of Places API (New) looked like an unexplained silent failure. Show the server-provided message when present, and stop the Atlas bucket-list search from swallowing its error without a trace. --- client/src/components/Planner/PlaceFormModal.test.tsx | 9 ++++++--- client/src/components/Planner/PlaceFormModal.tsx | 5 +++-- client/src/pages/atlas/useAtlas.ts | 2 +- client/src/utils/apiError.ts | 9 +++++++++ 4 files changed, 19 insertions(+), 6 deletions(-) create mode 100644 client/src/utils/apiError.ts diff --git a/client/src/components/Planner/PlaceFormModal.test.tsx b/client/src/components/Planner/PlaceFormModal.test.tsx index 4ddfae0e..2dea519d 100644 --- a/client/src/components/Planner/PlaceFormModal.test.tsx +++ b/client/src/components/Planner/PlaceFormModal.test.tsx @@ -225,13 +225,16 @@ describe('PlaceFormModal', () => { expect(screen.getByDisplayValue('48.8584')).toBeInTheDocument(); }); - it('FE-PLANNER-PLACEFORM-021: maps search error shows toast', async () => { + it('FE-PLANNER-PLACEFORM-021: maps search error surfaces the server-provided reason', async () => { const addToast = vi.fn(); window.__addToast = addToast; const user = userEvent.setup(); + // The backend forwards the real upstream error (e.g. a Google Places API message); + // the modal must show it instead of a generic "search failed" so the cause is visible. server.use( - http.post('/api/maps/search', () => HttpResponse.json({ error: 'fail' }, { status: 500 })), + http.post('/api/maps/search', () => + HttpResponse.json({ error: 'Places API (New) has not been used in project 123 or it is disabled' }, { status: 403 })), ); render(); @@ -241,7 +244,7 @@ describe('PlaceFormModal', () => { await waitFor(() => { expect(addToast).toHaveBeenCalledWith( - expect.stringMatching(/search failed/i), + expect.stringMatching(/Places API \(New\) has not been used/i), 'error', undefined, ); diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx index ae70fa61..fc76fb79 100644 --- a/client/src/components/Planner/PlaceFormModal.tsx +++ b/client/src/components/Planner/PlaceFormModal.tsx @@ -10,6 +10,7 @@ import { Search, Paperclip, X, AlertTriangle, Loader2 } from 'lucide-react' import { useTranslation } from '../../i18n' import CustomTimePicker from '../shared/CustomTimePicker' import { DEFAULT_FORM, isGoogleMapsUrl, type PlaceFormData } from './PlaceFormModal.helpers' +import { getApiErrorMessage } from '../../utils/apiError' import type { Place, Category, Assignment } from '../../types' // The submit payload mirrors the form, but lat/lng are parsed to numbers and @@ -188,7 +189,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) { const result = await mapsApi.search(mapsSearch, language) setMapsResults(result.places || []) } catch (err: unknown) { - toast.error(t('places.mapsSearchError')) + toast.error(getApiErrorMessage(err, t('places.mapsSearchError'))) } finally { setIsSearchingMaps(false) } @@ -228,7 +229,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) { } catch (err) { console.error('Failed to fetch place details:', err) setMapsSearch(previousSearch) - toast.error(t('places.mapsSearchError')) + toast.error(getApiErrorMessage(err, t('places.mapsSearchError'))) } finally { setIsSearchingMaps(false) } diff --git a/client/src/pages/atlas/useAtlas.ts b/client/src/pages/atlas/useAtlas.ts index d8dee913..2fef96ed 100644 --- a/client/src/pages/atlas/useAtlas.ts +++ b/client/src/pages/atlas/useAtlas.ts @@ -622,7 +622,7 @@ export function useAtlas() { try { const result = await mapsApi.search(bucketSearch, language) setBucketSearchResults(result.places || []) - } catch {} finally { setBucketSearching(false) } + } catch (err) { console.error('Bucket-list place search failed:', err) } finally { setBucketSearching(false) } } const handleSelectBucketPoi = (result: any) => { diff --git a/client/src/utils/apiError.ts b/client/src/utils/apiError.ts new file mode 100644 index 00000000..181cb42a --- /dev/null +++ b/client/src/utils/apiError.ts @@ -0,0 +1,9 @@ +/** + * Pulls the server-provided error string out of an axios-style error so the UI can + * surface the real reason (e.g. a Google Places API message such as "Places API (New) + * has not been used in project … or it is disabled") instead of a generic fallback. + */ +export function getApiErrorMessage(err: unknown, fallback: string): string { + const message = (err as { response?: { data?: { error?: unknown } } })?.response?.data?.error + return typeof message === 'string' && message.trim() ? message : fallback +}