From f10a0918a71602b7790d0a2173a72a8765f14d47 Mon Sep 17 00:00:00 2001
From: Jean Pierre Fouche
<261405437+jeanpierrefouche-ukhsa@users.noreply.github.com>
Date: Fri, 22 May 2026 19:15:06 +0100
Subject: [PATCH 1/4] task/CDD-3293-CMS-FIX-Dual-category-chart-component fix
secondary categories not updating the issue is with the selector for the
segments form - this was not finding the form
task/CDD-3293-CMS-FIX-Dual-category-chart-component
delete all segments on change
task/CDD-3293-CMS-FIX-Dual-category-chart-component
Fix observer - added appropriate class attribute,
fixed the matcher to use appropriate css call instead
of string.contains
---
.../static/js/dual_category_chart_form.js | 60 ++++++++++---------
cms/dynamic_content/components.py | 3 +
2 files changed, 36 insertions(+), 27 deletions(-)
diff --git a/cms/dashboard/static/js/dual_category_chart_form.js b/cms/dashboard/static/js/dual_category_chart_form.js
index 5be66baa8..4835b2d00 100644
--- a/cms/dashboard/static/js/dual_category_chart_form.js
+++ b/cms/dashboard/static/js/dual_category_chart_form.js
@@ -52,8 +52,8 @@ class FormStateManger {
this.secondary_field_value_state = Array.from(inputs).map(input => {
const selectedValues = [];
- for (let i = 0; i < input.options.length; i++) {
- const option = input.options[i];
+ for (const element of input.options) {
+ const option = element;
if (option.selected && option.value !== "") {
selectedValues.push(option.value);
}
@@ -71,9 +71,7 @@ class FormStateManger {
* @return {boolean} whether the value should be selected
*/
isSecondaryFieldOptionValueSelected(value, segmentIndex) {
- return this.secondary_field_value_state &&
- this.secondary_field_value_state[segmentIndex] &&
- this.secondary_field_value_state[segmentIndex].includes(value);
+ return this.secondary_field_value_state?.[segmentIndex]?.includes(value);
}
/**
@@ -83,17 +81,16 @@ class FormStateManger {
* @return {boolean} whether the value should be selected
*/
isPrimaryValueSelected(value) {
- return this.primary_field_values_state &&
- this.primary_field_values_state.includes(value)
+ return this.primary_field_values_state?.includes(value)
}
}
-class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blocks.StructBlockDefinition {
+class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField.blocks.StructBlockDefinition {
- static SELECTORS = {
- SEGMENTS_CONTAINER: '[class="dual-category-chart-card"] [data-streamfield-list-container]',
- SEGMENT_ITEM: 'dual-category-chart-card__segments',
+ static SELECTORS = {
+ SEGMENTS_CONTAINER: '.dual-category-chart-segments-container-form',
+ SEGMENT_ITEM: '.dual-category-chart-card__segments',
SECONDARY_FIELD_VALUE: '[id$="secondary_field_value"]'
};
@@ -173,7 +170,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
if (!this.geography_type_field) missingFields.push(FIELD_SUFFIXES.GEOGRAPHY);
if (!this.secondary_category_field) missingFields.push(FIELD_SUFFIXES.SECONDARY_CATEGORY);
- if(missingFields.length > 0) {
+ if (missingFields.length > 0) {
console.error(`Category form fields not found: ${missingFields.join(', ')}`, {
prefix,
x_axis_field: this.x_axis_field,
@@ -203,12 +200,11 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
const missingFields = [];
if (!this.primary_field_values_inputs) missingFields.push(FIELD_SUFFIXES.PRIMARY_VALUES);
if (!this.secondary_field_value_inputs) missingFields.push(FIELD_SUFFIXES.SECONDARY_VALUES);
-
if (missingFields.length > 0) {
console.error(`Sub-category form fields not found: ${missingFields.join(', ')}`, {
prefix,
- primary_field_values: this.primary_field_values_inputs,
- secondary_field_value: this.primary_field_values_inputs?.length || 0
+ primary_field_values: this.primary_field_values_inputs?.length || 0,
+ secondary_field_value: this.secondary_field_value_inputs?.length || 0,
});
}
}
@@ -219,25 +215,25 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
* and restore saved state for existing content
*/
initialiseFieldOptions() {
- if(!this.x_axis_field || !this.secondary_category_field || !this.secondary_category_choices) {
+ if (!this.x_axis_field || !this.secondary_category_field || !this.secondary_category_choices) {
return;
}
const { DEFAULT_OPTION } = DualCategoryChartCardBlockDefinition;
- if(this.x_axis_field && this.primary_field_values_inputs) {
+ if (this.x_axis_field && this.primary_field_values_inputs) {
this.updatePrimaryFieldValueOptions(this.x_axis_field.value);
}
- if(this.secondary_category_field && this.secondary_field_value_inputs) {
+ if (this.secondary_category_field && this.secondary_field_value_inputs) {
this.updateSecondaryFieldValueOptions(this.secondary_category_field.value);
}
- if(!this.x_axis_field.value && this.primary_field_values_inputs) {
+ if (!this.x_axis_field.value && this.primary_field_values_inputs) {
this.primary_field_values_inputs.innerHTML = ``;
}
- if(!this.secondary_category_field.value && this.secondary_field_value_inputs.length > 0) {
+ if (!this.secondary_category_field.value && this.secondary_field_value_inputs.length > 0) {
this.secondary_field_value_inputs.forEach(input => {
input.innerHTML = ``;
});
@@ -325,10 +321,10 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
* @param prefix
*/
observeSegmentBlocks(prefix) {
- const { SELECTORS } = DualCategoryChartCardBlockDefinition;
+ const { SELECTORS } = DualCategoryChartCardBlockDefinition;
const segments_container = document.querySelector(SELECTORS.SEGMENTS_CONTAINER)
- if(!segments_container) {
+ if (!segments_container) {
console.warn("Segments container not found - observer disabled");
return;
}
@@ -337,8 +333,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach(node => {
- if (node.nodeType === 1 && node.classList.contains(SELECTORS.SEGMENT_ITEM)) {
-
+ if (node.nodeType === 1 && node.matches(SELECTORS.SEGMENT_ITEM)) {
this.stateManager.storeCurrentSelections(this.secondary_field_value_inputs);
this.setupSubCategoryFormFields(prefix);
@@ -374,15 +369,25 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
this.secondary_category_field.addEventListener("change", (evt) => {
this.stateManager.clearSecondaryFieldState();
this.updateSecondaryFieldValueOptions(evt.target.value);
+
+ // clear all segments
+ document
+ .querySelectorAll('.dual-category-chart-card__segments')
+ .forEach(segments => {
+ const section = segments.closest('section[id$="-section"]');
+ section
+ ?.querySelector('[data-streamfield-action="DELETE"]')
+ ?.click();
+ });
});
this.geography_type_field.addEventListener("change", (evt) => {
- if(this.x_axis_field.value === FIELD_SUFFIXES.GEOGRAPHY_SUBCATEGORY_KEY) {
+ if (this.x_axis_field.value === FIELD_SUFFIXES.GEOGRAPHY_SUBCATEGORY_KEY) {
this.stateManager.clearPrimaryFieldState();
this.updatePrimaryFieldValueOptions(this.x_axis_field.value);
}
- if(this.secondary_category_field.value === FIELD_SUFFIXES.GEOGRAPHY_SUBCATEGORY_KEY) {
+ if (this.secondary_category_field.value === FIELD_SUFFIXES.GEOGRAPHY_SUBCATEGORY_KEY) {
this.stateManager.clearSecondaryFieldState();
this.updateSecondaryFieldValueOptions(this.secondary_category_field.value);
}
@@ -390,6 +395,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
}
render(placeholder, prefix, initialState, initialError) {
+
const block = super.render(placeholder, prefix, initialState, initialError);
// initialise form state
@@ -414,7 +420,7 @@ class DualCategoryChartCardBlockDefinition extends window.wagtailStreamField.blo
}
-window.telepath.register(
+globalThis.telepath.register(
'cms.dynamic_content.cards.DualCategoryChartCard',
DualCategoryChartCardBlockDefinition,
);
diff --git a/cms/dynamic_content/components.py b/cms/dynamic_content/components.py
index 55b26f576..6f8d7208b 100644
--- a/cms/dynamic_content/components.py
+++ b/cms/dynamic_content/components.py
@@ -69,6 +69,9 @@ class Meta:
class DualCategoryChartSegmentComponents(blocks.StreamBlock):
segment = DualCategoryChartSegmentComponent()
+ class Meta:
+ form_classname = "dual-category-chart-segments-container-form"
+
class HeadlineNumberComponent(elements.BaseMetricsElement):
body = blocks.TextBlock(required=False, help_text=help_texts.OPTIONAL_BODY_FIELD)
From d4f90a8ad9f71538ad25cf0a1a764e26b4b69e7a Mon Sep 17 00:00:00 2001
From: Jean Pierre Fouche
<261405437+jeanpierrefouche-ukhsa@users.noreply.github.com>
Date: Tue, 26 May 2026 23:04:09 +0100
Subject: [PATCH 2/4] task/CDD-3293-CMS-FIX-Dual-category-chart-component fix
multiple charts issue: - DualCategoryChartCardBlockDefinition is a singleton
and so the code needs to treat each chart as a new instance. - CSS queries
must be scoped to rootNode, this being the DOM root element on which each
chart is rendered.
---
.../static/js/dual_category_chart_form.js | 67 ++++++++++---------
1 file changed, 36 insertions(+), 31 deletions(-)
diff --git a/cms/dashboard/static/js/dual_category_chart_form.js b/cms/dashboard/static/js/dual_category_chart_form.js
index 4835b2d00..1cf5c9374 100644
--- a/cms/dashboard/static/js/dual_category_chart_form.js
+++ b/cms/dashboard/static/js/dual_category_chart_form.js
@@ -87,7 +87,15 @@ class FormStateManger {
class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField.blocks.StructBlockDefinition {
+ render(placeholder, prefix, initialState, initialError) {
+ const block = super.render(placeholder, prefix, initialState, initialError);
+ const chart = new DualCategoryChartCard()
+ chart.render(prefix, initialState, block)
+ return block;
+ }
+}
+class DualCategoryChartCard {
static SELECTORS = {
SEGMENTS_CONTAINER: '.dual-category-chart-segments-container-form',
SEGMENT_ITEM: '.dual-category-chart-card__segments',
@@ -132,7 +140,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* @param prefix - {string} html form prefix for element IDs
*/
setupDataScript(prefix) {
- const script_id = `${prefix}-${DualCategoryChartCardBlockDefinition.FIELD_SUFFIXES.DATA_SCRIPT}`;
+ const script_id = `${prefix}-${DualCategoryChartCard.FIELD_SUFFIXES.DATA_SCRIPT}`;
this.data_script = document.getElementById(script_id);
if (!this.data_script) {
@@ -158,12 +166,12 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* `primary_field_values` options with age related options.
* @param prefix - {string} html form prefix for element IDs
*/
- setupCategoryFormFields(prefix) {
- const { FIELD_SUFFIXES } = DualCategoryChartCardBlockDefinition;
+ setupCategoryFormFields(prefix, rootNode) {
+ const { FIELD_SUFFIXES } = DualCategoryChartCard;
- this.x_axis_field = document.getElementById(`${prefix}-${FIELD_SUFFIXES.X_AXIS}`);
- this.geography_type_field = document.getElementById(`${prefix}-${FIELD_SUFFIXES.GEOGRAPHY}`);
- this.secondary_category_field = document.getElementById(`${prefix}-${FIELD_SUFFIXES.SECONDARY_CATEGORY}`);
+ this.x_axis_field = rootNode.querySelector(`select#${prefix}-${FIELD_SUFFIXES.X_AXIS}`);
+ this.geography_type_field = rootNode.querySelector(`select#${prefix}-${FIELD_SUFFIXES.GEOGRAPHY}`);
+ this.secondary_category_field = rootNode.querySelector(`select#${prefix}-${FIELD_SUFFIXES.SECONDARY_CATEGORY}`);
const missingFields = [];
if (!this.x_axis_field) missingFields.push(FIELD_SUFFIXES.X_AXIS);
@@ -187,13 +195,13 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* to the category selected in `x_axis` field from our `primary form fields`
* @param prefix
*/
- setupSubCategoryFormFields(prefix) {
- const { FIELD_SUFFIXES, SELECTORS } = DualCategoryChartCardBlockDefinition;
+ setupSubCategoryFormFields(prefix, rootNode) {
+ const { FIELD_SUFFIXES, SELECTORS } = DualCategoryChartCard;
- this.primary_field_values_inputs = document.getElementById(
- `${prefix}-${FIELD_SUFFIXES.PRIMARY_VALUES}`
+ this.primary_field_values_inputs = rootNode.querySelector(
+ `select#${prefix}-${FIELD_SUFFIXES.PRIMARY_VALUES}`
);
- this.secondary_field_value_inputs = document.querySelectorAll(
+ this.secondary_field_value_inputs = rootNode.querySelectorAll(
`[id^="${prefix}-${FIELD_SUFFIXES.SEGMENTS}-"]${SELECTORS.SECONDARY_FIELD_VALUE}`
);
@@ -219,7 +227,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
return;
}
- const { DEFAULT_OPTION } = DualCategoryChartCardBlockDefinition;
+ const { DEFAULT_OPTION } = DualCategoryChartCard;
if (this.x_axis_field && this.primary_field_values_inputs) {
this.updatePrimaryFieldValueOptions(this.x_axis_field.value);
@@ -248,7 +256,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* @param subcategory_key {string} representing the sub category key (primary category)
*/
getFieldOptions(subcategory_key) {
- const { GEOGRAPHY_SUBCATEGORY_KEY } = DualCategoryChartCardBlockDefinition.FIELD_SUFFIXES
+ const { GEOGRAPHY_SUBCATEGORY_KEY } = DualCategoryChartCard.FIELD_SUFFIXES
if (subcategory_key && subcategory_key !== GEOGRAPHY_SUBCATEGORY_KEY) {
return this.secondary_category_choices[subcategory_key] || [];
@@ -267,7 +275,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* @param subcategory_key - {string} the subcategory key used to retrieve the primary field value options.
*/
updatePrimaryFieldValueOptions(subcategory_key) {
- const { DEFAULT_OPTION } = DualCategoryChartCardBlockDefinition;
+ const { DEFAULT_OPTION } = DualCategoryChartCard;
this.primary_field_values_inputs.innerHTML = ``;
@@ -292,7 +300,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* @param subcategory_key - {string} the subcategory key used to retrieve the primary field value options.
*/
updateSecondaryFieldValueOptions(subcategory_key) {
- const { DEFAULT_OPTION } = DualCategoryChartCardBlockDefinition;
+ const { DEFAULT_OPTION } = DualCategoryChartCard;
this.secondary_field_value_inputs.forEach((secondary_field_value, index) => {
secondary_field_value.innerHTML = ``;
@@ -320,9 +328,9 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* `SecondaryFormFields` and then update the options of these lists to ensure they align with the primary fields.
* @param prefix
*/
- observeSegmentBlocks(prefix) {
- const { SELECTORS } = DualCategoryChartCardBlockDefinition;
- const segments_container = document.querySelector(SELECTORS.SEGMENTS_CONTAINER)
+ observeSegmentBlocks(prefix, rootNode) {
+ const { SELECTORS } = DualCategoryChartCard;
+ const segments_container = rootNode.querySelector(SELECTORS.SEGMENTS_CONTAINER)
if (!segments_container) {
console.warn("Segments container not found - observer disabled");
@@ -336,7 +344,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
if (node.nodeType === 1 && node.matches(SELECTORS.SEGMENT_ITEM)) {
this.stateManager.storeCurrentSelections(this.secondary_field_value_inputs);
- this.setupSubCategoryFormFields(prefix);
+ this.setupSubCategoryFormFields(prefix, rootNode);
if (this.secondary_category_field.value) {
this.updateSecondaryFieldValueOptions(this.secondary_category_field.value);
}
@@ -358,8 +366,8 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
* @name setupEvents
* @description Setup event handlers for dynamic choice fields `secondary_category` and `x_axis_field`.
*/
- setupEvents() {
- const { FIELD_SUFFIXES } = DualCategoryChartCardBlockDefinition;
+ setupEvents(rootNode) {
+ const { FIELD_SUFFIXES } = DualCategoryChartCard;
this.x_axis_field.addEventListener("change", (evt) => {
this.stateManager.clearPrimaryFieldState();
@@ -371,7 +379,7 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
this.updateSecondaryFieldValueOptions(evt.target.value);
// clear all segments
- document
+ rootNode
.querySelectorAll('.dual-category-chart-card__segments')
.forEach(segments => {
const section = segments.closest('section[id$="-section"]');
@@ -394,9 +402,8 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
});
}
- render(placeholder, prefix, initialState, initialError) {
-
- const block = super.render(placeholder, prefix, initialState, initialError);
+ render(prefix, initialState, block) {
+ const rootNode = block.container[4]
// initialise form state
this.stateManager.initialise(initialState);
@@ -405,17 +412,15 @@ class DualCategoryChartCardBlockDefinition extends globalThis.wagtailStreamField
this.setupDataScript(prefix);
// setup primary and secondary form fields
- this.setupCategoryFormFields(prefix);
- this.setupSubCategoryFormFields(prefix);
+ this.setupCategoryFormFields(prefix, rootNode);
+ this.setupSubCategoryFormFields(prefix, rootNode);
// Initial population of field options based on current/initial values
this.initialiseFieldOptions();
// Setup observer for watching segments list and event handlers on
- this.observeSegmentBlocks(prefix);
- this.setupEvents();
-
- return block;
+ this.observeSegmentBlocks(prefix, rootNode);
+ this.setupEvents(rootNode);
}
}
From 811096361e32680b77ea6eecb8bebddda082bc9d Mon Sep 17 00:00:00 2001
From: Jean Pierre Fouche
<261405437+jeanpierrefouche-ukhsa@users.noreply.github.com>
Date: Tue, 26 May 2026 23:53:45 +0100
Subject: [PATCH 3/4] task/CDD-3293-CMS-FIX-Dual-category-chart-component
Changelog document
---
...3-CMS-FIX-Dual-category-chart-component.md | 41 +++++++++++++++++++
1 file changed, 41 insertions(+)
create mode 100644 changelog/CDD-3293-CMS-FIX-Dual-category-chart-component.md
diff --git a/changelog/CDD-3293-CMS-FIX-Dual-category-chart-component.md b/changelog/CDD-3293-CMS-FIX-Dual-category-chart-component.md
new file mode 100644
index 000000000..7bda0152f
--- /dev/null
+++ b/changelog/CDD-3293-CMS-FIX-Dual-category-chart-component.md
@@ -0,0 +1,41 @@
+# CDD-3293-CMS-FIX-Dual-category-chart-component
+
+# Author
+Jean-Pierre Fouche
+
+# Ticket
+[CDD-3293-CMS-FIX-Dual-category-chart-component](https://ukhsa.atlassian.net/browse/CDD-3293)
+
+# Date
+26 May 2026
+
+# Description
+
+This change fixes the issue described in the above ticket.
+
+Dual Category Charts are managed in the Wagtail CMS admin interface with two important fields:
+
+```mermaid
+graph TD
+xaxis["x-axis"] --> | links to | PrimaryFieldValues["Primary Field Values(single multi-select