From c531f02dc85ddeeab499af4bcdbc426cfcbaa759 Mon Sep 17 00:00:00 2001 From: Paul Weerheim Date: Fri, 4 Sep 2026 15:09:24 +0200 Subject: [PATCH 1/3] Fix transparent dialog panel and cap modal size The theme mapped every colour token except --color-background, so the bg-background class of Dialog.Content produced nothing and the panel stayed transparent. Both containers also had no height cap, so a tall modal grew past the window and took its footer with it. They now stop at 90% of the window in both directions, and the content scrolls. Modal registered its Escape listener once, and only when canCloseModal was true at that moment. A later change never reached it. --- src/lib/components/modals/Modal.svelte | 24 ++++++++++++------- .../ui/dialog/dialog-content.svelte | 4 +++- src/tailwind.css | 2 ++ 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/lib/components/modals/Modal.svelte b/src/lib/components/modals/Modal.svelte index 32c7ca2..fd1692f 100644 --- a/src/lib/components/modals/Modal.svelte +++ b/src/lib/components/modals/Modal.svelte @@ -11,19 +11,17 @@ let shortRandomString = Utils.uuidv4().slice(0, 8); function closeModalOnEscape(event: KeyboardEvent) { - if (event.key === 'Escape') { + // The test reads the value of now. A caller can block the close while a + // task runs, and release it after. + if (event.key === 'Escape' && canCloseModal) { onClose(); } } onMount(() => { - if (canCloseModal) { - //also add escape key listener to close modal - document.addEventListener('keydown', closeModalOnEscape); - } + document.addEventListener('keydown', closeModalOnEscape); return () => { - // Cleanup: remove event listener if it was added document.removeEventListener('keydown', closeModalOnEscape); }; }); @@ -53,7 +51,7 @@ {/if} - diff --git a/src/lib/geo/spatial-selection.ts b/src/lib/geo/spatial-selection.ts index 1d696e5..7ca72c1 100644 --- a/src/lib/geo/spatial-selection.ts +++ b/src/lib/geo/spatial-selection.ts @@ -10,6 +10,7 @@ * columns. See `beacon-core/src/query/filter/geo_json.rs`. */ import type { GeoJsonFilter, GeoJsonPolygon, MinMaxFilter } from '@/beacon-api/types'; +import { detectCoordinateColumns } from '@/geo/coordinate-columns'; import { getSettings } from '@/stores/settings'; export type SpatialSelectionMode = 'polygon' | 'box' | 'cross-section'; @@ -25,6 +26,19 @@ export type SpatialSelection = { line?: LngLat[]; /** Cross section only: the full width of the band, in kilometres. */ widthKm?: number; + /** + * The columns the filter tests. The user picks them in the query builder. + * Both are absent on an area of an older record, and {@link selectionColumns} + * then falls back to {@link detectCoordinateColumns}. + */ + latitudeColumn?: string; + longitudeColumn?: string; +}; + +/** The pair of columns a spatial filter tests. */ +export type CoordinatePair = { + latitude: string; + longitude: string; }; export type Bounds = { @@ -223,6 +237,67 @@ export function makeCrossSectionSelection(line: LngLat[], widthKm: number): Spat }; } +/** Put the two column names on a selection. The draw tools drop them. */ +export function withColumns( + selection: SpatialSelection, + columns: CoordinatePair | null +): SpatialSelection { + if (!columns) return selection; + + return { + ...selection, + latitudeColumn: columns.latitude, + longitudeColumn: columns.longitude + }; +} + +/** + * The two columns a spatial filter must test, or null. + * + * The names on the selection win, because the user picked them. They only win + * while the query still selects both: a filter on a column that the query does + * not select is invalid. Detection then answers, which also serves an area of + * an older record, and an area that the map viewer drew. + */ +export function selectionColumns( + selection: SpatialSelection | null | undefined, + availableNames: string[] +): CoordinatePair | null { + const latitude = selection?.latitudeColumn; + const longitude = selection?.longitudeColumn; + + if ( + latitude && + longitude && + availableNames.includes(latitude) && + availableNames.includes(longitude) + ) { + return { latitude, longitude }; + } + + const detection = detectCoordinateColumns(availableNames); + if (!detection.latitude || !detection.longitude) return null; + + return { latitude: detection.latitude.name, longitude: detection.longitude.name }; +} + +/** + * The column of a selection that the query does not select, or null. + * + * The builder reports this. The area stays on the draft, so the user can pick + * the column again, or select it in the query. + */ +export function missingSelectionColumn( + selection: SpatialSelection | null | undefined, + availableNames: string[] +): string | null { + const wanted = [selection?.latitudeColumn, selection?.longitudeColumn].filter( + Boolean + ) as string[]; + + return wanted.find((name) => !availableNames.includes(name)) ?? null; +} + /** True when the selection has a usable area. */ export function isUsableSelection(selection: SpatialSelection | null | undefined): boolean { return !!selection && selection.ring.length >= 4; @@ -284,7 +359,9 @@ export function fromGeoJsonFilter(filter: GeoJsonFilter): SpatialSelection | nul return { mode: 'polygon', - ring: ring.map((point) => [Number(point[0]), Number(point[1])] as LngLat) + ring: ring.map((point) => [Number(point[0]), Number(point[1])] as LngLat), + latitudeColumn: filter.latitude_query_parameter, + longitudeColumn: filter.longitude_query_parameter }; } diff --git a/src/lib/query/draft.ts b/src/lib/query/draft.ts index ab22b15..9f79200 100644 --- a/src/lib/query/draft.ts +++ b/src/lib/query/draft.ts @@ -15,11 +15,11 @@ import { getSettings } from '@/stores/settings'; import { Utils } from '@/utils'; import { isUsableSelection, + selectionColumns, toBboxFilters, toGeoJsonFilter, type SpatialSelection } from '@/geo/spatial-selection'; -import { detectCoordinateColumns } from '@/geo/coordinate-columns'; /** A single selected column plus the filters applied to it. */ export type SelectedField = { @@ -102,20 +102,21 @@ export function compileDraft(draft: QueryDraft | null | undefined): CompiledQuer * The box is always derived here, and is never stored on a field. So one delete * of `spatialFilter` removes every part of the area again. * - * The query keeps no filter when it does not select both a latitude and a - * longitude column. + * The two columns come from the area itself, because the user picks them in the + * builder. See {@link selectionColumns}. The query keeps no filter while it + * selects neither pair. */ function addSpatialFilters(builder: QueryBuilder, draft: QueryDraft): void { const selection = draft.spatialFilter; if (!isUsableSelection(selection)) return; const names = draft.selectedFields.map((field) => field.name); - const { latitude, longitude } = detectCoordinateColumns(names); - if (!latitude || !longitude) return; + const columns = selectionColumns(selection, names); + if (!columns) return; - builder.addFilter(toGeoJsonFilter(selection!, latitude.name, longitude.name)); + builder.addFilter(toGeoJsonFilter(selection!, columns.latitude, columns.longitude)); - for (const filter of toBboxFilters(selection!, latitude.name, longitude.name)) { + for (const filter of toBboxFilters(selection!, columns.latitude, columns.longitude)) { builder.addFilter(filter); } } diff --git a/src/lib/query/seed-hydration.ts b/src/lib/query/seed-hydration.ts index 8f11328..8e0c2f4 100644 --- a/src/lib/query/seed-hydration.ts +++ b/src/lib/query/seed-hydration.ts @@ -139,16 +139,18 @@ function isDerivedBoxFilter( return false; } - const name = filter.for_query_parameter.toLowerCase(); + const name = filter.for_query_parameter; const matches = (min: number, max: number) => { return Math.abs(Number(filter.min) - min) < 1e-9 && Math.abs(Number(filter.max) - max) < 1e-9; }; - if (name.includes('latitude')) { + // The geo filter names its own two columns, so an area on `x` and `y` also + // matches. A name test for "latitude" holds only for the default pair. + if (name === selection.latitudeColumn) { return matches(bounds.minLat, bounds.maxLat); } - if (name.includes('longitude')) { + if (name === selection.longitudeColumn) { return matches(bounds.minLon, bounds.maxLon); } diff --git a/src/routes/visualisations/map-viewer/+page.svelte b/src/routes/visualisations/map-viewer/+page.svelte index 975ec16..b51be76 100644 --- a/src/routes/visualisations/map-viewer/+page.svelte +++ b/src/routes/visualisations/map-viewer/+page.svelte @@ -16,7 +16,11 @@ import VisualisationTabs from '@/components/visualisation/VisualisationTabs.svelte'; import MapDrawTools from '@/components/visualisation/MapDrawTools.svelte'; import { MapViewController } from '@/components/visualisation/MapViewController.svelte'; - import type { SpatialSelection } from '@/geo/spatial-selection'; + import { + selectionColumns, + withColumns, + type SpatialSelection + } from '@/geo/spatial-selection'; import { addToast } from '@/stores/toasts'; import { runBlockReason } from '@/query/query-guard'; import { settings } from '@/stores/settings'; @@ -174,9 +178,23 @@ untrack(() => workspace.updateActiveMapView(state)); }); - /** Write the drawn area into the query. The effect above then re-runs it. */ + /** + * Write the drawn area into the query. The effect above then re-runs it. + * + * The area names the two columns it tests. The builder can pick another pair + * than the detection finds, for example `x` and `y`. A redraw here must keep + * that pair, so the columns come from the area of the block. + */ function applyAreaFilter() { - workspace.updateActiveSpatialFilter(selection); + if (!selection) { + workspace.updateActiveSpatialFilter(null); + return; + } + + const stored = workspace.activeBlock?.draft?.spatialFilter ?? selection; + const columns = selectionColumns(stored, map.availableColumnNames); + + workspace.updateActiveSpatialFilter(withColumns(selection, columns)); } From ff59224ec99dc573acd36e52664bb1050e619de1 Mon Sep 17 00:00:00 2001 From: Jasper-Maris <87017907+Jasper-Maris@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:40:57 +0200 Subject: [PATCH 3/3] Fix indentation for MapDrawTools component Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../query-builder/GeospatialFilterModal.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/components/query-builder/GeospatialFilterModal.svelte b/src/lib/components/query-builder/GeospatialFilterModal.svelte index 97c17f7..98895dc 100644 --- a/src/lib/components/query-builder/GeospatialFilterModal.svelte +++ b/src/lib/components/query-builder/GeospatialFilterModal.svelte @@ -204,11 +204,11 @@
(isDrawing = drawing)} - /> + {map} + bind:selection={draft} + showApply={false} + onDrawingChange={(drawing) => (isDrawing = drawing)} + />