diff --git a/apps/journal/app/lib/activities.server.ts b/apps/journal/app/lib/activities.server.ts index 4058326..2848c61 100644 --- a/apps/journal/app/lib/activities.server.ts +++ b/apps/journal/app/lib/activities.server.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { eq, desc, and, isNotNull, sql } from "drizzle-orm"; +import { eq, desc, and, isNotNull, inArray, sql } from "drizzle-orm"; import { unionAll } from "drizzle-orm/pg-core"; import { getDb } from "./db.ts"; import { activities, routes, syncImports, users, follows, remoteActors } from "@trails-cool/db/schema/journal"; @@ -376,12 +376,17 @@ async function getSimplifiedActivityGeojsonBatch(ids: string[]): Promise) { + // Use the query builder for the id list: a raw `ANY(${ids}::text[])` + // makes drizzle expand the array to `($1,$2,...)`, yielding the invalid + // `ANY((...)::text[])` — which throws and silently drops every preview. + const rows = await db + .select({ + id: activities.id, + geojson: sql`ST_AsGeoJSON(ST_Simplify(${activities.geom}, 0.001))`, + }) + .from(activities) + .where(and(inArray(activities.id, ids), isNotNull(activities.geom))); + for (const row of rows) { if (row.geojson) map.set(row.id, row.geojson); } } catch { diff --git a/apps/journal/app/lib/routes.server.ts b/apps/journal/app/lib/routes.server.ts index 91ed3f3..406113e 100644 --- a/apps/journal/app/lib/routes.server.ts +++ b/apps/journal/app/lib/routes.server.ts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { eq, desc, and } from "drizzle-orm"; +import { eq, desc, and, inArray, isNotNull } from "drizzle-orm"; import { getDb } from "./db.ts"; import { routes, routeVersions } from "@trails-cool/db/schema/journal"; import type { Visibility } from "@trails-cool/db/schema/journal"; @@ -209,12 +209,17 @@ async function getSimplifiedGeojsonBatch(ids: string[]): Promise) { + // Query-builder id list: a raw `ANY(${ids}::text[])` makes drizzle expand + // the array to `($1,$2,...)`, yielding the invalid `ANY((...)::text[])` + // — it throws and silently drops every route preview. + const rows = await db + .select({ + id: routes.id, + geojson: sql`ST_AsGeoJSON(ST_Simplify(${routes.geom}, 0.001))`, + }) + .from(routes) + .where(and(inArray(routes.id, ids), isNotNull(routes.geom))); + for (const row of rows) { if (row.geojson) map.set(row.id, row.geojson); } } catch { diff --git a/e2e/list-map-previews.test.ts b/e2e/list-map-previews.test.ts new file mode 100644 index 0000000..254cd4e --- /dev/null +++ b/e2e/list-map-previews.test.ts @@ -0,0 +1,30 @@ +import { test, expect, gotoHydrated } from "./fixtures/test"; +import { setupVirtualAuthenticator, registerUser } from "./helpers/auth"; + +// A GPX fixture with geometry → the activity gets a PostGIS geom, so its list +// card should render a Leaflet map preview. Guards the batch-geojson query +// (a malformed `ANY(${ids}::text[])` previously threw and was swallowed, +// silently dropping every list preview). +const GPX = "packages/fit/__fixtures__/alpine.gpx"; + +test.describe("List map previews", () => { + test("activities list renders a map preview for an activity with geometry", async ({ page }) => { + const cdp = await page.context().newCDPSession(page); + await setupVirtualAuthenticator(cdp); + + const stamp = Date.now(); + await registerUser(page, `lmp-${stamp}@example.com`, `lmp${stamp}`); + + await gotoHydrated(page, "/activities/new"); + await page.getByLabel("Name").fill("Preview check"); + await page.locator('input[name="gpx"]').setInputFiles(GPX); + await page.getByRole("button", { name: "Create Activity" }).click(); + await expect(page).toHaveURL(/\/activities\/[0-9a-f-]+$/, { timeout: 10000 }); + + await gotoHydrated(page, "/activities"); + // The Leaflet container mounts after the client-only dynamic import; no + // "No map preview" fallback should remain. + await expect(page.locator(".leaflet-container").first()).toBeVisible({ timeout: 15000 }); + await expect(page.getByText("No map preview")).toHaveCount(0); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index ba4a5ad..14dd779 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -130,6 +130,14 @@ export default defineConfig({ baseURL: "http://localhost:3000", }, }, + { + name: "list-map-previews", + testMatch: "list-map-previews.test.ts", + use: { + ...devices["Desktop Chrome"], + baseURL: "http://localhost:3000", + }, + }, ], webServer: process.env.CI ? [