Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions corehq/apps/app_manager/detail_screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,26 @@ class AddressPopup(HideShortColumn):
template_form = 'address-popup'


@register_format_type('geo-boundary')
class GeoBoundary(HideShortColumn):
template_form = 'geo_boundary'


@register_format_type('geo-boundary-color')
class GeoBoundaryColor(HideShortColumn):
template_form = 'geo_boundary_color_hex'


@register_format_type('geo-points')
class GeoPoints(HideShortColumn):
template_form = 'geo_points'


@register_format_type('geo-points-colors')
class GeoPointsColors(HideShortColumn):
template_form = 'geo_points_colors_hex'


@register_format_type('picture')
class Picture(FormattedDetailColumn):
template_form = 'image'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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";
Expand Down Expand Up @@ -256,18 +257,18 @@
return (self.field.observableVal() || self.saveAttempted()) && !Utils.isValidPropertyName(self.field.observableVal());
}, self);
self.caseListOptimizationsWarningText = gettext(
"Warning: Calculated property used is not compatible with caching and may result in stale data on case list properties"

Check warning on line 260 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Missing trailing comma

Check warning on line 260 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 8 spaces but found 6
);

function containsSpecialFunctions(str) {
// Match for following

Check warning on line 264 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 8 spaces but found 6
// 1. Xpath contains any of the following methods - today(), now(), here(), random(), uuid(), sleep(), depend()

Check warning on line 265 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 8 spaces but found 6
// 2. If Xpath contains any instance() expressions which doesn’t start with instance('casedb')

Check warning on line 266 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 8 spaces but found 6
const patterns = [

Check warning on line 267 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 8 spaces but found 6
/today\(\)/,

Check warning on line 268 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 12 spaces but found 8
/now\(\)/,

Check warning on line 269 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 12 spaces but found 8
/here\(\)/,

Check warning on line 270 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 12 spaces but found 8
/random\(\)/,

Check warning on line 271 in corehq/apps/app_manager/static/app_manager/js/details/bootstrap3/column.js

View workflow job for this annotation

GitHub Actions / Lint Javascript

Expected indentation of 12 spaces but found 8
/uuid\(\)/,
/sleep\(\)/,
/depend\(\)/,
Expand Down Expand Up @@ -304,33 +305,84 @@
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 = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 => {
Expand All @@ -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) {
Expand All @@ -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."));
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 = (
Expand Down
Loading