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.
This commit is contained in:
Maurice
2026-05-31 22:57:39 +02:00
parent c37ee2c6c3
commit 959d6c3714
4 changed files with 19 additions and 6 deletions
@@ -225,13 +225,16 @@ describe('PlaceFormModal', () => {
expect(screen.getByDisplayValue('48.8584')).toBeInTheDocument(); 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(); const addToast = vi.fn();
window.__addToast = addToast; window.__addToast = addToast;
const user = userEvent.setup(); 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( 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(<PlaceFormModal {...defaultProps} />); render(<PlaceFormModal {...defaultProps} />);
@@ -241,7 +244,7 @@ describe('PlaceFormModal', () => {
await waitFor(() => { await waitFor(() => {
expect(addToast).toHaveBeenCalledWith( expect(addToast).toHaveBeenCalledWith(
expect.stringMatching(/search failed/i), expect.stringMatching(/Places API \(New\) has not been used/i),
'error', 'error',
undefined, undefined,
); );
@@ -10,6 +10,7 @@ import { Search, Paperclip, X, AlertTriangle, Loader2 } from 'lucide-react'
import { useTranslation } from '../../i18n' import { useTranslation } from '../../i18n'
import CustomTimePicker from '../shared/CustomTimePicker' import CustomTimePicker from '../shared/CustomTimePicker'
import { DEFAULT_FORM, isGoogleMapsUrl, type PlaceFormData } from './PlaceFormModal.helpers' import { DEFAULT_FORM, isGoogleMapsUrl, type PlaceFormData } from './PlaceFormModal.helpers'
import { getApiErrorMessage } from '../../utils/apiError'
import type { Place, Category, Assignment } from '../../types' import type { Place, Category, Assignment } from '../../types'
// The submit payload mirrors the form, but lat/lng are parsed to numbers and // 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) const result = await mapsApi.search(mapsSearch, language)
setMapsResults(result.places || []) setMapsResults(result.places || [])
} catch (err: unknown) { } catch (err: unknown) {
toast.error(t('places.mapsSearchError')) toast.error(getApiErrorMessage(err, t('places.mapsSearchError')))
} finally { } finally {
setIsSearchingMaps(false) setIsSearchingMaps(false)
} }
@@ -228,7 +229,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
} catch (err) { } catch (err) {
console.error('Failed to fetch place details:', err) console.error('Failed to fetch place details:', err)
setMapsSearch(previousSearch) setMapsSearch(previousSearch)
toast.error(t('places.mapsSearchError')) toast.error(getApiErrorMessage(err, t('places.mapsSearchError')))
} finally { } finally {
setIsSearchingMaps(false) setIsSearchingMaps(false)
} }
+1 -1
View File
@@ -622,7 +622,7 @@ export function useAtlas() {
try { try {
const result = await mapsApi.search(bucketSearch, language) const result = await mapsApi.search(bucketSearch, language)
setBucketSearchResults(result.places || []) setBucketSearchResults(result.places || [])
} catch {} finally { setBucketSearching(false) } } catch (err) { console.error('Bucket-list place search failed:', err) } finally { setBucketSearching(false) }
} }
const handleSelectBucketPoi = (result: any) => { const handleSelectBucketPoi = (result: any) => {
+9
View File
@@ -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
}