diff --git a/corehq/apps/app_manager/helpers/validators.py b/corehq/apps/app_manager/helpers/validators.py
index 38570a1b4c34..3d11a99ce8dd 100644
--- a/corehq/apps/app_manager/helpers/validators.py
+++ b/corehq/apps/app_manager/helpers/validators.py
@@ -647,19 +647,35 @@ def validate_details_for_build(self):
for detail in [self.module.case_details.short, self.module.case_details.long]:
if detail.case_tile_template:
- if not detail.display == "short":
- errors.append({
- 'type': self.__invalid_tile_configuration_type,
- 'module': self.get_module_info(),
- 'reason': _('Case tiles may only be used for the case list (not the case details).')
- })
+ if detail.display != "short":
+ if detail.case_tile_template != "custom":
+ errors.append({
+ 'type': self.__invalid_tile_configuration_type,
+ 'module': self.get_module_info(),
+ 'reason': _('Case tiles on the case detail must be manually configured.'),
+ })
+
+ tab_spans = detail.get_tab_spans()
+ tile_rows = defaultdict(set) # tile row index => {tabs that appear in that row}
+ for index, span in enumerate(tab_spans):
+ for col in detail.columns[span[0]:span[1]]:
+ if col.grid_y is not None:
+ tile_rows[col.grid_y].add(index)
+ for row_index, tab_index_list in tile_rows.items():
+ if len(tab_index_list) > 1:
+ errors.append({
+ 'type': self.__invalid_tile_configuration_type,
+ 'module': self.get_module_info(),
+ 'reason': _('Each row of the tile may contain fields only from a single tab. '
+ 'Row #{} contains fields from multiple tabs.').format(row_index + 1),
+ })
col_by_tile_field = {c.case_tile_field: c for c in detail.columns}
for field in case_tile_template_config(detail.case_tile_template).fields:
if field not in col_by_tile_field:
errors.append({
'type': self.__invalid_tile_configuration_type,
'module': self.get_module_info(),
- 'reason': _('A case property must be assigned to the "{}" tile field.'.format(field))
+ 'reason': _('A case property must be assigned to the "{}" tile field.').format(field)
})
self._validate_fields_with_format_duplicate('address', 'Address', detail.columns, errors)
self._validate_clickable_icons(detail.columns, errors)
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 f11a1b7e5352..d930c7bdcf78 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
@@ -48,6 +48,23 @@ hqDefine("app_manager/js/details/column", function () {
});
self.original.late_flag = _.isNumber(self.original.late_flag) ? self.original.late_flag : 30;
+ // Set up tab defaults
+ const tabDefaults = {
+ isTab: false,
+ hasNodeset: false,
+ nodeset: "",
+ nodesetCaseType: "",
+ nodesetFilter: "",
+ relevant: "",
+ };
+ self.original = _.defaults(self.original, tabDefaults);
+ let screenHasChildCaseTypes = screen.childCaseTypes && screen.childCaseTypes.length;
+ if (!self.original.nodeset && !self.original.nodesetCaseType && screenHasChildCaseTypes) {
+ // If there's no nodeset but there are child case types, default to showing a case type
+ self.original.nodesetCaseType = screen.childCaseTypes[0];
+ }
+ _.extend(self, _.pick(self.original, _.keys(tabDefaults)));
+
self.original.case_tile_field = ko.utils.unwrapObservable(self.original.case_tile_field) || "";
self.case_tile_field = ko.observable(self.original.case_tile_field);
@@ -99,30 +116,13 @@ hqDefine("app_manager/js/details/column", function () {
return Number(self.tileColumnStart()) + Number(self.tileWidth());
});
self.showInTilePreview = ko.computed(function () {
- return self.coordinatesVisible() && self.tileRowStart() && self.tileColumnStart() && self.tileWidth() && self.tileHeight();
+ return !self.isTab && self.coordinatesVisible() && self.tileRowStart() && self.tileColumnStart() && self.tileWidth() && self.tileHeight();
});
self.tileContent = ko.observable();
self.setTileContent = function () {
self.tileContent(self.header.val());
};
- // Set up tab defaults
- const tabDefaults = {
- isTab: false,
- hasNodeset: false,
- nodeset: "",
- nodesetCaseType: "",
- nodesetFilter: "",
- relevant: "",
- };
- self.original = _.defaults(self.original, tabDefaults);
- let screenHasChildCaseTypes = screen.childCaseTypes && screen.childCaseTypes.length;
- if (!self.original.nodeset && !self.original.nodesetCaseType && screenHasChildCaseTypes) {
- // If there's no nodeset but there are child case types, default to showing a case type
- self.original.nodesetCaseType = screen.childCaseTypes[0];
- }
- _.extend(self, _.pick(self.original, _.keys(tabDefaults)));
-
self.screen = screen;
self.lang = screen.lang;
self.model = uiElement.select([{
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 0bc09047c88e..a5d53c55648e 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
@@ -64,8 +64,11 @@ hqDefine("app_manager/js/details/screen", function () {
if (hqImport('hqwebapp/js/toggles').toggleEnabled('CASE_LIST_TILE_CUSTOM')) {
baseCaseTileTemplateOptions = baseCaseTileTemplateOptions.concat([["custom", gettext("Manually configure Case Tiles")]]);
}
+ if (self.columnKey === 'short') {
+ baseCaseTileTemplateOptions = baseCaseTileTemplateOptions.concat(options.caseTileTemplateOptions);
+ }
- self.caseTileTemplateOptions = baseCaseTileTemplateOptions.concat(options.caseTileTemplateOptions);
+ self.caseTileTemplateOptions = baseCaseTileTemplateOptions;
self.caseTileTemplateOptions = self.caseTileTemplateOptions.map(function (templateOption) {
return {templateValue: templateOption[0], templateName: templateOption[1]};
});
@@ -150,6 +153,21 @@ hqDefine("app_manager/js/details/screen", function () {
});
};
+ // Given a column model, return a boolean indicating whether the column is on an odd
+ // or an even tab, for the sake of being able to differentiate in the case tile preview
+ // which rows go with which tab.
+ self.tabPolarity = function (column) {
+ const self = this;
+ let flag = false;
+ _.find(self.columns(), function (c) {
+ if (c.isTab) {
+ flag = !flag;
+ }
+ return c === column;
+ });
+ return flag;
+ };
+
self.adjustTileGridArea = function (activeColumnIndex, rowDelta, columnDelta, widthDelta, heightDelta) {
let matrix = self._buildMatrix();
matrix = self._adjustTileGridArea(matrix, activeColumnIndex, rowDelta, columnDelta, widthDelta, heightDelta);
diff --git a/corehq/apps/app_manager/suite_xml/features/case_tiles.py b/corehq/apps/app_manager/suite_xml/features/case_tiles.py
index 33a24eae58c9..0373be45f542 100644
--- a/corehq/apps/app_manager/suite_xml/features/case_tiles.py
+++ b/corehq/apps/app_manager/suite_xml/features/case_tiles.py
@@ -11,7 +11,7 @@
from corehq.apps.app_manager import id_strings
from corehq.apps.app_manager.exceptions import SuiteError
from corehq.apps.app_manager.suite_xml.xml_models import (
- Detail, XPathVariable, Text, TileGroup, Style, EndpointAction
+ Detail, XPathVariable, TileGroup, Style, EndpointAction
)
from corehq.apps.app_manager.util import (
module_offers_search,
@@ -75,7 +75,7 @@ def __init__(self, app, module, detail, detail_id, detail_type, build_profile_id
self.detail_column_infos = detail_column_infos
self.entries_helper = entries_helper
- def build_case_tile_detail(self):
+ def build_case_tile_detail(self, detail, start, end):
from corehq.apps.app_manager.suite_xml.sections.details import DetailContributor
"""
Return a Detail node from an apps.app_manager.models.Detail that is
@@ -87,10 +87,10 @@ def build_case_tile_detail(self):
if self.detail.case_tile_template == CUSTOM:
from corehq.apps.app_manager.detail_screen import get_column_generator
- title = Text(locale_id=id_strings.detail_title_locale(self.detail_type))
- detail = Detail(id=self.detail_id, title=title)
- for column_info in self.detail_column_infos:
+ start = start or 0
+ end = end or len(self.detail_column_infos)
+ for column_info in self.detail_column_infos[start:end]:
# column_info is an instance of DetailColumnInfo named tuple.
style = None
if any(field is not None for field in [column_info.column.grid_x, column_info.column.grid_y,
@@ -133,31 +133,32 @@ def build_case_tile_detail(self):
self.app, self.module, detail.actions, self.build_profile_id, self.entries_helper)
# Add case search action if needed
- if module_offers_search(self.module) and not module_uses_inline_search(self.module):
- if (case_search_action := DetailContributor.get_case_search_action(
- self.module,
- self.build_profile_id,
- self.detail_id
- )) is not None:
- detail.actions.append(case_search_action)
-
- # Excludes legacy tile template to preserve behavior of existing apps using this template.
- if self.detail.case_tile_template not in [CaseTileTemplates.PERSON_SIMPLE.value, CUSTOM]:
- self._populate_sort_elements_in_detail(detail)
-
- DetailContributor.add_no_items_text_to_detail(detail, self.app, self.detail_type, self.module)
-
- DetailContributor.add_select_text_to_detail(detail, self.app, self.detail_type, self.module)
-
- if self.module.has_grouped_tiles():
- detail.tile_group = TileGroup(
- function=f"string(./index/{self.detail.case_tile_group.index_identifier})",
- header_rows=self.detail.case_tile_group.header_rows
- )
+ if self.detail_type.endswith('short'):
+ if module_offers_search(self.module) and not module_uses_inline_search(self.module):
+ if (case_search_action := DetailContributor.get_case_search_action(
+ self.module,
+ self.build_profile_id,
+ self.detail_id
+ )) is not None:
+ detail.actions.append(case_search_action)
+
+ # Excludes legacy tile template to preserve behavior of existing apps using this template.
+ if self.detail.case_tile_template not in [CaseTileTemplates.PERSON_SIMPLE.value, CUSTOM]:
+ self._populate_sort_elements_in_detail(detail)
+
+ DetailContributor.add_no_items_text_to_detail(detail, self.app, self.detail_type, self.module)
+
+ DetailContributor.add_select_text_to_detail(detail, self.app, self.detail_type, self.module)
+
+ if self.module.has_grouped_tiles():
+ detail.tile_group = TileGroup(
+ function=f"string(./index/{self.detail.case_tile_group.index_identifier})",
+ header_rows=self.detail.case_tile_group.header_rows
+ )
+
+ if hasattr(self.module, 'lazy_load_case_list_fields') and self.module.lazy_load_case_list_fields:
+ detail.lazy_loading = self.module.lazy_load_case_list_fields
- if (self.detail_type == 'case_short' or self.detail_type == 'search_short') \
- and hasattr(self.module, 'lazy_load_case_list_fields') and self.module.lazy_load_case_list_fields:
- detail.lazy_loading = self.module.lazy_load_case_list_fields
return detail
def _get_matched_detail_column(self, case_tile_field):
diff --git a/corehq/apps/app_manager/suite_xml/sections/details.py b/corehq/apps/app_manager/suite_xml/sections/details.py
index abb7ad55a288..b296ba5a35da 100644
--- a/corehq/apps/app_manager/suite_xml/sections/details.py
+++ b/corehq/apps/app_manager/suite_xml/sections/details.py
@@ -116,31 +116,20 @@ def get_section_elements(self):
) # list of DetailColumnInfo named tuples
if detail_column_infos:
detail_id = id_strings.detail(module, detail_type)
- if detail.case_tile_template:
- helper = CaseTileHelper(self.app, module, detail, detail_id, detail_type,
- self.build_profile_id, detail_column_infos, self.entries_helper)
-
- d = helper.build_case_tile_detail()
- self._add_custom_variables(detail, d)
+ locale_id = id_strings.detail_title_locale(detail_type)
+ title = Text(locale_id=locale_id) if locale_id else Text()
+ d = self.build_detail(
+ module,
+ detail_type,
+ detail,
+ detail_column_infos,
+ title,
+ tabs=list(detail.get_tabs()),
+ id=detail_id,
+ print_template=detail.print_template['path'] if detail.print_template else None,
+ )
+ if d:
elements.append(d)
- else:
- print_template_path = None
- if detail.print_template:
- print_template_path = detail.print_template['path']
- locale_id = id_strings.detail_title_locale(detail_type)
- title = Text(locale_id=locale_id) if locale_id else Text()
- d = self.build_detail(
- module,
- detail_type,
- detail,
- detail_column_infos,
- tabs=list(detail.get_tabs()),
- id=detail_id,
- title=title,
- print_template=print_template_path,
- )
- if d:
- elements.append(d)
# add the persist case context if needed and if
# case tiles are present and have their own persistent block
@@ -159,8 +148,8 @@ def get_section_elements(self):
return elements
- def build_detail(self, module, detail_type, detail, detail_column_infos, tabs=None, id=None,
- title=None, nodeset=None, print_template=None, start=0, end=None, relevant=None):
+ def build_detail(self, module, detail_type, detail, detail_column_infos, title, tabs=None, id=None,
+ nodeset=None, print_template=None, start=0, end=None, relevant=None):
"""
Recursively builds the Detail object.
(Details can contain other details for each of their tabs)
@@ -185,7 +174,7 @@ def build_detail(self, module, detail_type, detail, detail_column_infos, tabs=No
detail_type,
detail,
detail_column_infos,
- title=Text(locale_id=id_strings.detail_tab_title_locale(
+ Text(locale_id=id_strings.detail_tab_title_locale(
module, detail_type, tab
)),
nodeset=self._get_detail_tab_nodeset(module, detail, tab),
@@ -223,19 +212,33 @@ def build_detail(self, module, detail_type, detail, detail_column_infos, tabs=No
)
if variables:
d.variables.extend(variables)
-
- # Add fields
if end is None:
end = len(detail_column_infos)
- for column_info in detail_column_infos[start:end]:
- # column_info is an instance of DetailColumnInfo named tuple.
- fields = get_column_generator(
- self.app, module, detail, parent_tab_nodeset=nodeset,
- detail_type=detail_type, entries_helper=self.entries_helper,
- *column_info
- ).fields
- for field in fields:
- d.fields.append(field)
+
+ # Add fields
+ if detail.case_tile_template:
+ detail_id = id_strings.detail(module, detail_type)
+ helper = CaseTileHelper(
+ self.app,
+ module,
+ detail,
+ detail_id,
+ detail_type,
+ self.build_profile_id,
+ detail_column_infos,
+ self.entries_helper,
+ )
+ d = helper.build_case_tile_detail(d, start, end)
+ else:
+ for column_info in detail_column_infos[start:end]:
+ # column_info is an instance of DetailColumnInfo named tuple.
+ fields = get_column_generator(
+ self.app, module, detail, parent_tab_nodeset=nodeset,
+ detail_type=detail_type, entries_helper=self.entries_helper,
+ *column_info
+ ).fields
+ for field in fields:
+ d.fields.append(field)
# Add actions
if detail_type.endswith('short') and not module.put_in_root:
diff --git a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_detail.html b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_detail.html
index dd0f660fde0f..f42158049401 100644
--- a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_detail.html
+++ b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_detail.html
@@ -19,6 +19,11 @@
+ {% if request|toggle_enabled:'CASE_LIST_TILE_CUSTOM' %}
+ {% include 'app_manager/partials/modules/case_tile_templates.html' %}
+ {% include 'app_manager/partials/modules/case_tile_preview.html' %}
+ {% endif %}
+
{% include 'app_manager/partials/modules/case_list_properties.html' %}
diff --git a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_list_properties.html b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_list_properties.html
index dd0b6d0ff20f..1a1b7f2c416c 100644
--- a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_list_properties.html
+++ b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_list_properties.html
@@ -7,13 +7,12 @@
|
{% trans "Property" %} |
+ {% trans "Display Text" %} |
- {% trans "Display Text" %} |
{% trans "Format" %} |
{% trans "Case Tile Mapping" %} |
- {% trans "Display Text" %} |
{% trans "Format" %} |
@@ -58,24 +57,21 @@
- |
+ |
|
-
+ |
|
-
+ |
|
-
+ |
|
@@ -89,7 +85,7 @@
+ data-bind="text: gettext('Please enter an expression.'), visible: showWarning">
|
@@ -99,10 +95,15 @@
|
+
+ |
+ |
+ |
+
-
+
|
diff --git a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_tile_preview.html b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_tile_preview.html
index d035a5b9e7dd..d2cc1d79c93b 100644
--- a/corehq/apps/app_manager/templates/app_manager/partials/modules/case_tile_preview.html
+++ b/corehq/apps/app_manager/templates/app_manager/partials/modules/case_tile_preview.html
@@ -13,7 +13,9 @@
'grid-row-end': tileRowEnd,
'grid-column-start': tileColumnStart,
'grid-column-end': tileColumnEnd,
- }">
+ }, css: {
+ 'odd-tab': $parent.tabPolarity($data),
+ },">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/corehq/apps/app_manager/tests/data/suite/case-tile-case-detail.xml b/corehq/apps/app_manager/tests/data/suite/case-tile-case-detail.xml
new file mode 100644
index 000000000000..a148da24ab77
--- /dev/null
+++ b/corehq/apps/app_manager/tests/data/suite/case-tile-case-detail.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/corehq/apps/app_manager/tests/test_build_errors.py b/corehq/apps/app_manager/tests/test_build_errors.py
index b41adcb8789d..7570c03cc94b 100644
--- a/corehq/apps/app_manager/tests/test_build_errors.py
+++ b/corehq/apps/app_manager/tests/test_build_errors.py
@@ -18,6 +18,7 @@
CaseSearchLabel,
CaseSearchProperty,
DetailColumn,
+ DetailTab,
FormLink,
Module,
)
@@ -132,7 +133,7 @@ def test_parent_cycle_in_app(self, *args):
self._clean_unique_id(errors)
self.assertIn(cycle_error, errors)
- def test_case_tile_configuration_errors(self, *args):
+ def test_case_tile_mapping_errors(self, *args):
case_tile_error = {
'type': "invalid tile configuration",
'module': {'id': 0, 'name': {'en': 'View'}},
@@ -147,6 +148,83 @@ def test_case_tile_configuration_errors(self, *args):
self._clean_unique_id(errors)
self.assertIn(case_tile_error, errors)
+ def test_case_tile_case_detail(self, *args):
+ case_tile_error = {
+ 'type': 'invalid tile configuration',
+ 'module': {'id': 0, 'name': {'en': 'Add Song module'}},
+ 'reason': 'Case tiles on the case detail must be manually configured.',
+ }
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module = factory.new_basic_module('Add Song', 'song', with_form=False)
+ module.case_details.long.case_tile_template = "one_3X_two_4X_one_2X"
+ module.case_details.long.columns.append(DetailColumn(
+ format='plain',
+ field='artist',
+ header={'en': 'Artist'},
+ ))
+
+ errors = app.validate_app()
+ self._clean_unique_id(errors)
+ self.assertIn(case_tile_error, errors)
+
+ module.case_details.long.case_tile_template = "custom"
+ errors = app.validate_app()
+ self._clean_unique_id(errors)
+ self.assertNotIn(case_tile_error, errors)
+
+ def test_case_tile_case_detail_tabs(self, *args):
+ case_tile_error = {
+ 'type': 'invalid tile configuration',
+ 'module': {'id': 0, 'name': {'en': 'Add Song module'}},
+ 'reason': 'Each row of the tile may contain fields only from a single tab. '
+ 'Row #1 contains fields from multiple tabs.'
+ }
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module = factory.new_basic_module('Add Song', 'song', with_form=False)
+ module.case_details.long.case_tile_template = "custom"
+
+ # Start with a legitimate tab+column layout
+ module.case_details.long.tabs = [
+ DetailTab(starting_index=0),
+ DetailTab(starting_index=2),
+ ]
+ module.case_details.long.columns = []
+ module.case_details.long.columns.append(DetailColumn(
+ format='plain',
+ field='artist', header={'en': 'Artist'},
+ grid_x=0, grid_y=0, width=4, height=1,
+ ))
+ module.case_details.long.columns.append(DetailColumn(
+ format='plain',
+ field='name', header={'en': 'Name'},
+ grid_x=5, grid_y=0, width=4, height=1,
+ ))
+ module.case_details.long.columns.append(DetailColumn(
+ format='plain',
+ field='mood', header={'en': 'Mood'},
+ grid_x=0, grid_y=1, width=4, height=1,
+ ))
+ module.case_details.long.columns.append(DetailColumn(
+ format='plain',
+ field='energy', header={'en': 'Energy'},
+ grid_x=5, grid_y=1, width=4, height=1,
+ ))
+
+ module.case_details.long_case_tile_template = "custom"
+ errors = app.validate_app()
+ self._clean_unique_id(errors)
+ self.assertNotIn(case_tile_error, errors)
+
+ # Move field from second tab into first row of tile
+ module.case_details.long.columns[2].grid_y = 0
+ module.case_details.long.columns[2].grid_x = 9
+
+ errors = app.validate_app()
+ self._clean_unique_id(errors)
+ self.assertIn(case_tile_error, errors)
+
def create_app_with_module(self):
factory = AppFactory(build_version='2.51.0')
app = factory.app
diff --git a/corehq/apps/app_manager/tests/test_suite_custom_case_tiles.py b/corehq/apps/app_manager/tests/test_suite_custom_case_tiles.py
index 3b5df18f3861..4b04886cebea 100644
--- a/corehq/apps/app_manager/tests/test_suite_custom_case_tiles.py
+++ b/corehq/apps/app_manager/tests/test_suite_custom_case_tiles.py
@@ -3,8 +3,10 @@
from corehq.apps.app_manager.models import (
Application,
DetailColumn,
+ DetailTab,
Module,
)
+from corehq.apps.app_manager.tests.app_factory import AppFactory
from corehq.apps.app_manager.tests.util import (
SuiteMixin,
patch_get_xform_resource_overrides,
@@ -31,6 +33,7 @@ def add_columns_for_case_details(_module, field='a', format='plain', useXpathExp
@patch_get_xform_resource_overrides()
class SuiteCustomCaseTilesTest(SimpleTestCase, SuiteMixin):
+ file_path = ('data', 'suite')
def test_custom_case_tile(self, *args):
app = Application.new_app('domain', 'Untitled Application')
@@ -84,10 +87,9 @@ def test_custom_case_tile(self, *args):
self.assertXmlDoesNotHaveXpath(suite, "./detail[@id='m0_case_short']/field[2]")
def test_custom_case_tile_address(self, *args):
- app = Application.new_app('domain', 'Untitled Application')
-
- module = app.add_module(Module.new_module('Untitled Module', None))
- module.case_type = 'patient'
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module = factory.new_basic_module('register', 'patient', with_form=False)
module.case_details.short.case_tile_template = "custom"
add_columns_for_case_details(module, format='address')
@@ -116,10 +118,9 @@ def test_custom_case_tile_address(self, *args):
)
def test_custom_case_tile_empty_style(self, *args):
- app = Application.new_app('domain', 'Untitled Application')
-
- module = app.add_module(Module.new_module('Untitled Module', None))
- module.case_type = 'patient'
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module = factory.new_basic_module('register', 'patient', with_form=False)
module.case_details.short.case_tile_template = "custom"
module.case_details.short.columns = [
DetailColumn(
@@ -150,3 +151,73 @@ def test_custom_case_tile_empty_style(self, *args):
app.create_suite(),
"./detail[@id='m0_case_short']/field[1]"
)
+
+ def test_case_tile_for_case_detail(self, *args):
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module, form = factory.new_basic_module('Add Song', 'song')
+ module.case_details.long.case_tile_template = "custom"
+
+ module.case_details.long.columns = [
+ DetailColumn(
+ model='case', format='plain',
+ field='artist', header={'en': 'Artist'},
+ grid_x=0, grid_y=0, width=4, height=1,
+ ),
+ DetailColumn(
+ model='case', format='plain',
+ field='name', header={'en': 'Name'},
+ grid_x=5, grid_y=0, width=4, height=1,
+ ),
+ ]
+
+ self.assertXmlPartialEqual(
+ self.get_xml('case-tile-case-detail'),
+ app.create_suite(),
+ "./detail[@id='m0_case_long']"
+ )
+
+ def test_case_tile_for_case_detail_tabs(self, *args):
+ factory = AppFactory(build_version='2.51.0')
+ app = factory.app
+ module, form = factory.new_basic_module('Add Song', 'song')
+ module.case_details.long.case_tile_template = "custom"
+
+ module.case_details.long.tabs = [
+ DetailTab(starting_index=0),
+ DetailTab(starting_index=2),
+ ]
+
+ module.case_details.long.columns = [
+ DetailColumn(
+ model='case', format='plain',
+ field='artist', header={'en': 'Artist'},
+ grid_x=0, grid_y=0, width=6, height=1,
+ ),
+ DetailColumn(
+ model='case', format='plain',
+ field='name', header={'en': 'Name'},
+ grid_x=5, grid_y=0, width=6, height=1,
+ ),
+ DetailColumn(
+ model='case', format='plain',
+ field='rating', header={'en': 'Rating'},
+ grid_x=0, grid_y=1, width=4, height=1,
+ ),
+ DetailColumn(
+ model='case', format='plain',
+ field='energy', header={'en': 'Energy'},
+ grid_x=5, grid_y=1, width=4, height=1,
+ ),
+ DetailColumn(
+ model='case', format='plain',
+ field='mood', he1der={'en': 'Mood'},
+ grid_x=9, grid_y=1, width=4, height=1,
+ ),
+ ]
+
+ self.assertXmlPartialEqual(
+ self.get_xml('case-tile-case-detail-tabs'),
+ app.create_suite(),
+ "./detail[@id='m0_case_long']"
+ )
diff --git a/corehq/apps/app_manager/views/modules.py b/corehq/apps/app_manager/views/modules.py
index 40f5e69ac84f..a02ed94acd5b 100644
--- a/corehq/apps/app_manager/views/modules.py
+++ b/corehq/apps/app_manager/views/modules.py
@@ -1197,6 +1197,7 @@ def edit_module_detail_screens(request, domain, app_id, module_unique_id):
parent_select = params.get('parent_select', None)
fixture_select = params.get('fixture_select', None)
sort_elements = params.get('sort_elements', None)
+ case_tile_template = params.get('caseTileTemplate', None)
print_template = params.get('printTemplate', None)
custom_variables_dict = {
'short': params.get("short_custom_variables_dict", None),
@@ -1232,6 +1233,11 @@ def edit_module_detail_screens(request, domain, app_id, module_unique_id):
# Note that we use the empty tuple as the sentinel because a filter
# value of None represents clearing the filter.
detail.short.filter = filter
+ if case_tile_template is not None:
+ if short is not None:
+ detail.short.case_tile_template = case_tile_template
+ else:
+ detail.long.case_tile_template = case_tile_template
if custom_xml is not None:
detail.short.custom_xml = custom_xml
@@ -1416,7 +1422,6 @@ def _set_if_not_none(param_name, attribute_name=None):
if value is not None:
setattr(detail.short, attribute_name or param_name, value)
- _set_if_not_none('caseTileTemplate', 'case_tile_template')
_set_if_not_none('persistTileOnForms', 'persist_tile_on_forms')
_set_if_not_none('persistentCaseTileFromModule', 'persistent_case_tile_from_module')
_set_if_not_none('enableTilePullDown', 'pull_down_tile')
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
index a0a4b58bdbb1..0f1420e4a646 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/controller.js
@@ -202,15 +202,21 @@ hqDefine("cloudcare/js/formplayer/menus/controller", function () {
}
return;
}
- var detailObject = detailObjects[detailTabIndex];
- var menuListView = getDetailList(detailObject);
-
var tabModels = _.map(detailObjects, function (detail, index) {
return {title: detail.get('title'), id: index, active: index === detailTabIndex};
});
var tabCollection = new Backbone.Collection();
tabCollection.reset(tabModels);
+ let contentView;
+ const detailObject = detailObjects[detailTabIndex],
+ usesCaseTiles = detailObject.get('usesCaseTiles');
+ if (usesCaseTiles && !detailObject.get('entities')) {
+ contentView = getCaseTile(detailObject.toJSON());
+ } else {
+ contentView = getDetailList(detailObject);
+ }
+
var tabListView = views.DetailTabListView({
collection: tabCollection,
onTabClick: function (detailTabIndex) {
@@ -223,7 +229,7 @@ hqDefine("cloudcare/js/formplayer/menus/controller", function () {
isMultiSelect: isMultiSelect,
});
$('#case-detail-modal').find('.js-detail-tabs').html(tabListView.render().el);
- $('#case-detail-modal').find('.js-detail-content').html(menuListView.render().el);
+ $('#case-detail-modal').find('.js-detail-content').html(contentView.render().el);
$('#case-detail-modal').find('.js-detail-footer-content').html(detailFooterView.render().el);
$('#case-detail-modal').modal('show');
@@ -248,8 +254,12 @@ hqDefine("cloudcare/js/formplayer/menus/controller", function () {
collection: listCollection,
headers: detailObject.get('headers'),
styles: detailObject.get('styles'),
+ tiles: detailObject.get('tiles'),
title: detailObject.get('title'),
};
+ if (detailObject.get('usesCaseTiles')) {
+ return views.CaseTileDetailView(menuData);
+ }
return views.CaseListDetailView(menuData);
}
@@ -279,7 +289,7 @@ hqDefine("cloudcare/js/formplayer/menus/controller", function () {
});
};
- // return a case tile from a detail object (for persistent case tile)
+ // return a case tile from a detail object (for persistent case tile and case tile in case detail)
var getCaseTile = function (detailObject) {
var detailModel = new Backbone.Model({
data: detailObject.details,
diff --git a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
index 31bea2283f8a..cb21ef78123a 100644
--- a/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
+++ b/corehq/apps/cloudcare/static/cloudcare/js/formplayer/menus/views.js
@@ -473,6 +473,34 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
},
});
+ const CaseTileViewUnclickable = CaseTileView.extend({
+ events: {},
+ className: "list-cell-wrapper-style panel panel-default",
+ rowClick: function () {},
+ });
+
+ const initCaseTileList = function (options) {
+ const numEntitiesPerRow = options.numEntitiesPerRow || 1;
+ const numRows = options.maxHeight;
+ const numColumns = options.maxWidth;
+ const useUniformUnits = options.useUniformUnits;
+
+ const caseTileStyles = buildCaseTileStyles(options.tiles, options.styles, numRows, numColumns,
+ numEntitiesPerRow, useUniformUnits, 'list');
+
+ const gridPolyfillPath = FormplayerFrontend.getChannel().request('gridPolyfillPath');
+
+ $("#list-cell-layout-style").html(caseTileStyles.cellLayoutStyle).data("css-polyfilled", false);
+ $("#list-cell-grid-style").html(caseTileStyles.cellGridStyle).data("css-polyfilled", false);
+ // If we have multiple cases per line, need to generate the outer grid style as well
+ if (caseTileStyles.cellWrapperStyle && caseTileStyles.cellContainerStyle) {
+ $("#list-cell-wrapper-style").html(caseTileStyles.cellWrapperStyle).data("css-polyfilled", false);
+ $("#list-cell-container-style").html(caseTileStyles.cellContainerStyle).data("css-polyfilled", false);
+ }
+
+ $.getScript(gridPolyfillPath);
+ };
+
const CaseTileGroupedView = CaseTileView.extend({
tagName: "div",
className: "formplayer-request list-cell-wrapper-style case-tile-group panel panel-default",
@@ -540,7 +568,9 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
});
const PersistentCaseTileView = CaseTileView.extend({
- className: "formplayer-request persistent-sticky",
+ className: function () {
+ return "persistent-sticky" + (this.options.hasInlineTile ? " formplayer-request" : "");
+ },
rowClick: function (e) {
e.preventDefault();
if (this.options.hasInlineTile) {
@@ -599,8 +629,8 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
},
initialize: function (options) {
- const self = this;
- var sidebarNoItemsText = gettext("Please perform a search.");
+ const self = this,
+ sidebarNoItemsText = gettext("Please perform a search.");
self.styles = options.styles;
self.hasNoItems = options.collection.length === 0 || options.triggerEmptyCaseList;
self.noItemsText = options.triggerEmptyCaseList ? sidebarNoItemsText : this.options.collection.noItemsText;
@@ -1086,26 +1116,7 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
initialize: function (options) {
CaseTileListView.__super__.initialize.apply(this, arguments);
-
- const numEntitiesPerRow = options.numEntitiesPerRow || 1;
- const numRows = options.maxHeight;
- const numColumns = options.maxWidth;
- const useUniformUnits = options.useUniformUnits;
-
- const caseTileStyles = buildCaseTileStyles(options.tiles, options.styles, numRows, numColumns,
- numEntitiesPerRow, useUniformUnits, 'list');
-
- const gridPolyfillPath = FormplayerFrontend.getChannel().request('gridPolyfillPath');
-
- $("#list-cell-layout-style").html(caseTileStyles.cellLayoutStyle).data("css-polyfilled", false);
- $("#list-cell-grid-style").html(caseTileStyles.cellGridStyle).data("css-polyfilled", false);
- // If we have multiple cases per line, need to generate the outer grid style as well
- if (caseTileStyles.cellWrapperStyle && caseTileStyles.cellContainerStyle) {
- $("#list-cell-wrapper-style").html(caseTileStyles.cellWrapperStyle).data("css-polyfilled", false);
- $("#list-cell-container-style").html(caseTileStyles.cellContainerStyle).data("css-polyfilled", false);
- }
-
- $.getScript(gridPolyfillPath);
+ initCaseTileList(options);
registerContinueListener(this, options);
},
@@ -1213,6 +1224,21 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
template: _.template($("#case-view-list-detail-template").html() || ""),
childView: CaseViewUnclickable,
});
+ const CaseTileDetailView = CaseListView.extend({
+ template: _.template($("#case-view-tile-detail-template").html() || ""),
+ childView: CaseTileViewUnclickable,
+
+ initialize: function (options) {
+ CaseTileDetailView.__super__.initialize.apply(this, arguments);
+ initCaseTileList(options);
+ },
+
+ childViewOptions: function () {
+ const dict = CaseTileDetailView.__super__.childViewOptions.apply(this, arguments);
+ dict.prefix = 'list';
+ return dict;
+ },
+ });
const BreadcrumbView = Marionette.View.extend({
tagName: "li",
@@ -1446,6 +1472,9 @@ hqDefine("cloudcare/js/formplayer/menus/views", function () {
CaseListDetailView: function (options) {
return new CaseListDetailView(options);
},
+ CaseTileDetailView: function (options) {
+ return new CaseTileDetailView(options);
+ },
CaseListView: function (options) {
return new CaseListView(options);
},
diff --git a/corehq/apps/cloudcare/templates/formplayer/case_list.html b/corehq/apps/cloudcare/templates/formplayer/case_list.html
index 5af57581b198..ab82c658d01c 100644
--- a/corehq/apps/cloudcare/templates/formplayer/case_list.html
+++ b/corehq/apps/cloudcare/templates/formplayer/case_list.html
@@ -331,3 +331,7 @@ <%- title %>
+
+
diff --git a/corehq/apps/hqwebapp/static/app_manager/less/case_tile_preview.less b/corehq/apps/hqwebapp/static/app_manager/less/case_tile_preview.less
index 3d6f592a602e..01dc8d9d0fb7 100644
--- a/corehq/apps/hqwebapp/static/app_manager/less/case_tile_preview.less
+++ b/corehq/apps/hqwebapp/static/app_manager/less/case_tile_preview.less
@@ -7,12 +7,16 @@
.cell {
display: grid;
margin: 2px;
- border: 1px @cc-light-cool-accent-low dotted;
+ border: 1px @cc-brand-mid dotted;
position: relative;
// TODO: to be truly adorable, add transitions on changes to grid style, this doesn't work
transition: 2s;
+ &.odd-tab {
+ background-color: @cc-bg;
+ }
+
.content {
padding: 5px;
}
diff --git a/corehq/toggles/__init__.py b/corehq/toggles/__init__.py
index e152a3e28dbd..866b618eed06 100644
--- a/corehq/toggles/__init__.py
+++ b/corehq/toggles/__init__.py
@@ -709,7 +709,7 @@ def _ensure_valid_randomness(randomness):
CASE_LIST_TILE_CUSTOM = StaticToggle(
'case_list_tile_custom',
- 'USH: Configure custom case list tile',
+ 'USH: Configure custom case tile for case list and case detail',
TAG_CUSTOM,
[NAMESPACE_DOMAIN],
help_link='https://confluence.dimagi.com/pages/viewpage.action?'
|