feat(import): selective GPX/KML element import and performance improvements

Add type-selector UI in the file import modal letting users choose which
GPX elements (waypoints, routes, tracks) or KML/KMZ elements (points,
paths) to import. KML LineString placemarks are now imported as path
places with route_geometry.

Performance improvements:
- Extract MemoPlaceRow with React.memo and contentVisibility:auto to cut
  unnecessary re-renders in PlacesSidebar
- Add weatherQueue to cap concurrent weather fetches at 3
- Replace sequential per-place deletes with a single bulkDelete API call
  (new DELETE /places/bulk endpoint + deletePlacesMany service)
- Memoize atlas/photo/weather service calls to avoid redundant requests
- Add multi-select mode to PlacesSidebar for bulk operations

Add large GPX/KML/KMZ fixtures for integration/perf testing and two
profiler analysis scripts under scripts/.
This commit is contained in:
jubnl
2026-04-18 01:28:37 +02:00
parent 9a31fcac7b
commit 6a718fccea
45 changed files with 22471 additions and 285 deletions
+25
View File
@@ -12,6 +12,7 @@ export interface PlacesSlice {
addPlace: (tripId: number | string, placeData: Partial<Place>) => Promise<Place>
updatePlace: (tripId: number | string, placeId: number, placeData: Partial<Place>) => Promise<Place>
deletePlace: (tripId: number | string, placeId: number) => Promise<void>
deletePlacesMany: (tripId: number | string, placeIds: number[]) => Promise<void>
}
export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice => ({
@@ -80,4 +81,28 @@ export const createPlacesSlice = (set: SetState, get: GetState): PlacesSlice =>
throw new Error(getApiErrorMessage(err, 'Error deleting place'))
}
},
deletePlacesMany: async (tripId, placeIds) => {
if (placeIds.length === 0) return
try {
await placeRepo.deleteMany(tripId, placeIds)
const idSet = new Set(placeIds)
set(state => {
const updatedAssignments = { ...state.assignments }
let changed = false
for (const [dayId, items] of Object.entries(state.assignments)) {
if (items.some((a: Assignment) => a.place?.id != null && idSet.has(a.place.id))) {
updatedAssignments[dayId] = items.filter((a: Assignment) => !idSet.has(a.place?.id!))
changed = true
}
}
return {
places: state.places.filter(p => !idSet.has(p.id)),
...(changed ? { assignments: updatedAssignments } : {}),
}
})
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error deleting places'))
}
},
})