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' } } -] +])