fix(collections): white screen when editing a place (undefined in places) (#1081)

updatePlace wrote `res.place` into the places list, but the endpoint returns the
updated place directly (not wrapped in { place }, unlike savePlace) — so an
`undefined` slipped into the list and the category-filter's presentCategories()
crashed on `undefined.category_id`, blanking the whole page. The WS echo used to
mask it by refetching; excluding the editor's own socket exposed it.

- Read the updated place directly and guard against a falsy response.
- Fix the api return types to match (updatePlace/setStatus return the place).
- Harden filterPlaces / statusCounts / presentCategories / mappablePlaces against
  a stray undefined entry so a single bad row can never white-screen the page.
This commit is contained in:
Maurice
2026-07-01 14:52:28 +02:00
committed by Maurice
parent ef3f043e00
commit 3198fd35eb
3 changed files with 10 additions and 8 deletions
+2 -2
View File
@@ -62,9 +62,9 @@ export const collectionsApi = {
ax.post(`${base}/places`, body satisfies CollectionSavePlaceRequest).then((r: AxiosResponse) => r.data),
saveFromTrip: (body: CollectionSaveFromTripRequest): Promise<CollectionSaveResult> =>
ax.post(`${base}/places/from-trip`, body satisfies CollectionSaveFromTripRequest).then((r: AxiosResponse) => r.data),
updatePlace: (pid: number, body: CollectionPlaceUpdateRequest): Promise<{ place: CollectionPlace }> =>
updatePlace: (pid: number, body: CollectionPlaceUpdateRequest): Promise<CollectionPlace> =>
ax.patch(`${base}/places/${pid}`, body satisfies CollectionPlaceUpdateRequest).then((r: AxiosResponse) => r.data),
setStatus: (pid: number, status: CollectionStatus): Promise<{ place: CollectionPlace }> =>
setStatus: (pid: number, status: CollectionStatus): Promise<CollectionPlace> =>
ax.post(`${base}/places/${pid}/status`, { status }).then((r: AxiosResponse) => r.data),
deletePlace: (pid: number): Promise<unknown> =>
ax.delete(`${base}/places/${pid}`).then((r: AxiosResponse) => r.data),
@@ -53,6 +53,7 @@ export function filterPlaces(
): CollectionPlace[] {
const q = search.trim().toLowerCase()
return places.filter(p => {
if (!p) return false
if (statusFilter !== 'all' && p.status !== statusFilter) return false
if (categoryFilter !== 'all' && (p.category_id ?? null) !== categoryFilter) return false
if (!q) return true
@@ -66,8 +67,8 @@ export function filterPlaces(
/** Count places per status for the filter chips. */
export function statusCounts(places: CollectionPlace[]): Record<StatusFilter, number> {
const counts: Record<StatusFilter, number> = { all: places.length, idea: 0, want: 0, visited: 0 }
for (const p of places) counts[p.status] += 1
const counts: Record<StatusFilter, number> = { all: 0, idea: 0, want: 0, visited: 0 }
for (const p of places) { if (!p) continue; counts.all += 1; counts[p.status] += 1 }
return counts
}
@@ -77,7 +78,7 @@ export interface CategoryOption { id: number; name: string; color: string | null
export function presentCategories(places: CollectionPlace[]): CategoryOption[] {
const byId = new Map<number, CategoryOption>()
for (const p of places) {
if (p.category_id == null || !p.category) continue
if (!p || p.category_id == null || !p.category) continue
const existing = byId.get(p.category_id)
if (existing) existing.count += 1
else byId.set(p.category_id, { id: p.category_id, name: p.category.name, color: p.category.color ?? null, icon: p.category.icon ?? null, count: 1 })
@@ -87,7 +88,7 @@ export function presentCategories(places: CollectionPlace[]): CategoryOption[] {
/** Only the places that can render on a map. */
export function mappablePlaces(places: CollectionPlace[]): CollectionPlace[] {
return places.filter(p => typeof p.lat === 'number' && typeof p.lng === 'number')
return places.filter(p => p && typeof p.lat === 'number' && typeof p.lng === 'number')
}
/**
+3 -2
View File
@@ -203,8 +203,9 @@ export const useCollectionStore = create<CollectionState>((set, get) => ({
},
updatePlace: async (placeId, body) => {
const res = await collectionsApi.updatePlace(placeId, body)
set({ places: get().places.map(p => (p.id === placeId ? res.place : p)) })
// The endpoint returns the updated place directly (not wrapped in { place }).
const updated = await collectionsApi.updatePlace(placeId, body)
if (updated) set({ places: get().places.map(p => (p.id === placeId ? updated : p)) })
},
deletePlace: async (placeId: number) => {