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 +}