From dc252bb3048ddf2f5ab6675a6791a35271223c52 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Thu, 24 Sep 2026 19:00:24 +0000 Subject: [PATCH] Composites: give a partner's embed the token it passes in MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A JS embed of either composite mounted a map that could not draw. MEASURED the way a partner actually embeds — one origin serving the tree with CORS, a SEPARATE origin serving the page: research-map-plus-grid "No Mapbox access token provided", style 0, tiles 0, no canvas dashboard-plus-grid the same (iframe embeds were fine throughout: style 2, tiles 9, 0 failures) Both composites declared NO PROPS, so the host's profile-config — token included — was read by nobody. Our own leaves hide this completely: a leaf carries a BAKED token and assigns window.MAP_TOKEN, which the shared-token bridge hands to the nested map. The failure therefore exists ONLY where there is no leaf, which is precisely the partner page the embed snippet is written for. It is also the snippet we publish: profile-config='{"tk":"YOUR_MAPBOX_TOKEN"}' is the documented way to supply a key, and it did nothing. Both now parse their host config, publish the token page-wide through the shared composable, and hand it to their children. The two differ in how children are mounted, so the last step differs: research-map-plus-grid composes by TAG + BUNDLE URL, so the token is threaded into the child element's profile-config, where its own ProfileLoader reads it. dashboard-plus-grid bakes its children in and bypasses ProfileLoader entirely — and ProfileLoader is the only provider of `mapboxToken` in the repo, so DashboardMapPanel's inject fell through to its empty default. It now provides `mapboxToken` itself, from the host's tk with the shared page token behind it. Spread last and only when present, so a host that supplies none leaves the key absent and the child asks for exactly what its standalone leaf asks for. A malformed attribute is caught and ignored: the map half is more useful token-less than the page is with a thrown setup(). This went in the PLUGINS, not the library. A shared change reaches every map — that is the lesson SHOW_EXPORT_BUTTON taught this week, and nothing here needs to be shared to work. MEASURED AFTER, same harness: research-map-plus-grid canvas 1280x906, style 2, tiles 9, 0 failures; dashboard-plus-grid canvas present, style 2, tiles 10, 0 failures — basemap, pins, legend, table and charts all drawn. Screenshots read. Both LEAVES re-checked for regression: canvas 1, 0 failed requests, each still painting its own map. --- .../dashboard-plus-grid.vue | 43 ++++++++++++- .../research-map-plus-grid.vue | 41 ++++++++++++- .../dashboard-plus-grid.js | 60 +++++++++---------- .../dashboard-plus-grid/default/index.html | 2 +- .../dashboard-plus-grid/index.html | 2 +- public/doxa-maps/index.html | 2 +- public/doxa-maps/manifest.json | 8 +-- .../research-map-plus-grid.js | 16 ++--- .../research-map-plus-grid/default/index.html | 2 +- .../research-map-plus-grid/index.html | 2 +- 10 files changed, 126 insertions(+), 52 deletions(-) diff --git a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue index c7dc608c4..2466d7183 100644 --- a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue +++ b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue @@ -13,8 +13,9 @@ * Same "import the REAL components, no fork" discipline as big-picture: the grid is the exact * people-groups-grid component; the dashboard is the exact research-dashboard component. */ -import { reactive, provide, ref } from 'vue' +import { reactive, provide, ref, computed } from 'vue' import { resolveEmbedBase } from '@map/utils/embedBase.js' +import { useSharedMapToken, publishMapToken } from '@map/composables/useSharedMapToken.js' import StackLayout from '@map/layouts/StackLayout.vue' import ResearchDashboard from '../research-dashboard/research-dashboard.vue' import PeopleGroupsGrid from '../people-groups-grid/people-groups-grid.vue' @@ -217,13 +218,49 @@ function onDashboardFilter({ filterSet = {}, searchTerm = '' } = {}) { // Note this is exactly the two-host defect class: research-dashboard standalone PASSES the same // gate (1/1, verified this tick), and the identical component inside this composite FAILED. Only a // gate that drives BOTH hosts can tell those apart, which is why the composite is now a target. +// ★ THE HOST'S OWN profile-config, WHICH THIS COMPOSITE USED TO THROW AWAY. +// It declared no props, so a partner's lost its +// token entirely. MEASURED on a third-party origin before this: 'No Mapbox access token provided', +// 0 style and 0 tile requests to api.mapbox.com, no canvas — the JS embed, one of the two shipped +// embed methods, mounted a dashboard whose map could not draw. Our own leaf hides it, because that +// page carries a BAKED token; the failure exists only where there is no leaf, which is exactly the +// partner case. Same defect and same shape as research-map-plus-grid. +const props = defineProps({ + /** The host's JSON config, exactly as the embed snippet writes it. */ + profileConfig: { type: [String, Object], default: null }, +}) +const hostConfig = computed(() => { + const raw = props.profileConfig + if (!raw) return {} + if (typeof raw === 'object') return raw + // A malformed attribute must not take the element down with it. + try { return JSON.parse(raw) } catch { return {} } +}) +const hostToken = computed(() => { + const tk = hostConfig.value?.tk + return typeof tk === 'string' && tk ? tk : '' +}) +if (hostToken.value) publishMapToken(hostToken.value) + +// ★ AND PROVIDE IT THE WAY ProfileLoader WOULD. This composite bakes its children in rather than +// mounting them through ProfileLoader, and ProfileLoader is the ONLY provider of 'mapboxToken' in +// the repo — so DashboardMapPanel's inject('mapboxToken', ref('')) fell through to its empty +// default here. The shared page token keeps a late-arriving token working (the leaf FETCHES one), +// which is what the composable exists for; the host's own tk still wins. +const sharedToken = useSharedMapToken() +const mapboxToken = computed(() => hostToken.value || sharedToken.value || '') +provide('mapboxToken', mapboxToken) + const SHARE_LEAF = 'dashboard-plus-grid/dashboard-plus-grid/index.html' const shareBase = resolveEmbedBase(null) -provide('profileConfig', ref({ +provide('profileConfig', computed(() => ({ profile: 'research-dashboard', layout: 'page-scroll', shareUrl: (shareBase ? shareBase.replace(/\/+$/, '') + '/' : '') + SHARE_LEAF, -})) + // Spread last so a host that supplies none leaves the key absent, and never overwrite the two + // settings above — those are this composite's own layout and share decisions, not the host's. + ...(hostToken.value ? { tk: hostToken.value } : {}), +}))) const sections = [ // The research-dashboard leaf SELF-TITLES (its own DashboardTopBar renders a "Research Dashboard" diff --git a/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue b/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue index a40e793e5..63fb1842e 100644 --- a/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue +++ b/plugin-designs/research-map-plus-grid/research-map-plus-grid.vue @@ -55,8 +55,40 @@ * ON via prop. This is why the swap-inside-one-element idea was genuinely complex: the two top * elements do not merely look different, they imply OPPOSITE control ownership below. */ +import { computed } from 'vue' import StackLayout from '@map/layouts/StackLayout.vue' import { resolveEmbedBase } from '@map/utils/embedBase.js' +import { publishMapToken } from '@map/composables/useSharedMapToken.js' + +// ★ THE HOST'S OWN profile-config, WHICH THIS COMPOSITE USED TO THROW AWAY. +// It declared no props at all, so a partner's +// had its token dropped on the floor. On our own generated leaf that was invisible: the leaf page +// carries a BAKED token, assigns window.MAP_TOKEN, and the shared-token bridge hands it to the +// nested map. A partner page has no leaf and no baked token, so the JS embed — one of the two +// shipped embed methods — mounted a map that could not draw. MEASURED on a third-party origin +// before this: 'No Mapbox access token provided', 0 style and 0 tile requests, no canvas. +const props = defineProps({ + /** The host's JSON config, exactly as the embed snippet writes it. */ + profileConfig: { type: [String, Object], default: null }, +}) + +const hostConfig = computed(() => { + const raw = props.profileConfig + if (!raw) return {} + if (typeof raw === 'object') return raw + // A malformed attribute must not take the whole element down with it — the map half is more + // useful token-less than the page is with a thrown setup(). + try { return JSON.parse(raw) } catch { return {} } +}) + +// Publish page-wide as well as threading it below. Threading covers THIS child deterministically; +// publishing covers any element on the page that mounted before the attribute was read, which is +// the shape the shared-token composable exists for. +const hostToken = computed(() => { + const tk = hostConfig.value?.tk + return typeof tk === 'string' && tk ? tk : '' +}) +if (hostToken.value) publishMapToken(hostToken.value) // The absolute url this composite is shared and iframed as. Built here rather than written as a // literal because resolveEmbedBase() walks profileConfig.embedBaseUrl → window.MAP_EMBED_BASE_URL → @@ -103,7 +135,7 @@ const MAP_H = '88svh' const bundleSrc = (bundle, file) => import.meta.env.DEV ? `/plugin-designs/${bundle}/index.js` : `../../${bundle}/${file}.js` -const sections = [ +const sections = computed(() => [ // No section title: the research map self-titles through its own chrome, so a StackLayout title // here would render the name twice — the duplicate-header bug the Driver rejected on the desktop // sibling (BUG-RESEARCH-DASHBOARD-HEADER-STACK-WRONG). One header per element. @@ -155,6 +187,11 @@ const sections = [ // reader is scanning, a fly-tour that moves the camera underneath fights the other half. // The standalone leaf keeps both; this is a per-host decision, which is why it is config. legendTools: false, + // Thread the host's token through to the child. Spread so a host that supplies none + // leaves the key ABSENT rather than empty: ProfileLoader's ladder is `config.tk || + // sharedToken`, and an empty string would be falsy anyway, but an absent key keeps the + // child's own config identical to what its standalone leaf sets. + ...(hostToken.value ? { tk: hostToken.value } : {}), assetsBase: '../../assets', shareUrl: SHARE_URL }) @@ -176,7 +213,7 @@ const sections = [ src: bundleSrc('people-groups-grid', 'people-groups-grid'), attrs: { 'show-search': 'true', 'show-filters': 'true', 'show-total': 'true' } } -] +])