From 15d0ef35583489348907b055ead6a464d3f4217b Mon Sep 17 00:00:00 2001 From: Doxa Maps Date: Sat, 3 Oct 2026 06:54:24 +0000 Subject: [PATCH] Gospel Resources: Show all writes through the one history writer, only on a page it owns, and its check runs in gates:pre --- internal/check-view-address.mjs | 1 - package.json | 2 +- plugin-designs/gospel-resources/data-tables.vue | 10 ++++++---- plugin-designs/gospel-resources/filter-url.js | 16 ++++++++-------- .../scripts/check-clear-filter-url.mjs | 17 +++++++++++------ 5 files changed, 26 insertions(+), 20 deletions(-) diff --git a/internal/check-view-address.mjs b/internal/check-view-address.mjs index 96a44083e..88620d496 100644 --- a/internal/check-view-address.mjs +++ b/internal/check-view-address.mjs @@ -26,7 +26,6 @@ const KNOWN = { 'library/utils/languageRequest.js': [1, '?lang= on a generated page only (MAP_OWNS_PAGE); keeps state and every other param'], 'library/layouts/app-wrapper-material/index.html': [1, "the layout page's own #/ hash; a fragment-only URL keeps the query"], 'plugin-designs/gospel-internet/src/useBoard.js': [1, 'its own keys on its own leaves (writeUrl); keeps state, foreign params and the hash'], - 'plugin-designs/gospel-resources/data-tables.vue': [1, "clearFilter: replaceState({}, '', pathname) wipes the whole query and hash; reported to its owner"], 'plugin-designs/regions-dashboard/region-dashboards.vue': [2, '# on its own leaf (URL_SYNC); P2 moves it to `place`'], } // SOURCE files only: gates and node scripts may simulate history. A write is `.pushState(` / `.replaceState(` (an aliased diff --git a/package.json b/package.json index f62845bd6..43d6287d6 100644 --- a/package.json +++ b/package.json @@ -360,7 +360,7 @@ "fix:sri": "node internal/fix-sri.mjs", "gallery": "node internal/generate-gallery.mjs", "gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:script-flags check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight check:no-fleet-names:self-test check:pin-hint-floor:self-test check:poster-sizes:self-test check:shell-header:self-test check:preview-flag-dev-only:self-test check:poster-type-registry:self-test check:host-floor:self-test check:phone-print-keep:self-test check:pinned-fetch-fresh:self-test", - "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:circular-layout check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:upg100-zone-copy check:upg100-zone-copy:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep check:pinned-fetch-fresh check:view-address check:view-address:self-test", + "gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:pins-in-state check:fork-leaders check:circular-layout check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-internal-role-nouns check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:side-lanes check:side-lanes:self-test check:poster-section check:poster-section:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sections check:poster-sections:self-test check:poster-design-catalogue check:poster-design-catalogue:self-test check:upg100-zone-copy check:upg100-zone-copy:self-test check:wall-photo-weight check:no-fleet-names check:pin-hint-floor check:poster-sizes check:upg-count check:shell-header check:pg-name-gate check:preview-flag-dev-only check:poster-type-registry check:host-floor check:phone-print-keep check:pinned-fetch-fresh check:view-address check:view-address:self-test check:gospel-clear-filter-url", "gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs", "internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs", "library-index": "node internal/generate-library-index.mjs", diff --git a/plugin-designs/gospel-resources/data-tables.vue b/plugin-designs/gospel-resources/data-tables.vue index eb4d139ce..e92878bbe 100644 --- a/plugin-designs/gospel-resources/data-tables.vue +++ b/plugin-designs/gospel-resources/data-tables.vue @@ -27,7 +27,8 @@ import { EN_MESSAGES } from '@map/i18n/index.js' import DataTable from '@map/components/tables/DataTable.vue' import { useShadowStyles } from '@map/composables/useShadowStyles.js' import { resolveAssetsBase } from '@map/utils/embedBase.js' -import { withoutKeys, FILTER_KEYS } from './filter-url.js' +import { commitUrl, urlOwnerOf } from '@map/composables/useViewAddress.js' +import { searchWithout, FILTER_KEYS } from './filter-url.js' const profileConfig = inject('profileConfig', ref(null)) @@ -149,11 +150,12 @@ const fLabel = ref((params.get('label') || '').trim()) const fProvince = ref((params.get('province') || '').trim()) const fold = (v) => String(v || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase().trim() const hasFilter = computed(() => !!(fIso.value || fCountry.value || fProvince.value)) -// "Show all" drops only the filter's own keys (filter-url.js): the tab, a host's or partner's keys, -// the hash and the history state stay as they were. +// "Show all" drops only the filter's own keys (filter-url.js) through the one history writer, and only on a page +// this element owns (urlOwner 'app'); a partner's or a host's address is never written. The tab, every other key, +// the hash and the history state stay. function clearFilter() { fIso.value = ''; fCountry.value = ''; fLabel.value = ''; fProvince.value = '' - try { window.history.replaceState(window.history.state, '', withoutKeys(window.location.href, FILTER_KEYS)) } catch (_) {} + if (urlOwnerOf(profileConfig.value) === 'app') commitUrl({ search: searchWithout(window.location.search, FILTER_KEYS) }) } const active = ref(TABS.some((t) => t.id === params.get('tab')) ? params.get('tab') : 'links') diff --git a/plugin-designs/gospel-resources/filter-url.js b/plugin-designs/gospel-resources/filter-url.js index 38500d3c8..3e78e4ffb 100644 --- a/plugin-designs/gospel-resources/filter-url.js +++ b/plugin-designs/gospel-resources/filter-url.js @@ -1,16 +1,16 @@ /** - * filter-url.js — the address after this page drops its OWN filter keys. + * filter-url.js — the query after this page drops its OWN filter keys. * * The tables page reads a deep-link filter from the query (?iso= / ?country= / ?label= / ?province=). * "Show all" must remove exactly those keys: the page's ?tab=, any key a host or a partner put on the - * address, and the #hash are someone else's and stay as they were. Pure (no DOM), so a node check can - * prove it without a browser. + * address, and the #hash are someone else's and stay as they were. The result is in commitUrl's `search` + * shape ('' or '?…'), and every other parameter is kept as written, byte for byte. Pure (no DOM), so a node + * check can prove it without a browser. */ export const FILTER_KEYS = ['iso', 'country', 'label', 'province'] -/** `href` without `keys`, as pathname + search + hash (what history.replaceState takes). */ -export function withoutKeys(href, keys = FILTER_KEYS) { - const u = new URL(href) - for (const k of keys) u.searchParams.delete(k) - return u.pathname + u.search + u.hash +/** `search` ('' or '?…') without the parameters named in `keys`, in the same shape. */ +export function searchWithout(search, keys = FILTER_KEYS) { + const kept = String(search || '').replace(/^\?/, '').split('&').filter((p) => p && !keys.includes(p.split('=')[0])) + return kept.length ? '?' + kept.join('&') : '' } diff --git a/plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs b/plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs index 2bd117479..426df8022 100644 --- a/plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs +++ b/plugin-designs/gospel-resources/scripts/check-clear-filter-url.mjs @@ -8,18 +8,20 @@ * alone. * * Proves it two ways: the pure helper (filter-url.js) on real-shaped addresses, and the component — - * data-tables.vue's clearFilter calls that helper and keeps history.state, and the old wipe is gone. + * data-tables.vue's clearFilter writes through commitUrl (the one history writer, which keeps the hash and the + * state) only on a page it owns, with no direct history write left. * `--poison` runs the old behaviour through the same assertions and requires them to FAIL. * Exit 0 pass · 1 fail. */ import { readFileSync } from 'node:fs' import { resolve, dirname } from 'node:path' import { fileURLToPath } from 'node:url' -import { withoutKeys, FILTER_KEYS } from '../filter-url.js' +import { searchWithout, FILTER_KEYS } from '../filter-url.js' const HERE = dirname(fileURLToPath(import.meta.url)) const POISON = process.argv.includes('--poison') -const strip = POISON ? (href) => new URL(href).pathname : (href) => withoutKeys(href, FILTER_KEYS) +// The address after "Show all", as commitUrl writes it ({ search } given, the current hash kept). Poison: the old wipe. +const strip = POISON ? (href) => new URL(href).pathname : (href) => { const u = new URL(href); return u.pathname + searchWithout(u.search, FILTER_KEYS) + u.hash } let fails = 0 const ok = (c, m) => { if (c) console.log('ok ' + m); else { fails++; console.log('FAIL ' + m) } } @@ -34,13 +36,16 @@ const b = strip(`${base}?iso=hin`) ok(b === new URL(base).pathname, `only filter keys: no dangling "?" (${b})`) const c = strip(`${base}?tab=countries`) ok(c === new URL(base).pathname + '?tab=countries', 'no filter: the address is unchanged') +const d = strip(`${base}?tab=links&q=a%20b+c&iso=hin`) +ok(d.endsWith('?tab=links&q=a%20b+c'), `other parameters are kept as written, byte for byte (${d})`) if (!POISON) { const vue = readFileSync(resolve(HERE, '../data-tables.vue'), 'utf8') const fn = (vue.match(/function clearFilter\(\)\s*\{[\s\S]*?\n\}/) || [''])[0] - ok(/withoutKeys\(window\.location\.href,\s*FILTER_KEYS\)/.test(fn), 'data-tables.vue clearFilter uses the helper') - ok(/replaceState\(window\.history\.state,/.test(fn), 'and keeps history.state') - ok(!/replaceState\(\{\},\s*'',\s*window\.location\.pathname\)/.test(vue), 'the old whole-address wipe is gone') + ok(/import \{[^}]*\bcommitUrl\b[^}]*\burlOwnerOf\b[^}]*\} from '@map\/composables\/useViewAddress\.js'/.test(vue), 'data-tables.vue imports the one history writer (commitUrl) and urlOwnerOf') + ok(/commitUrl\(\{\s*search:\s*searchWithout\(window\.location\.search,\s*FILTER_KEYS\)\s*\}\)/.test(fn), 'clearFilter writes through commitUrl with only the filter keys dropped (the hash and state kept by commitUrl)') + ok(/urlOwnerOf\(profileConfig\.value\)\s*===\s*'app'/.test(fn), 'and only on a page it owns (urlOwner app): a partner\'s address is never written') + ok(!/\.(push|replace)State\s*\(/.test(vue), 'no direct history write is left in data-tables.vue') } if (POISON) { console.log(fails ? `POISON OK: ${fails} caught` : 'POISON FAILED'); process.exit(fails ? 0 : 1) }