From f98375f4cd6cd1aef6a3f09bbf7516370d4c26a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 31 Jul 2026 14:12:07 +0200 Subject: [PATCH 1/2] Overlays behind map markers --- .../v2/destinations_index_controller.js | 6 ++-- app/javascript/maplibre/raster_layers.js | 21 +++++++----- .../javascript/maplibre_vector_style_test.mjs | 32 ++++++++++++++++--- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/app/javascript/controllers/v2/destinations_index_controller.js b/app/javascript/controllers/v2/destinations_index_controller.js index 2ddedc936..0720fbb61 100644 --- a/app/javascript/controllers/v2/destinations_index_controller.js +++ b/app/javascript/controllers/v2/destinations_index_controller.js @@ -9,7 +9,7 @@ import { pickLayers, resolveMapStyle, styleForBaseLayer, basesNeedStyleSwitch, a import { GeocoderIControl } from 'maplibre/geocoder_control' import { OverlayLayersToggleIControl } from 'maplibre/overlay_layers_toggle_control' import { DeclusterViewportIControl } from 'maplibre/decluster_viewport_control' -import { DestinationsMapLayers } from 'maplibre/destinations_map_layers' +import { DestinationsMapLayers, CLUSTER_LAYER_ID } from 'maplibre/destinations_map_layers' const DEFAULT_ZOOM = 12 const MIN_CHARS = 3 @@ -571,7 +571,7 @@ export default class extends Controller { overlayLayers, this._overlayVisibility, this._overlayToggles, - { includeRaster: resolved.mode === 'vector' } + { includeRaster: resolved.mode === 'vector', beforeId: CLUSTER_LAYER_ID } ).catch(() => { /* overlay style fetch failed */ }) } if (map.isStyleLoaded()) runOverlays() @@ -655,7 +655,7 @@ export default class extends Controller { this._mapOverlayLayers || [], this._overlayVisibility, this._overlayToggles, - { includeRaster: true } + { includeRaster: true, beforeId: CLUSTER_LAYER_ID } ).catch(() => { /* overlay style fetch failed */ }) this._mapLayers = new DestinationsMapLayers(this._map, this._destinationsMapLayersOptions) // Keep current camera; fitBounds on reconnect fights setStyle and can drop the viewport fetch. diff --git a/app/javascript/maplibre/raster_layers.js b/app/javascript/maplibre/raster_layers.js index e91e335e2..c89d3175f 100644 --- a/app/javascript/maplibre/raster_layers.js +++ b/app/javascript/maplibre/raster_layers.js @@ -165,7 +165,7 @@ function resolveStyleResourceUrl (styleUrl, resource) { * Skips background layers and layers authored with visibility:"none" * @returns {string[]} layer ids added for this overlay (toggle-controlled) */ -export function mergeVectorOverlayStyle (map, style, overlayIndex, visible, styleUrl = null) { +export function mergeVectorOverlayStyle (map, style, overlayIndex, visible, styleUrl = null, beforeId = null) { const prefix = 'overlay-' + overlayIndex + '-' const layerIds = [] const layers = (style.layers || []).filter((layer) => { @@ -190,6 +190,7 @@ export function mergeVectorOverlayStyle (map, style, overlayIndex, visible, styl if (raw.url) raw.url = resolveStyleResourceUrl(styleUrl, raw.url) map.addSource(sid, raw) }) + const insertBefore = beforeId && map.getLayer(beforeId) ? beforeId : undefined layers.forEach((layer) => { const lid = prefix + layer.id layerIds.push(lid) @@ -205,7 +206,7 @@ export function mergeVectorOverlayStyle (map, style, overlayIndex, visible, styl next.layout.visibility = visible ? 'visible' : 'none' if (layer.source) next.source = prefix + layer.source try { - map.addLayer(next) + map.addLayer(next, insertBefore) } catch (e) { // Skip layers that the current map glyphs/sprite cannot render. } @@ -213,7 +214,7 @@ export function mergeVectorOverlayStyle (map, style, overlayIndex, visible, styl return layerIds } -function applySingleRasterOverlay (map, overlay, index, visible) { +function applySingleRasterOverlay (map, overlay, index, visible, beforeId = null) { const sid = 'overlay-' + index const layerId = sid + '-layer' if (!map.getSource(sid)) { @@ -224,6 +225,7 @@ function applySingleRasterOverlay (map, overlay, index, visible) { attribution: overlay.attribution || '' }) } + const insertBefore = beforeId && map.getLayer(beforeId) ? beforeId : undefined if (!map.getLayer(layerId)) { map.addLayer({ id: layerId, @@ -231,7 +233,7 @@ function applySingleRasterOverlay (map, overlay, index, visible) { source: sid, layout: { visibility: visible ? 'visible' : 'none' }, paint: { 'raster-opacity': 0.75 } - }) + }, insertBefore) } else { map.setLayoutProperty(layerId, 'visibility', visible ? 'visible' : 'none') } @@ -246,11 +248,14 @@ function applySingleRasterOverlay (map, overlay, index, visible) { * @param {object[]} overlays from pickLayers * @param {Record} visibilityByGroupId keyed by overlay-N * @param {object[]} [overlayToggles] - * @param {{ includeRaster?: boolean }} [options] includeRaster=false skips raster overlays already baked in composite style + * @param {{ includeRaster?: boolean, beforeId?: string }} [options] + * includeRaster=false skips raster overlays already baked in composite style + * beforeId inserts overlay layers below an existing map layer (e.g. markers) */ export async function applyOverlays (map, overlays, visibilityByGroupId = {}, overlayToggles = null, options = {}) { if (!map || !overlays || !overlays.length) return [] const includeRaster = options.includeRaster !== false + const beforeId = options.beforeId || null const toggles = [] for (let j = 0; j < overlays.length; j++) { const o = overlays[j] @@ -262,13 +267,13 @@ export async function applyOverlays (map, overlays, visibilityByGroupId = {}, ov if (o.vectorStyleUrl) { try { const style = await fetchStyleJson(o.vectorStyleUrl) - layerIds = mergeVectorOverlayStyle(map, style, j, visible, o.vectorStyleUrl) + layerIds = mergeVectorOverlayStyle(map, style, j, visible, o.vectorStyleUrl, beforeId) } catch (e) { // Vector overlays are never baked into the composite style — always allow raster fallback. - if (o.url) layerIds = applySingleRasterOverlay(map, o, j, visible) + if (o.url) layerIds = applySingleRasterOverlay(map, o, j, visible, beforeId) } } else if (includeRaster && o.url) { - layerIds = applySingleRasterOverlay(map, o, j, visible) + layerIds = applySingleRasterOverlay(map, o, j, visible, beforeId) } else if (!o.vectorStyleUrl && o.url) { // Raster already in composite style — keep toggle metadata only. layerIds = [groupId + '-layer'] diff --git a/test/javascript/maplibre_vector_style_test.mjs b/test/javascript/maplibre_vector_style_test.mjs index 049278617..22123ca15 100644 --- a/test/javascript/maplibre_vector_style_test.mjs +++ b/test/javascript/maplibre_vector_style_test.mjs @@ -67,7 +67,7 @@ function buildRasterStyle (mapLayers) { return { style: { version: 8, sources, layers }, overlayToggles } } -function mergeVectorOverlayStyle (map, style, overlayIndex, visible) { +function mergeVectorOverlayStyle (map, style, overlayIndex, visible, beforeId = null) { const prefix = 'overlay-' + overlayIndex + '-' const layerIds = [] const layers = (style.layers || []).filter((layer) => { @@ -84,6 +84,7 @@ function mergeVectorOverlayStyle (map, style, overlayIndex, visible) { const sid = prefix + id if (!map.getSource(sid)) map.addSource(sid, { ...style.sources[id] }) }) + const insertBefore = beforeId && map.getLayer(beforeId) ? beforeId : undefined layers.forEach((layer) => { const lid = prefix + layer.id layerIds.push(lid) @@ -93,7 +94,7 @@ function mergeVectorOverlayStyle (map, style, overlayIndex, visible) { id: lid, source: layer.source ? prefix + layer.source : undefined, layout: { ...(layer.layout || {}), visibility: visible ? 'visible' : 'none' } - }) + }, insertBefore) } }) return layerIds @@ -102,11 +103,17 @@ function mergeVectorOverlayStyle (map, style, overlayIndex, visible) { function fakeMap () { const sources = new Map() const layers = new Map() + const order = [] return { getSource: (id) => sources.get(id), addSource: (id, src) => { sources.set(id, src) }, getLayer: (id) => layers.get(id), - addLayer: (layer) => { layers.set(layer.id, layer) }, + addLayer: (layer, beforeId) => { + layers.set(layer.id, layer) + const idx = beforeId ? order.indexOf(beforeId) : -1 + if (idx >= 0) order.splice(idx, 0, layer.id) + else order.push(layer.id) + }, setLayoutProperty: (id, key, value) => { const layer = layers.get(id) if (layer) { @@ -115,7 +122,8 @@ function fakeMap () { } }, _sources: sources, - _layers: layers + _layers: layers, + _order: order } } @@ -234,6 +242,22 @@ describe('overlay layers prefer vector_style_url', () => { assert.ok(map.getLayer('overlay-0-highway')) }) + it('inserts overlay layers before markers when beforeId exists', () => { + const map = fakeMap() + map.addLayer({ id: 'destinations-v2-clusters', type: 'circle' }) + mergeVectorOverlayStyle(map, { + version: 8, + sources: { + restrictions: { type: 'vector', url: 'https://maps.example.com/tiles.json' } + }, + layers: [ + { id: 'highway', type: 'line', source: 'restrictions', 'source-layer': 'highway' } + ] + }, 0, true, 'destinations-v2-clusters') + + assert.deepEqual(map._order, ['overlay-0-highway', 'destinations-v2-clusters']) + }) + it('pickLayers exposes overlay vectorStyleUrl for applyOverlays', () => { const { overlays } = pickLayers({ Truck: { From 21364165d05a5d4a260bed0e0c441b61287f8d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gw=C3=A9na=C3=ABl=20Rault?= Date: Fri, 31 Jul 2026 14:32:26 +0200 Subject: [PATCH 2/2] Add extra dashboard to v2 layout --- app/views/v2/layouts/application.html.haml | 7 +++++++ test/controllers/destinations_controller_test.rb | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/views/v2/layouts/application.html.haml b/app/views/v2/layouts/application.html.haml index 9ed696b95..92fc14412 100644 --- a/app/views/v2/layouts/application.html.haml +++ b/app/views/v2/layouts/application.html.haml @@ -214,6 +214,13 @@ %i.fa.fa-chart-line.fa-fw .hidden-menu = t 'customers.menu.analytics' + - if @reseller.extra_dashboard_url && current_user.customer + %li.menu-section.menu-list-item + = link_to @reseller.extra_dashboard_url.gsub('{LG}', I18n.locale.to_s).gsub('{ID}', current_user.customer.id.to_s), target: '_blank' do + .d-flex.align-items-center.justify-content-center + %i.fa.fa-chart-column.fa-fw + .hidden-menu + = t 'customers.menu.extra_analytics' - if current_user.customer && @reseller.subscription_url && (current_user.customer.test? || (current_user.customer.end_subscription && current_user.customer.end_subscription < Time.now)) %li#menu-subscription.subscription.menu-section.menu-list-item diff --git a/test/controllers/destinations_controller_test.rb b/test/controllers/destinations_controller_test.rb index bad56bb95..6ad0c1265 100644 --- a/test/controllers/destinations_controller_test.rb +++ b/test/controllers/destinations_controller_test.rb @@ -170,6 +170,16 @@ def around assert_select 'button.floating-btn.xl-floating-button.destinations-position-drag-cancel.d-none', 1 end + test 'v2 layout menu-left includes extra dashboard when configured' do + customer = users(:user_one).customer + @reseller.update!(extra_dashboard_url: 'https://extra.example.com/{LG}/{ID}') + + get :index + assert_response :success + expected = "https://extra.example.com/#{I18n.locale}/#{customer.id}" + assert_select ".menu-left a[href='#{expected}'][target='_blank']", text: I18n.t('customers.menu.extra_analytics') + end + test 'v2 index includes vector_style_url in map layers config as-is' do layer = users(:user_one).layer style_url = 'https://maps.example.com/styles/custom/style.json'