mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user