From ca03f69a5edb02773a242fa10f3c9b9da4708043 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Wed, 17 Dec 2025 13:44:25 +0530 Subject: [PATCH 01/16] add new formats --- corehq/apps/app_manager/detail_screen.py | 20 +++++++++++++++++++ .../static/app_manager/js/details/utils.js | 12 +++++++++++ 2 files changed, 32 insertions(+) diff --git a/corehq/apps/app_manager/detail_screen.py b/corehq/apps/app_manager/detail_screen.py index fddbe010d567..ab10fe8f45af 100644 --- a/corehq/apps/app_manager/detail_screen.py +++ b/corehq/apps/app_manager/detail_screen.py @@ -608,6 +608,26 @@ class AddressPopup(HideShortColumn): template_form = 'address-popup' +@register_format_type('geo-boundary') +class GeoBoundary(FormattedDetailColumn): + template_form = 'geo-boundary' + + +@register_format_type('geo-boundary-color') +class GeoBoundaryColor(FormattedDetailColumn): + template_form = 'geo-boundary-color' + + +@register_format_type('geo-points') +class GeoPoints(FormattedDetailColumn): + template_form = 'geo-points' + + +@register_format_type('geo-points-colors') +class GeoPointsColors(FormattedDetailColumn): + template_form = 'geo-points-colors' + + @register_format_type('picture') class Picture(FormattedDetailColumn): template_form = 'image' diff --git a/corehq/apps/app_manager/static/app_manager/js/details/utils.js b/corehq/apps/app_manager/static/app_manager/js/details/utils.js index c358d8ea5379..e669771cadb9 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/utils.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/utils.js @@ -45,6 +45,18 @@ module.getFieldFormats = function () { }, { value: "markdown", label: gettext('Markdown'), + },{ + value: "geo-boundary", + label: gettext('Geo Boundary'), + }, { + value: "geo-boundary-color", + label: gettext('Geo Boundary Color'), + }, { + value: "geo-points", + label: gettext('Geo Points'), + }, { + value: "geo-points-colors", + label: gettext('Geo Points Colors'), }]; if (toggles.toggleEnabled('CASE_LIST_MAP')) { From 2e1cf99a67d3a389b2dc65c82528884ed54da54c Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 14:27:04 +0530 Subject: [PATCH 02/16] add validation for to restrict to one property for new formats --- .../static/app_manager/js/details/screen.js | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 6d6c55d775d8..47738a61d1c6 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -509,7 +509,12 @@ export default function (spec, config, options) { // Only save if property names are valid var errors = [], containsTab = false, - imageColumnCount = 0; + imageColumnCount = 0, + geoBoundaryCount = 0, + geoBoundaryColorCount = 0, + geoPointsCount = 0, + geoPointsColorsCount = 0; + _.each(self.columns(), function (column) { column.saveAttempted(true); if (column.isTab) { @@ -521,12 +526,32 @@ export default function (spec, config, options) { errors.push(gettext("There is an error in your property name: ") + column.field.value); } else if (column.format.value === 'image') { imageColumnCount += 1; + } else if (column.format.value === 'geo-boundary') { + geoBoundaryCount += 1; + } else if (column.format.value === 'geo-boundary-color') { + geoBoundaryColorCount += 1; + } else if (column.format.value === 'geo-points') { + geoPointsCount += 1; + } else if (column.format.value === 'geo-points-colors') { + geoPointsColorsCount += 1; } }); if (imageColumnCount > 1) { errors.push(gettext("You can only have one property with the 'Image' format")); } + if (geoBoundaryCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Boundary' format")); + } + if (geoBoundaryColorCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Boundary Color' format")); + } + if (geoPointsCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Points' format")); + } + if (geoPointsColorsCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Points Colors' format")); + } if (containsTab) { if (!self.columns()[0].isTab) { errors.push(gettext("All properties must be below a tab.")); From 090a749a33823d8c1a8949c31fb7a711ed18c585 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 14:41:35 +0530 Subject: [PATCH 03/16] add function to calculate dynamic formats to include if dependencies are present --- .../static/app_manager/js/details/screen.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 47738a61d1c6..33f90a874e09 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -354,6 +354,33 @@ export default function (spec, config, options) { self.fire('change'); }; + // Key = format name, Value = array of required dependency formats (all must be present) + const COLUMN_FORMAT_DEPENDENCIES = { + 'geo-boundary': ['address'], + 'geo-points': ['address'], + 'geo-boundary-color': ['address', 'geo-boundary'], + 'geo-points-colors': ['address', 'geo-points'], + }; + const columnsHasFormat = function (formatName) { + return _.some(self.columns(), function(col) { + return col.format && col.format.val && col.format.val() === formatName; + }); + }; + const areAllDependenciesPresent = function (dependencies) { + return _.every(dependencies, function(dep) { + return columnsHasFormat(dep); + }); + }; + const calculateDynamicFormatsToInclude = function () { + const formatsToInclude = []; + _.each(COLUMN_FORMAT_DEPENDENCIES, function(dependencies, formatName) { + if (areAllDependenciesPresent(dependencies)) { + formatsToInclude.push(formatName); + } + }); + return formatsToInclude; + }; + self.initColumnAsColumn = function (column) { column.model.setEdit(false); column.field.setEdit(true); From 5c40c2446a15e72b65b295974322a58d0cd285d7 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 14:50:50 +0530 Subject: [PATCH 04/16] fix: correctly remove translatable enum fixes the issue where current logic simply removes the last element which may or may not be translatable enum --- .../apps/app_manager/static/app_manager/js/details/column.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index 0895accd2d7b..0e51fcb08e17 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -327,7 +327,10 @@ export default function (col, screen) { } } else { // Restrict Translatable Text usage to Calculated Properties only - menuOptions.splice(-1); + const index = menuOptions.findIndex(f => f.value.includes('translatable-enum')); + if (index !== -1) { + menuOptions.splice(index, 1); + } } self.format = uiElementSelect.new(menuOptions).val(self.original.format || null); From 93432839f09d5cfd933e4f6b21867c6fb586e825 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 15:35:53 +0530 Subject: [PATCH 05/16] function to add dynamic formats to a column --- .../static/app_manager/js/details/column.js | 77 +++++++++++++------ 1 file changed, 53 insertions(+), 24 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index 0e51fcb08e17..6bc805b511d8 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -304,36 +304,65 @@ export default function (col, screen) { return false; }, self); - // Add the graphing option if self is a graph so self we can set the value to graph - let menuOptions = Utils.getFieldFormats(); - if (self.original.format === "graph") { - menuOptions = menuOptions.concat([{ - value: "graph", - label: "", - }]); - } + const filterFormats = function (menuOptions, currentFormatValue) { + let filteredOptions = menuOptions; + // Add the graphing option if self is a graph so self we can set the value to graph + if (currentFormatValue === "graph") { + filteredOptions = filteredOptions.concat([{ + value: "graph", + label: "", + }]); + } - if (self.useXpathExpression) { - const menuOptionsToRemove = ['picture', 'audio']; - for (let i = 0; i < menuOptionsToRemove.length; i++) { - for (let j = 0; j < menuOptions.length; j++) { - if ( - menuOptions[j].value !== self.original.format - && menuOptions[j].value === menuOptionsToRemove[i] - ) { - menuOptions.splice(j, 1); + if (self.useXpathExpression) { + const menuOptionsToRemove = ['picture', 'audio']; + for (let i = 0; i < menuOptionsToRemove.length; i++) { + for (let j = 0; j < filteredOptions.length; j++) { + if ( + filteredOptions[j].value !== self.original.format + && filteredOptions[j].value === menuOptionsToRemove[i] + ) { + filteredOptions.splice(j, 1); + } } } + } else { + // Restrict Translatable Text usage to Calculated Properties only + const index = filteredOptions.findIndex(f => f.value.includes('translatable-enum')); + if (index !== -1) { + filteredOptions.splice(index, 1); + } } - } else { - // Restrict Translatable Text usage to Calculated Properties only - const index = menuOptions.findIndex(f => f.value.includes('translatable-enum')); - if (index !== -1) { - menuOptions.splice(index, 1); - } - } + return filteredOptions; + }; + let menuOptions = filterFormats(Utils.getFieldFormats(), self.original.format); self.format = uiElementSelect.new(menuOptions).val(self.original.format || null); + + self.updateFormatOptions = function (dynamicFormats, formatsToInclude) { + let updateMenuOptions = Utils.getFieldFormats(); + updateMenuOptions = updateMenuOptions.filter(function(option) { + if (!dynamicFormats.includes(option.value)) { + return true; + } + return formatsToInclude.includes(option.value); + }); + + const currentFormatValue = self.format && self.format.val ? self.format.val() : null; + updateMenuOptions = filterFormats(updateMenuOptions, currentFormatValue); + + self.format.setOptions(updateMenuOptions); + + const shouldClearSelection = currentFormatValue && dynamicFormats.includes(currentFormatValue) && + !formatsToInclude.includes(currentFormatValue); + if (shouldClearSelection) { + self.format.val('plain'); + self.format.ui.find('select').val('plain'); + } else if (currentFormatValue) { + self.format.val(currentFormatValue); + } + }; + self.supportsOptimizations = ko.observable(false); self.setSupportOptimizations = function () { let optimizationsSupported = ( From e22698272ed1d54049f8fa39f3b2dc9d1944c9c0 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 16:02:54 +0530 Subject: [PATCH 06/16] function to update menu options of all columns if a dependency format is selected, exceute on page load --- .../static/app_manager/js/details/column.js | 1 + .../static/app_manager/js/details/screen.js | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index 6bc805b511d8..b4a2fd410e50 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -338,6 +338,7 @@ export default function (col, screen) { let menuOptions = filterFormats(Utils.getFieldFormats(), self.original.format); self.format = uiElementSelect.new(menuOptions).val(self.original.format || null); + self.previousFormat = self.format.val(); self.updateFormatOptions = function (dynamicFormats, formatsToInclude) { let updateMenuOptions = Utils.getFieldFormats(); diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 33f90a874e09..700863cb179e 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -361,6 +361,10 @@ export default function (spec, config, options) { 'geo-boundary-color': ['address', 'geo-boundary'], 'geo-points-colors': ['address', 'geo-points'], }; + const uniqueDependencies = Array.from( + new Set(_.flatten(Object.values(COLUMN_FORMAT_DEPENDENCIES))), + ); + const columnsHasFormat = function (formatName) { return _.some(self.columns(), function(col) { return col.format && col.format.val && col.format.val() === formatName; @@ -380,6 +384,15 @@ export default function (spec, config, options) { }); return formatsToInclude; }; + const updateAllColumnFormats = function () { + const formatsToInclude = calculateDynamicFormatsToInclude(); + const dynamicFormats = Object.keys(COLUMN_FORMAT_DEPENDENCIES); + _.each(self.columns(), function (col) { + if (!col.isTab) { + col.updateFormatOptions(dynamicFormats, formatsToInclude); + } + }); + }; self.initColumnAsColumn = function (column) { column.model.setEdit(false); @@ -422,6 +435,16 @@ export default function (spec, config, options) { }]); } }); + column.format.on('change', function () { + const newFormat = column.format.val(); + const dependencyChanged = + uniqueDependencies.includes(column.previousFormat) || + uniqueDependencies.includes(newFormat); + if (dependencyChanged) { + updateAllColumnFormats(); + } + column.previousFormat = newFormat; + }); return column; }; @@ -463,6 +486,9 @@ export default function (spec, config, options) { self.initColumnAsColumn(self.columns()[i]); } + // Update all column formats on page load to account for dynamic formats + updateAllColumnFormats(); + self.caseTileRowMax = ko.computed(() => _.max([self.columns().length + 1, 7])); self.caseTileRowMax.subscribe(function (newValue) { self.updateTileRowMaxForColumns(newValue); From 61b4da01470c13fdd64b63dbaba49d5d189b718e Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 18 Dec 2025 16:12:10 +0530 Subject: [PATCH 07/16] update format options on adding or removing new display property --- .../app_manager/static/app_manager/js/details/screen.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 700863cb179e..3e3c5cff38d3 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -545,6 +545,7 @@ export default function (spec, config, options) { } else if (change.status === 'deleted') { move = 1; affectedColumns = self.columns.slice(change.index); + updateAllColumnFormats(); } if (affectedColumns) { affectedColumns.forEach(c => { @@ -756,6 +757,12 @@ export default function (spec, config, options) { self.columns.splice(index, 0, column); } column.useXpathExpression = !!columnConfiguration.useXpathExpression; + + const formatsToInclude = calculateDynamicFormatsToInclude(); + const dynamicFormats = Object.keys(FORMAT_DEPENDENCIES); + if (!column.isTab) { + column.updateFormatOptions(dynamicFormats, formatsToInclude); + } }; self.pasteCallback = function (data, index) { try { From 874f9e930ab010acd36f5655e7d42815fc4ad297 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Fri, 19 Dec 2025 13:48:43 +0530 Subject: [PATCH 08/16] show alert message to user on clearing selection --- .../static/app_manager/js/details/column.js | 5 ++- .../static/app_manager/js/details/screen.js | 10 ++---- .../static/app_manager/js/details/utils.js | 31 +++++++++++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index b4a2fd410e50..0442c8a6b748 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -15,6 +15,7 @@ import ko from "knockout"; import _ from "underscore"; import initialPageData from "hqwebapp/js/initial_page_data"; import main from "hqwebapp/js/bootstrap3/main"; +import alertUser from "hqwebapp/js/bootstrap3/alert_user"; import Utils from "app_manager/js/details/utils"; import uiElementInput from "hqwebapp/js/ui_elements/bootstrap3/ui-element-input"; import uiElementKeyValueMapping from "hqwebapp/js/ui_elements/bootstrap3/ui-element-key-val-mapping"; @@ -359,7 +360,9 @@ export default function (col, screen) { if (shouldClearSelection) { self.format.val('plain'); self.format.ui.find('select').val('plain'); - } else if (currentFormatValue) { + const message = Utils.dynamicFormats.getDependencyAlertMessage(currentFormatValue); + alertUser.alert_user(message, 'warning', false, true); + } else if (currentFormatValue != null) { self.format.val(currentFormatValue); } }; diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 3e3c5cff38d3..73a5c11f98a9 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -354,13 +354,7 @@ export default function (spec, config, options) { self.fire('change'); }; - // Key = format name, Value = array of required dependency formats (all must be present) - const COLUMN_FORMAT_DEPENDENCIES = { - 'geo-boundary': ['address'], - 'geo-points': ['address'], - 'geo-boundary-color': ['address', 'geo-boundary'], - 'geo-points-colors': ['address', 'geo-points'], - }; + const COLUMN_FORMAT_DEPENDENCIES = Utils.dynamicFormats.COLUMN_FORMAT_DEPENDENCIES; const uniqueDependencies = Array.from( new Set(_.flatten(Object.values(COLUMN_FORMAT_DEPENDENCIES))), ); @@ -759,7 +753,7 @@ export default function (spec, config, options) { column.useXpathExpression = !!columnConfiguration.useXpathExpression; const formatsToInclude = calculateDynamicFormatsToInclude(); - const dynamicFormats = Object.keys(FORMAT_DEPENDENCIES); + const dynamicFormats = Object.keys(COLUMN_FORMAT_DEPENDENCIES); if (!column.isTab) { column.updateFormatOptions(dynamicFormats, formatsToInclude); } diff --git a/corehq/apps/app_manager/static/app_manager/js/details/utils.js b/corehq/apps/app_manager/static/app_manager/js/details/utils.js index e669771cadb9..332770904d66 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/utils.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/utils.js @@ -113,6 +113,37 @@ module.getFieldFormats = function () { return formats; }; +// Dynamic format configuration and utilities +module.dynamicFormats = { + // Configuration: Define format dependencies + // Key = format name, Value = array of required dependency formats (all must be present) + COLUMN_FORMAT_DEPENDENCIES: { + 'geo-boundary': ['address'], + 'geo-points': ['address'], + 'geo-boundary-color': ['address', 'geo-boundary'], + 'geo-points-colors': ['address', 'geo-points'], + }, + + getFormatLabel: function (formatValue) { + const format = module.getFieldFormats().find(f => f.value === formatValue); + return format.label; + }, + + getDependencyAlertMessage: function (formatValue) { + const formatLabel = this.getFormatLabel(formatValue); + const dependencies = this.COLUMN_FORMAT_DEPENDENCIES[formatValue]; + const dependencyLabels = dependencies.map(dep => this.getFormatLabel(dep)).join(', '); + return interpolate( + gettext('The "%(format)s" format requires columns with formats: %(dependencies)s.'), + { + format: formatLabel, + dependencies: dependencyLabels + }, + true + ); + }, +}; + module.getFieldHtml = function (field) { var text = field || ''; if (module.isAttachmentProperty(text)) { From f392f0a7232f5eb97afaa29f61e8b7c07a606f86 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Mon, 22 Dec 2025 17:33:02 +0530 Subject: [PATCH 09/16] show new formats only in case list page --- .../static/app_manager/js/details/column.js | 16 +++++++++++ .../static/app_manager/js/details/screen.js | 8 ++++-- .../static/app_manager/js/details/utils.js | 28 ++++++++++++++----- 3 files changed, 42 insertions(+), 10 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index 0442c8a6b748..211e433b21fe 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -334,6 +334,22 @@ export default function (col, screen) { filteredOptions.splice(index, 1); } } + + // Filter formats based on screen type (short=case list, long=case detail) + const formatDeps = Utils.dynamicFormats.COLUMN_FORMAT_DEPENDENCIES; + filteredOptions = filteredOptions.filter(option => { + const config = formatDeps[option.value]; + if (!config || option.value === currentFormatValue) { + return true; + } + if (config.display === 'short') { + return screen.columnKey === 'short'; + } else if (config.display === 'long') { + return screen.columnKey === 'long'; + } + return true; + }); + return filteredOptions; }; diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 73a5c11f98a9..31bb65b0a8f8 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -356,7 +356,9 @@ export default function (spec, config, options) { const COLUMN_FORMAT_DEPENDENCIES = Utils.dynamicFormats.COLUMN_FORMAT_DEPENDENCIES; const uniqueDependencies = Array.from( - new Set(_.flatten(Object.values(COLUMN_FORMAT_DEPENDENCIES))), + new Set(_.flatten( + Object.values(COLUMN_FORMAT_DEPENDENCIES).map(config => config.dependencies) + )) ); const columnsHasFormat = function (formatName) { @@ -371,8 +373,8 @@ export default function (spec, config, options) { }; const calculateDynamicFormatsToInclude = function () { const formatsToInclude = []; - _.each(COLUMN_FORMAT_DEPENDENCIES, function(dependencies, formatName) { - if (areAllDependenciesPresent(dependencies)) { + _.each(COLUMN_FORMAT_DEPENDENCIES, function(config, formatName) { + if (areAllDependenciesPresent(config.dependencies)) { formatsToInclude.push(formatName); } }); diff --git a/corehq/apps/app_manager/static/app_manager/js/details/utils.js b/corehq/apps/app_manager/static/app_manager/js/details/utils.js index 332770904d66..683e26e3e82a 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/utils.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/utils.js @@ -115,13 +115,27 @@ module.getFieldFormats = function () { // Dynamic format configuration and utilities module.dynamicFormats = { - // Configuration: Define format dependencies - // Key = format name, Value = array of required dependency formats (all must be present) + // Configuration: Define format dependencies and display restrictions + // Key = format name, Value = object with: + // - dependencies: array of required dependency formats (all must be present) + // - display: where format should be shown ('short' for case list, 'long' for case-detail, or 'both') COLUMN_FORMAT_DEPENDENCIES: { - 'geo-boundary': ['address'], - 'geo-points': ['address'], - 'geo-boundary-color': ['address', 'geo-boundary'], - 'geo-points-colors': ['address', 'geo-points'], + 'geo-boundary': { + dependencies: ['address'], + display: 'short' + }, + 'geo-points': { + dependencies: ['address'], + display: 'short' + }, + 'geo-boundary-color': { + dependencies: ['address', 'geo-boundary'], + display: 'short' + }, + 'geo-points-colors': { + dependencies: ['address', 'geo-points'], + display: 'short' + }, }, getFormatLabel: function (formatValue) { @@ -131,7 +145,7 @@ module.dynamicFormats = { getDependencyAlertMessage: function (formatValue) { const formatLabel = this.getFormatLabel(formatValue); - const dependencies = this.COLUMN_FORMAT_DEPENDENCIES[formatValue]; + const dependencies = this.COLUMN_FORMAT_DEPENDENCIES[formatValue].dependencies; const dependencyLabels = dependencies.map(dep => this.getFormatLabel(dep)).join(', '); return interpolate( gettext('The "%(format)s" format requires columns with formats: %(dependencies)s.'), From f5c624fe60e7c3a22e76425bd79b10e86ddc7fd1 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Tue, 23 Dec 2025 12:57:00 +0530 Subject: [PATCH 10/16] js lint --- .../app_manager/static/app_manager/js/details/column.js | 4 ++-- .../app_manager/static/app_manager/js/details/screen.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/column.js b/corehq/apps/app_manager/static/app_manager/js/details/column.js index 211e433b21fe..a6b3878647dc 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/column.js @@ -359,7 +359,7 @@ export default function (col, screen) { self.updateFormatOptions = function (dynamicFormats, formatsToInclude) { let updateMenuOptions = Utils.getFieldFormats(); - updateMenuOptions = updateMenuOptions.filter(function(option) { + updateMenuOptions = updateMenuOptions.filter(function (option) { if (!dynamicFormats.includes(option.value)) { return true; } @@ -378,7 +378,7 @@ export default function (col, screen) { self.format.ui.find('select').val('plain'); const message = Utils.dynamicFormats.getDependencyAlertMessage(currentFormatValue); alertUser.alert_user(message, 'warning', false, true); - } else if (currentFormatValue != null) { + } else if (currentFormatValue !== null) { self.format.val(currentFormatValue); } }; diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 31bb65b0a8f8..181c8144be6e 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -362,18 +362,18 @@ export default function (spec, config, options) { ); const columnsHasFormat = function (formatName) { - return _.some(self.columns(), function(col) { + return _.some(self.columns(), function (col) { return col.format && col.format.val && col.format.val() === formatName; }); }; const areAllDependenciesPresent = function (dependencies) { - return _.every(dependencies, function(dep) { + return _.every(dependencies, function (dep) { return columnsHasFormat(dep); }); }; const calculateDynamicFormatsToInclude = function () { const formatsToInclude = []; - _.each(COLUMN_FORMAT_DEPENDENCIES, function(config, formatName) { + _.each(COLUMN_FORMAT_DEPENDENCIES, function (config, formatName) { if (areAllDependenciesPresent(config.dependencies)) { formatsToInclude.push(formatName); } From 0cba2dc48dd79672cd6de32abeff0d2b6cae268c Mon Sep 17 00:00:00 2001 From: Ajeet Date: Wed, 7 Jan 2026 12:53:03 +0530 Subject: [PATCH 11/16] nit: optmise function --- .../static/app_manager/js/details/screen.js | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/screen.js index 181c8144be6e..3d190e43ee18 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/screen.js @@ -361,25 +361,20 @@ export default function (spec, config, options) { )) ); - const columnsHasFormat = function (formatName) { - return _.some(self.columns(), function (col) { - return col.format && col.format.val && col.format.val() === formatName; - }); - }; - const areAllDependenciesPresent = function (dependencies) { - return _.every(dependencies, function (dep) { - return columnsHasFormat(dep); - }); - }; const calculateDynamicFormatsToInclude = function () { - const formatsToInclude = []; - _.each(COLUMN_FORMAT_DEPENDENCIES, function (config, formatName) { - if (areAllDependenciesPresent(config.dependencies)) { - formatsToInclude.push(formatName); - } - }); - return formatsToInclude; + const formatSet = new Set( + self.columns() + .map(col => col.format?.val?.()) + .filter(Boolean) + ); + + return Object.entries(COLUMN_FORMAT_DEPENDENCIES) + .filter(([, config]) => + config.dependencies.every(dep => formatSet.has(dep)) + ) + .map(([formatName]) => formatName); }; + const updateAllColumnFormats = function () { const formatsToInclude = calculateDynamicFormatsToInclude(); const dynamicFormats = Object.keys(COLUMN_FORMAT_DEPENDENCIES); From 093c043de430eb9ed9bfe4017881dc877b27304f Mon Sep 17 00:00:00 2001 From: Ajeet Date: Wed, 7 Jan 2026 20:07:22 +0530 Subject: [PATCH 12/16] adds mobile only to label of new formats this is done as it is supposed to be used only for mobile app --- .../app_manager/static/app_manager/js/details/utils.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/utils.js b/corehq/apps/app_manager/static/app_manager/js/details/utils.js index 683e26e3e82a..c63a4fe0f52b 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/utils.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/utils.js @@ -47,16 +47,16 @@ module.getFieldFormats = function () { label: gettext('Markdown'), },{ value: "geo-boundary", - label: gettext('Geo Boundary'), + label: gettext('Geo Boundary (Mobile only)'), }, { value: "geo-boundary-color", - label: gettext('Geo Boundary Color'), + label: gettext('Geo Boundary Color (Mobile only)'), }, { value: "geo-points", - label: gettext('Geo Points'), + label: gettext('Geo Points (Mobile only)'), }, { value: "geo-points-colors", - label: gettext('Geo Points Colors'), + label: gettext('Geo Points Colors (Mobile only)'), }]; if (toggles.toggleEnabled('CASE_LIST_MAP')) { From c90e0d95d3467ec294e100f54a258d533e3b0415 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Fri, 9 Jan 2026 11:44:29 +0530 Subject: [PATCH 13/16] fix template form wording --- corehq/apps/app_manager/detail_screen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/corehq/apps/app_manager/detail_screen.py b/corehq/apps/app_manager/detail_screen.py index ab10fe8f45af..2757a87dd773 100644 --- a/corehq/apps/app_manager/detail_screen.py +++ b/corehq/apps/app_manager/detail_screen.py @@ -610,22 +610,22 @@ class AddressPopup(HideShortColumn): @register_format_type('geo-boundary') class GeoBoundary(FormattedDetailColumn): - template_form = 'geo-boundary' + template_form = 'geo_boundary' @register_format_type('geo-boundary-color') class GeoBoundaryColor(FormattedDetailColumn): - template_form = 'geo-boundary-color' + template_form = 'geo_boundary_color_hex' @register_format_type('geo-points') class GeoPoints(FormattedDetailColumn): - template_form = 'geo-points' + template_form = 'geo_points' @register_format_type('geo-points-colors') class GeoPointsColors(FormattedDetailColumn): - template_form = 'geo-points-colors' + template_form = 'geo_points_colors_hex' @register_format_type('picture') From 975cee78f464b08ecf2ce27a4240be0d6e6a7bf7 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Sat, 10 Jan 2026 13:26:13 +0530 Subject: [PATCH 14/16] set width as zero for new formats --- corehq/apps/app_manager/detail_screen.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/corehq/apps/app_manager/detail_screen.py b/corehq/apps/app_manager/detail_screen.py index 2757a87dd773..019895dabdeb 100644 --- a/corehq/apps/app_manager/detail_screen.py +++ b/corehq/apps/app_manager/detail_screen.py @@ -609,22 +609,22 @@ class AddressPopup(HideShortColumn): @register_format_type('geo-boundary') -class GeoBoundary(FormattedDetailColumn): +class GeoBoundary(HideShortColumn): template_form = 'geo_boundary' @register_format_type('geo-boundary-color') -class GeoBoundaryColor(FormattedDetailColumn): +class GeoBoundaryColor(HideShortColumn): template_form = 'geo_boundary_color_hex' @register_format_type('geo-points') -class GeoPoints(FormattedDetailColumn): +class GeoPoints(HideShortColumn): template_form = 'geo_points' @register_format_type('geo-points-colors') -class GeoPointsColors(FormattedDetailColumn): +class GeoPointsColors(HideShortColumn): template_form = 'geo_points_colors_hex' From 6a348485b3c1d5fb11e2bac05ef2ca230e68e881 Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 15 Jan 2026 19:42:02 +0530 Subject: [PATCH 15/16] syncs bootstrap5 split version for changes made --- .../js/details/bootstrap5/column.js | 94 ++++++++++++++----- .../js/details/bootstrap5/screen.js | 78 ++++++++++++++- 2 files changed, 150 insertions(+), 22 deletions(-) diff --git a/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/column.js b/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/column.js index ff217fff8610..c13b6254f9f5 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/column.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/column.js @@ -15,6 +15,7 @@ import ko from "knockout"; import _ from "underscore"; import initialPageData from "hqwebapp/js/initial_page_data"; import main from "hqwebapp/js/bootstrap5/main"; +import alertUser from "hqwebapp/js/bootstrap5/alert_user"; import Utils from "app_manager/js/details/utils"; import uiElementInput from "hqwebapp/js/ui_elements/bootstrap5/ui-element-input"; import uiElementKeyValueMapping from "hqwebapp/js/ui_elements/bootstrap5/ui-element-key-val-mapping"; @@ -304,33 +305,84 @@ export default function (col, screen) { return false; }, self); - // Add the graphing option if self is a graph so self we can set the value to graph - let menuOptions = Utils.getFieldFormats(); - if (self.original.format === "graph") { - menuOptions = menuOptions.concat([{ - value: "graph", - label: "", - }]); - } + const filterFormats = function (menuOptions, currentFormatValue) { + let filteredOptions = menuOptions; + // Add the graphing option if self is a graph so self we can set the value to graph + if (currentFormatValue === "graph") { + filteredOptions = filteredOptions.concat([{ + value: "graph", + label: "", + }]); + } - if (self.useXpathExpression) { - const menuOptionsToRemove = ['picture', 'audio']; - for (let i = 0; i < menuOptionsToRemove.length; i++) { - for (let j = 0; j < menuOptions.length; j++) { - if ( - menuOptions[j].value !== self.original.format - && menuOptions[j].value === menuOptionsToRemove[i] - ) { - menuOptions.splice(j, 1); + if (self.useXpathExpression) { + const menuOptionsToRemove = ['picture', 'audio']; + for (let i = 0; i < menuOptionsToRemove.length; i++) { + for (let j = 0; j < filteredOptions.length; j++) { + if ( + filteredOptions[j].value !== self.original.format + && filteredOptions[j].value === menuOptionsToRemove[i] + ) { + filteredOptions.splice(j, 1); + } } } + } else { + // Restrict Translatable Text usage to Calculated Properties only + const index = filteredOptions.findIndex(f => f.value.includes('translatable-enum')); + if (index !== -1) { + filteredOptions.splice(index, 1); + } } - } else { - // Restrict Translatable Text usage to Calculated Properties only - menuOptions.splice(-1); - } + // Filter formats based on screen type (short=case list, long=case detail) + const formatDeps = Utils.dynamicFormats.COLUMN_FORMAT_DEPENDENCIES; + filteredOptions = filteredOptions.filter(option => { + const config = formatDeps[option.value]; + if (!config || option.value === currentFormatValue) { + return true; + } + if (config.display === 'short') { + return screen.columnKey === 'short'; + } else if (config.display === 'long') { + return screen.columnKey === 'long'; + } + return true; + }); + + return filteredOptions; + }; + + let menuOptions = filterFormats(Utils.getFieldFormats(), self.original.format); self.format = uiElementSelect.new(menuOptions).val(self.original.format || null); + self.previousFormat = self.format.val(); + + self.updateFormatOptions = function (dynamicFormats, formatsToInclude) { + let updateMenuOptions = Utils.getFieldFormats(); + updateMenuOptions = updateMenuOptions.filter(function (option) { + if (!dynamicFormats.includes(option.value)) { + return true; + } + return formatsToInclude.includes(option.value); + }); + + const currentFormatValue = self.format && self.format.val ? self.format.val() : null; + updateMenuOptions = filterFormats(updateMenuOptions, currentFormatValue); + + self.format.setOptions(updateMenuOptions); + + const shouldClearSelection = currentFormatValue && dynamicFormats.includes(currentFormatValue) && + !formatsToInclude.includes(currentFormatValue); + if (shouldClearSelection) { + self.format.val('plain'); + self.format.ui.find('select').val('plain'); + const message = Utils.dynamicFormats.getDependencyAlertMessage(currentFormatValue); + alertUser.alert_user(message, 'warning', false, true); + } else if (currentFormatValue !== null) { + self.format.val(currentFormatValue); + } + }; + self.supportsOptimizations = ko.observable(false); self.setSupportOptimizations = function () { let optimizationsSupported = ( diff --git a/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/screen.js b/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/screen.js index 621d1e657c19..586ddeab7055 100644 --- a/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/screen.js +++ b/corehq/apps/app_manager/static/app_manager/js/details/bootstrap5/screen.js @@ -354,6 +354,37 @@ export default function (spec, config, options) { self.fire('change'); }; + const COLUMN_FORMAT_DEPENDENCIES = Utils.dynamicFormats.COLUMN_FORMAT_DEPENDENCIES; + const uniqueDependencies = Array.from( + new Set(_.flatten( + Object.values(COLUMN_FORMAT_DEPENDENCIES).map(config => config.dependencies) + )) + ); + + const calculateDynamicFormatsToInclude = function () { + const formatSet = new Set( + self.columns() + .map(col => col.format?.val?.()) + .filter(Boolean) + ); + + return Object.entries(COLUMN_FORMAT_DEPENDENCIES) + .filter(([, config]) => + config.dependencies.every(dep => formatSet.has(dep)) + ) + .map(([formatName]) => formatName); + }; + + const updateAllColumnFormats = function () { + const formatsToInclude = calculateDynamicFormatsToInclude(); + const dynamicFormats = Object.keys(COLUMN_FORMAT_DEPENDENCIES); + _.each(self.columns(), function (col) { + if (!col.isTab) { + col.updateFormatOptions(dynamicFormats, formatsToInclude); + } + }); + }; + self.initColumnAsColumn = function (column) { column.model.setEdit(false); column.field.setEdit(true); @@ -395,6 +426,16 @@ export default function (spec, config, options) { }]); } }); + column.format.on('change', function () { + const newFormat = column.format.val(); + const dependencyChanged = + uniqueDependencies.includes(column.previousFormat) || + uniqueDependencies.includes(newFormat); + if (dependencyChanged) { + updateAllColumnFormats(); + } + column.previousFormat = newFormat; + }); return column; }; @@ -436,6 +477,9 @@ export default function (spec, config, options) { self.initColumnAsColumn(self.columns()[i]); } + // Update all column formats on page load to account for dynamic formats + updateAllColumnFormats(); + self.caseTileRowMax = ko.computed(() => _.max([self.columns().length + 1, 7])); self.caseTileRowMax.subscribe(function (newValue) { self.updateTileRowMaxForColumns(newValue); @@ -492,6 +536,7 @@ export default function (spec, config, options) { } else if (change.status === 'deleted') { move = 1; affectedColumns = self.columns.slice(change.index); + updateAllColumnFormats(); } if (affectedColumns) { affectedColumns.forEach(c => { @@ -509,7 +554,12 @@ export default function (spec, config, options) { // Only save if property names are valid var errors = [], containsTab = false, - imageColumnCount = 0; + imageColumnCount = 0, + geoBoundaryCount = 0, + geoBoundaryColorCount = 0, + geoPointsCount = 0, + geoPointsColorsCount = 0; + _.each(self.columns(), function (column) { column.saveAttempted(true); if (column.isTab) { @@ -521,12 +571,32 @@ export default function (spec, config, options) { errors.push(gettext("There is an error in your property name: ") + column.field.value); } else if (column.format.value === 'image') { imageColumnCount += 1; + } else if (column.format.value === 'geo-boundary') { + geoBoundaryCount += 1; + } else if (column.format.value === 'geo-boundary-color') { + geoBoundaryColorCount += 1; + } else if (column.format.value === 'geo-points') { + geoPointsCount += 1; + } else if (column.format.value === 'geo-points-colors') { + geoPointsColorsCount += 1; } }); if (imageColumnCount > 1) { errors.push(gettext("You can only have one property with the 'Image' format")); } + if (geoBoundaryCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Boundary' format")); + } + if (geoBoundaryColorCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Boundary Color' format")); + } + if (geoPointsCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Points' format")); + } + if (geoPointsColorsCount > 1) { + errors.push(gettext("You can only have one property with the 'Geo Points Colors' format")); + } if (containsTab) { if (!self.columns()[0].isTab) { errors.push(gettext("All properties must be below a tab.")); @@ -678,6 +748,12 @@ export default function (spec, config, options) { self.columns.splice(index, 0, column); } column.useXpathExpression = !!columnConfiguration.useXpathExpression; + + const formatsToInclude = calculateDynamicFormatsToInclude(); + const dynamicFormats = Object.keys(COLUMN_FORMAT_DEPENDENCIES); + if (!column.isTab) { + column.updateFormatOptions(dynamicFormats, formatsToInclude); + } }; self.pasteCallback = function (data, index) { try { From 1d275782ad8028e4994c7ca2ff118518f2ec16bb Mon Sep 17 00:00:00 2001 From: Ajeet Date: Thu, 15 Jan 2026 14:20:06 +0000 Subject: [PATCH 16/16] "Bootstrap 5 Migration - Rebuilt diffs" --- .../javascript/app_manager/js/details/column.js.diff.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/app_manager/js/details/column.js.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/app_manager/js/details/column.js.diff.txt index 4a2033e259a4..63e88bc4cea0 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/app_manager/js/details/column.js.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/app_manager/js/details/column.js.diff.txt @@ -1,11 +1,13 @@ --- +++ -@@ -14,14 +14,14 @@ +@@ -14,15 +14,15 @@ import ko from "knockout"; import _ from "underscore"; import initialPageData from "hqwebapp/js/initial_page_data"; -import main from "hqwebapp/js/bootstrap3/main"; +-import alertUser from "hqwebapp/js/bootstrap3/alert_user"; +import main from "hqwebapp/js/bootstrap5/main"; ++import alertUser from "hqwebapp/js/bootstrap5/alert_user"; import Utils from "app_manager/js/details/utils"; -import uiElementInput from "hqwebapp/js/ui_elements/bootstrap3/ui-element-input"; -import uiElementKeyValueMapping from "hqwebapp/js/ui_elements/bootstrap3/ui-element-key-val-mapping"; @@ -21,7 +23,7 @@ import google from "analytix/js/google"; const microCaseImageName = 'cc_case_image'; -@@ -146,7 +146,7 @@ +@@ -147,7 +147,7 @@ $modalDiv.koApplyBindings(self); const $modal = $modalDiv.find('.modal'); $modal.appendTo('body');