From aacfd24b588129982ef3c96cd0cb99b976ae2d01 Mon Sep 17 00:00:00 2001 From: Yannis Biasutti Date: Mon, 6 Apr 2026 21:35:01 +0200 Subject: [PATCH] refactor(places): merge KML/KMZ routes into single POST /import/map endpoint --- client/src/api/client.ts | 8 +--- .../src/components/Planner/PlacesSidebar.tsx | 4 +- server/src/routes/places.ts | 37 +++---------------- server/src/services/placeService.ts | 7 ++++ server/tests/integration/places.test.ts | 12 +++--- 5 files changed, 21 insertions(+), 47 deletions(-) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index df74ae96..5dc4fb0d 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -105,13 +105,9 @@ export const placesApi = { const fd = new FormData(); fd.append('file', file) return apiClient.post(`/trips/${tripId}/places/import/gpx`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) }, - importKml: (tripId: number | string, file: File) => { + importMapFile: (tripId: number | string, file: File) => { const fd = new FormData(); fd.append('file', file) - return apiClient.post(`/trips/${tripId}/places/import/kml`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) - }, - importKmz: (tripId: number | string, file: File) => { - const fd = new FormData(); fd.append('file', file) - return apiClient.post(`/trips/${tripId}/places/import/kmz`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) + return apiClient.post(`/trips/${tripId}/places/import/map`, fd, { headers: { 'Content-Type': 'multipart/form-data' } }).then(r => r.data) }, importGoogleList: (tripId: number | string, url: string) => apiClient.post(`/trips/${tripId}/places/import/google-list`, { url }).then(r => r.data), diff --git a/client/src/components/Planner/PlacesSidebar.tsx b/client/src/components/Planner/PlacesSidebar.tsx index a7595940..74d9beff 100644 --- a/client/src/components/Planner/PlacesSidebar.tsx +++ b/client/src/components/Planner/PlacesSidebar.tsx @@ -111,9 +111,7 @@ const PlacesSidebar = React.memo(function PlacesSidebar({ setKmlKmzSummary(null) try { - const result = ext === 'kmz' - ? await placesApi.importKmz(tripId, kmlKmzFile) - : await placesApi.importKml(tripId, kmlKmzFile) + const result = await placesApi.importMapFile(tripId, kmlKmzFile) await loadTrip(tripId) setKmlKmzSummary(result.summary || null) diff --git a/server/src/routes/places.ts b/server/src/routes/places.ts index 010bff4f..732173fe 100644 --- a/server/src/routes/places.ts +++ b/server/src/routes/places.ts @@ -13,8 +13,7 @@ import { updatePlace, deletePlace, importGpx, - importKmlPlaces, - importKmzPlaces, + importMapFile, importGoogleList, searchPlaceImage, } from '../services/placeService'; @@ -74,7 +73,7 @@ router.post('/import/gpx', authenticate, requireTripAccess, uploadMulter.single( } }); -router.post('/import/kml', authenticate, requireTripAccess, uploadMulter.single('file'), (req: Request, res: Response) => { +router.post('/import/map', authenticate, requireTripAccess, uploadMulter.single('file'), async (req: Request, res: Response) => { const authReq = req as AuthRequest; if (!checkPermission('place_edit', authReq.user.role, authReq.trip!.user_id, authReq.user.id, authReq.trip!.user_id !== authReq.user.id)) { return res.status(403).json({ error: 'No permission' }); @@ -85,9 +84,9 @@ router.post('/import/kml', authenticate, requireTripAccess, uploadMulter.single( if (!file) return res.status(400).json({ error: 'No file uploaded' }); try { - const result = importKmlPlaces(tripId, file.buffer); + const result = await importMapFile(tripId, file.buffer, file.originalname); if (result.count === 0) { - return res.status(400).json({ error: 'No valid Placemarks found in KML file', summary: result.summary }); + return res.status(400).json({ error: 'No valid Placemarks found in map file', summary: result.summary }); } res.status(201).json(result); @@ -95,33 +94,7 @@ router.post('/import/kml', authenticate, requireTripAccess, uploadMulter.single( broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string); } } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to import KML file'; - res.status(400).json({ error: message }); - } -}); - -router.post('/import/kmz', authenticate, requireTripAccess, uploadMulter.single('file'), async (req: Request, res: Response) => { - const authReq = req as AuthRequest; - if (!checkPermission('place_edit', authReq.user.role, authReq.trip!.user_id, authReq.user.id, authReq.trip!.user_id !== authReq.user.id)) { - return res.status(403).json({ error: 'No permission' }); - } - - const { tripId } = req.params; - const file = (req as any).file; - if (!file) return res.status(400).json({ error: 'No file uploaded' }); - - try { - const result = await importKmzPlaces(tripId, file.buffer); - if (result.count === 0) { - return res.status(400).json({ error: 'No valid Placemarks found in KMZ file', summary: result.summary }); - } - - res.status(201).json(result); - for (const place of result.places) { - broadcast(tripId, 'place:created', { place }, req.headers['x-socket-id'] as string); - } - } catch (err: unknown) { - const message = err instanceof Error ? err.message : 'Failed to import KMZ file'; + const message = err instanceof Error ? err.message : 'Failed to import map file'; res.status(400).json({ error: message }); } }); diff --git a/server/src/services/placeService.ts b/server/src/services/placeService.ts index 9cc31b56..83cea291 100644 --- a/server/src/services/placeService.ts +++ b/server/src/services/placeService.ts @@ -411,6 +411,13 @@ export async function importKmzPlaces(tripId: string, kmzBuffer: Buffer): Promis return importKmlPlaces(tripId, kmlBuffer); } +export async function importMapFile(tripId: string, fileBuffer: Buffer, filename: string): Promise { + const ext = filename.toLowerCase().split('.').pop(); + if (ext === 'kmz') return importKmzPlaces(tripId, fileBuffer); + if (ext === 'kml') return importKmlPlaces(tripId, fileBuffer); + throw new Error(`Unsupported map file format: .${ext}. Please upload a .kml or .kmz file.`); +} + // --------------------------------------------------------------------------- // Import Google Maps list // --------------------------------------------------------------------------- diff --git a/server/tests/integration/places.test.ts b/server/tests/integration/places.test.ts index 00a3aff3..acea701c 100644 --- a/server/tests/integration/places.test.ts +++ b/server/tests/integration/places.test.ts @@ -546,7 +546,7 @@ describe('KML/KMZ Import', () => { .run('Museums', '#3b82f6', 'Landmark', user.id); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kml`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', KML_FIXTURE); @@ -572,7 +572,7 @@ describe('KML/KMZ Import', () => { .run('Parks', '#22c55e', 'Trees', user.id); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kml`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', KML_NESTED_FIXTURE); @@ -596,7 +596,7 @@ describe('KML/KMZ Import', () => { const trip = createTrip(testDb, user.id); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kml`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', KML_MALFORMED_FIXTURE); @@ -614,7 +614,7 @@ describe('KML/KMZ Import', () => { const nonUtf8Kml = Buffer.concat([prefix, invalidByte, suffix]); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kml`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', nonUtf8Kml, 'non-utf8.kml'); @@ -629,7 +629,7 @@ describe('KML/KMZ Import', () => { const trip = createTrip(testDb, user.id); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kmz`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', KMZ_FIXTURE); @@ -643,7 +643,7 @@ describe('KML/KMZ Import', () => { const trip = createTrip(testDb, user.id); const res = await request(app) - .post(`/api/trips/${trip.id}/places/import/kmz`) + .post(`/api/trips/${trip.id}/places/import/map`) .set('Cookie', authCookie(user.id)) .attach('file', Buffer.from('not-a-zip-archive'), 'invalid.kmz');