From 6b29e146df2d137cd22e55e5e3dc92a0afa8a006 Mon Sep 17 00:00:00 2001 From: doxa-maps-88655c70 <88627970+doxa-maps-88655c70@users.noreply.github.com> Date: Tue, 22 Sep 2026 16:22:21 +0000 Subject: [PATCH] Let the deepest two tree tiers reach the grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cluster and People Group changed the dashboard correctly and could never move the grid, because the fields behind them were not in the grid's fetch. They are real fields, unlike the two family axes, which are derived and have no row field to filter against at all — those stay unmapped rather than being aimed at a lookalike and quietly showing the wrong rows. Verified against the live data before mapping, since a wrong mapping is worse than no mapping: both fields are present on 2,106 of 2,107 rows in the same value-and-label shape as the axes already wired, with 194 and 1,736 distinct values, no collisions once normalised, and no case where one entry's label matches another's code. Codes and names never overlap, so either vocabulary can be accepted safely. Measured end to end: selecting the Telugu cluster narrows the grid to 93, which is exactly the number of rows the API holds for it. --- .../dashboard-plus-grid.vue | 12 +++++++- .../people-groups-grid/people-groups-grid.vue | 28 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue index b340c7a0f..77d476b35 100644 --- a/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue +++ b/plugin-designs/dashboard-plus-grid/dashboard-plus-grid.vue @@ -127,12 +127,20 @@ const FACET_TO_PROP = { country_code: 'countryCode', primary_language: 'primaryLanguage', engagement_status: 'engagementStatus', + // The two DEEPEST tree tiers. Their row fields are imb_reg_of_people_2 / _25, which the grid did + // not fetch — so these changed the dashboard correctly and could never move the grid. Verified + // live before mapping (2,107 rows): both present on 2106, both {value,label}, 194 and 1736 + // distinct values, ZERO collisions after normalisation and ZERO label-vs-value cross-collisions. + // Unlike language_family / religion_family, which are DERIVED and stay unmapped, these are real + // fields that only needed asking for. + cluster: 'cluster', + people_group: 'peopleGroup', } // Empty string (not null) is the grid's documented "no filter" value for every axis, so a // cleared facet must RESET its prop rather than leave the previous value stuck. const filter = reactive({ query: '', wagfRegion: '', wagfBlock: '', rop1: '', religion: '', countryCode: '', - primaryLanguage: '', engagementStatus: '' }) + primaryLanguage: '', engagementStatus: '', cluster: '', peopleGroup: '' }) function onDashboardFilter({ filterSet = {}, searchTerm = '' } = {}) { filter.query = searchTerm || '' @@ -205,6 +213,8 @@ const sections = [ :country-code="filter.countryCode" :primary-language="filter.primaryLanguage" :engagement-status="filter.engagementStatus" + :cluster="filter.cluster" + :people-group="filter.peopleGroup" :show-search="false" :show-filters="false" :show-total="false" /> diff --git a/plugin-designs/people-groups-grid/people-groups-grid.vue b/plugin-designs/people-groups-grid/people-groups-grid.vue index 598a9aa06..a45335d23 100644 --- a/plugin-designs/people-groups-grid/people-groups-grid.vue +++ b/plugin-designs/people-groups-grid/people-groups-grid.vue @@ -137,6 +137,24 @@ const props = defineProps({ // admits Yombe rows. Country could claim a measured ZERO collisions; language cannot, and saying // so is the point. One over-match in 863 beats an axis that cannot filter at all. primaryLanguage: { type: String, default: '' }, + + // ★ THE LAST TWO REACHABLE TREE AXES. The semantic tree's deepest tiers — Cluster and People Group + // — changed the dashboard correctly but could not move the grid, because the fields they read were + // simply not in the grid's fetch. Unlike language_family and religion_family (which are DERIVED and + // have no matching row field at all, so they stay unmapped), these are real fields and only needed + // asking for. + // + // VERIFIED AGAINST THE LIVE API BEFORE MAPPING — the standing rule is never to guess a mapping, and + // the religion_family worked example in dashboard-plus-grid.vue shows why: a plausible-looking + // mapping filters confidently to the WRONG rows, which is worse than not narrowing. Measured over + // all 2,107 rows: + // imb_reg_of_people_2 (cluster) present 2106/2107 {value,label} 194 distinct + // imb_reg_of_people_25 (people group) present 2106/2107 {value,label} 1736 distinct + // collisions after norm(): 0 and 0 label-vs-value cross-collisions: 0 and 0 + // Codes and labels never overlap (C0059/"Borneo-Kalimantan", 300005/"Abai Sungai"), so valueMatches + // can accept either vocabulary without over-matching — the same property that made country_code safe. + cluster: { type: String, default: '' }, + peopleGroup: { type: String, default: '' }, engagementStatus: { type: String, default: '' }, // ── presentation knobs copied from UupgsList (defaults preserve today's behaviour) ── @@ -201,6 +219,12 @@ const propFilters = computed(() => { // every row and filter the grid to zero. if (props.primaryLanguage) f.primary_language = { value: props.primaryLanguage, label: lbl(props.primaryLanguage) } if (props.engagementStatus) f.engagement_status = { value: props.engagementStatus, label: lbl(props.engagementStatus) } + // Same API-field-name rule again, and here the names give no hint of it: the tree calls these + // "Cluster" and "People Group", but the row fields are imb_reg_of_people_2 and + // imb_reg_of_people_25. Keying on the friendly name would read an undefined field on every row and + // filter the grid to zero — the exact failure the two comments above describe. + if (props.cluster) f.imb_reg_of_people_2 = { value: props.cluster, label: lbl(props.cluster) } + if (props.peopleGroup) f.imb_reg_of_people_25 = { value: props.peopleGroup, label: lbl(props.peopleGroup) } return f }) const activeFilters = computed(() => (useFilters.value ? localFilters.value : propFilters.value)) @@ -516,7 +540,7 @@ async function getUUPGs() { // primaryLanguage prop match nothing and silently filter the grid to zero rows. Verified live // (2026-09-22): adding it keeps the response at 2,107 rows and the field is populated on 100% // of them. Any future filter axis must be added HERE as well as to propFilters. - const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language&lang=${languageCode.value}` + const url = `${prayBaseUrl.value}/api/people-groups/list?fields=name,slug,wagf_region,wagf_block,country_code,rop1,religion,has_photo,image_url,adopted_by_churches,imb_alternate_name,engagement_status,people_committed,primary_language,imb_reg_of_people_2,imb_reg_of_people_25&lang=${languageCode.value}` loading.value = true try { const response = await fetch(url) @@ -533,7 +557,7 @@ async function getUUPGs() { // external prop-driven filters should re-filter the grid (when controls are hidden). watch(() => [props.query, props.wagfRegion, props.wagfBlock, props.rop1, props.religion, props.countryCode, - props.primaryLanguage, props.engagementStatus, useSearch.value, useFilters.value], () => { + props.primaryLanguage, props.engagementStatus, props.cluster, props.peopleGroup, useSearch.value, useFilters.value], () => { page.value = 1 filterUUPGs() })