From 33b2412f11b7a29c2119be620542e1d16c6e3bd9 Mon Sep 17 00:00:00 2001 From: emre2038 Date: Mon, 19 Jun 2023 08:25:44 +0300 Subject: [PATCH 01/10] whitelisting indicators summary --- src/js/api.js | 57 +++++++++++++++++++++++++++ src/js/index.js | 4 +- src/js/models/data_filter.js | 8 +--- src/js/models/data_filter_model.js | 4 +- src/js/versions/version_controller.js | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/js/api.js b/src/js/api.js index 10cc1ee00..3c7f6f668 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -14,6 +14,15 @@ export class API extends Observable { this.busyLoggingIn = false; this.failedLogins = 0; this.abortController = null; + this._restrictValues = {}; + } + + set restrictValues(value) { + this._restrictValues = value; + } + + get restrictValues() { + return this._restrictValues; } getToken() { @@ -53,6 +62,20 @@ export class API extends Observable { return this.loadUrl(url, this.abortController); } + getIndicatorChildDataWrapper(profileId, areaCode, indicatorId) { + const self = this; + + return self.getIndicatorChildData(profileId, areaCode, indicatorId).then(data => { + Object.keys(data).forEach((geo) => { + Object.keys(self.restrictValues).forEach(restrictKey => { + data[geo] = data[geo].filter(x => self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + }) + }); + + return data; + }); + } + loadThemes(profileId) { const url = `${this.baseUrl}/profile/${profileId}/points/themes/?format=json`; return this.loadUrl(url); @@ -193,6 +216,40 @@ export class API extends Observable { return this.loadUrl(url, this.abortController); } + async getIndicatorSummaryWrapper(profileId, areaCode, version) { + const self = this; + + return self.getIndicatorSummary(profileId, areaCode, version).then(data => { + Object.keys(data).forEach((category) => { + let categoryData = data[category]; + let subCategories = categoryData.subcategories; + + Object.keys(subCategories).forEach((subCategory) => { + let subCategoryData = subCategories[subCategory]; + let indicators = subCategoryData.indicators; + + Object.keys(indicators).forEach((indicator) => { + let indicatorData = indicators[indicator]; + + Object.keys(self.restrictValues).forEach(restrictKey => { + indicatorData.metadata.groups = indicatorData.metadata.groups.map(group => { + if (group.name === restrictKey) { + group.subindicators = group.subindicators.filter(element => self.restrictValues[restrictKey].includes(element)); + } + + return group; + }) + }) + + console.log({'groups': indicatorData.metadata.groups}) + }); + }); + }); + + return data; + }); + } + cancelAndInitAbortController() { if (this.abortController !== null) { //on first request this.abortController is null diff --git a/src/js/index.js b/src/js/index.js index c5309ab49..f85af00c1 100644 --- a/src/js/index.js +++ b/src/js/index.js @@ -110,11 +110,13 @@ async function init() { const errorNotifier = new ErrorNotifier(); errorNotifier.registerErrorHandler(); - const api = new API(pc.baseUrl, hostname); + const api = new API(pc.baseUrl, pc.config); const data = await api.getProfileConfiguration(hostname); pc.config.setConfig(data.configuration || {}) pc.config.setVersions(data.geography_hierarchy || {}) + api.restrictValues = pc.config.restrictValues; + pc.config.api = api; pc.profile = data.id; pc.config.baseUrl = pc.baseUrl; diff --git a/src/js/models/data_filter.js b/src/js/models/data_filter.js index 663831586..1c02832ed 100644 --- a/src/js/models/data_filter.js +++ b/src/js/models/data_filter.js @@ -1,14 +1,10 @@ export class DataFilter { - constructor(group, restrictValues, keys = { + constructor(group, keys = { name: 'name', values: 'subindicators' }) { this._name = group[keys.name]; - let values = group[keys.values]; - if (restrictValues[group[keys.name]] !== undefined) { - values = values.filter(element => restrictValues[group[keys.name]].includes(element)); - } - this._values = values; + this._values = group[keys.values]; this._can_aggregate = group.can_aggregate; } diff --git a/src/js/models/data_filter_model.js b/src/js/models/data_filter_model.js index e9eaaac00..44ec57968 100644 --- a/src/js/models/data_filter_model.js +++ b/src/js/models/data_filter_model.js @@ -90,7 +90,7 @@ export class DataFilterModel extends Observable { let self = this; let gr = {}; this.groups.forEach(group => { - let dataFilter = new DataFilter(group, this.restrictValues, self.keys); + let dataFilter = new DataFilter(group, self.keys); gr[dataFilter[this.keys.name]] = dataFilter; }); @@ -101,7 +101,7 @@ export class DataFilterModel extends Observable { let self = this; let filters = []; this.groups.forEach(group => { - let dataFilter = new DataFilter(group, this.restrictValues, self.keys); + let dataFilter = new DataFilter(group, self.keys); filters.push(dataFilter); }); diff --git a/src/js/versions/version_controller.js b/src/js/versions/version_controller.js index 5c5e2b8b6..fd6f119a9 100644 --- a/src/js/versions/version_controller.js +++ b/src/js/versions/version_controller.js @@ -245,7 +245,7 @@ export class VersionController extends Component { const areaCode = payload.payload.areaCode; this.versions.forEach((version) => { - const promise = this.api.getIndicatorSummary(profileId, areaCode, version.model.name) + const promise = this.api.getIndicatorSummaryWrapper(profileId, areaCode, version.model.name) .then((data) => { const childrenIndicators = new ChildrenIndicators(data); From cbbba9660eca4466d22a82424455ae5eb2dafc5e Mon Sep 17 00:00:00 2001 From: emre2038 Date: Mon, 19 Jun 2023 09:51:32 +0300 Subject: [PATCH 02/10] not filtering if the key is not included --- src/js/api.js | 9 ++++++--- .../data_mapper/components/indicator_tree_view.js | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/js/api.js b/src/js/api.js index 3c7f6f668..dba1658fd 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -68,10 +68,15 @@ export class API extends Observable { return self.getIndicatorChildData(profileId, areaCode, indicatorId).then(data => { Object.keys(data).forEach((geo) => { Object.keys(self.restrictValues).forEach(restrictKey => { - data[geo] = data[geo].filter(x => self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + // does not contain the key + // or + // key value is one of the restrictValue + data[geo] = data[geo].filter(x => Object.keys(x).indexOf(restrictKey) < 0 || self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); }) }); + console.log({data}) + return data; }); } @@ -240,8 +245,6 @@ export class API extends Observable { return group; }) }) - - console.log({'groups': indicatorData.metadata.groups}) }); }); }); diff --git a/src/js/elements/data_mapper/components/indicator_tree_view.js b/src/js/elements/data_mapper/components/indicator_tree_view.js index f6ba29727..b9de46e19 100644 --- a/src/js/elements/data_mapper/components/indicator_tree_view.js +++ b/src/js/elements/data_mapper/components/indicator_tree_view.js @@ -101,7 +101,7 @@ const IndicatorItemView = (props) => { useEffect(() => { if (!props.indicator.isHidden && props.indicator?.indicatorData === undefined && !loading) { setLoading(true); - props.api.getIndicatorChildData( + props.api.getIndicatorChildDataWrapper( props.controller.state.profileId, props.controller.state.profile.profile.geography.code, props.indicator.id From 3601ca628cdc1e1bfb627e1de21fbd2262ee861e Mon Sep 17 00:00:00 2001 From: emre2038 Date: Mon, 19 Jun 2023 22:21:27 +0300 Subject: [PATCH 03/10] hiding menu elements if they do not have data --- .../components/indicator_tree_view.js | 163 +++++++++--------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/src/js/elements/data_mapper/components/indicator_tree_view.js b/src/js/elements/data_mapper/components/indicator_tree_view.js index b9de46e19..b820823e2 100644 --- a/src/js/elements/data_mapper/components/indicator_tree_view.js +++ b/src/js/elements/data_mapper/components/indicator_tree_view.js @@ -8,8 +8,6 @@ import { } from "./styledElements"; import Box from "@mui/material/Box"; import {Typography} from "@mui/material"; -import VisibilityIcon from '@mui/icons-material/Visibility'; -import VisibilityOffIcon from '@mui/icons-material/VisibilityOff'; const LoadingItemView = (props) => { @@ -24,6 +22,20 @@ const LoadingItemView = (props) => { ) } +const getSubIndicators = (indicator) => { + const primaryGroup = indicator.metadata.primary_group; + const primaryGroupObj = indicator.metadata.groups.filter( + group => group.name === primaryGroup + ) + + if (primaryGroupObj.length > 0) { + return primaryGroupObj[0].subindicators.filter( + sub => sub !== undefined && sub + ); + } + return []; +} + const SubindicatorItemView = (props) => { const onClickSubindicator = useCallback( () => { @@ -76,17 +88,7 @@ const IndicatorItemView = (props) => { const subindicators = useMemo( () => { const indicator = props.indicator; - const primaryGroup = indicator.metadata.primary_group; - const primaryGroupObj = indicator.metadata.groups.filter( - group => group.name === primaryGroup - ) - - if (primaryGroupObj.length > 0) { - return primaryGroupObj[0].subindicators.filter( - sub => sub !== undefined && sub - ); - } - return []; + return getSubIndicators(indicator); }, [ props.indicator ] @@ -117,41 +119,43 @@ const IndicatorItemView = (props) => { } ); - return ( - - - {props.indicator.label} - - - } data-test-id={`datamapper-indicator-${props.indicator.id}`}> - {loading && } - {!loading && subindicators.length > 0 && subindicators.map( - (subindicator, index) => { - return ( - - ) - }) - } - - ) + if (subindicators.length > 0) { + return ( + + + {props.indicator.label} + + + } data-test-id={`datamapper-indicator-${props.indicator.id}`}> + {loading && } + {!loading && subindicators.map( + (subindicator, index) => { + return ( + + ) + }) + } + + ) + } } const IndicatorSubCategoryTreeView = (props) => { - const [indicators, setIndicators] = useState(props.subcategory.indicators); + const [indicators, setIndicators] = useState(props.subcategory.indicators.filter(x => getSubIndicators(x).length > 0)); const handleIndicatorChange = (indicator) => { let newArr = indicators.map(ni => { @@ -163,40 +167,41 @@ const IndicatorSubCategoryTreeView = (props) => { setIndicators(newArr) } - return ( - - - {props.subcategory.name} - - - } data-test-id={`datamapper-subcategory-${props.subcategory.id}`}> - {!props.subcategory.length > 0 && indicators != null && indicators.map( - (indicator, index) => { - - if (!indicator.isHidden) { - return ( - handleIndicatorChange(indicator)} - key={`datamapper-indicator-${indicator.id}-${index}-${props.controller.state.profile.profile.geography.code}`} - api={props.api} - controller={props.controller} - categoryName={props.categoryName} - SubCategoryName={props.subcategory.name} - parents={{ - ...props.parents, - subcategory: props.subcategory.name - }} - /> - ) - } - }) - } - - - ) + if (indicators != null && indicators.length > 0) { + return ( + + + {props.subcategory.name} + + + } data-test-id={`datamapper-subcategory-${props.subcategory.id}`}> + {!props.subcategory.length > 0 && indicators != null && indicators.map( + (indicator, index) => { + + if (!indicator.isHidden) { + return ( + handleIndicatorChange(indicator)} + key={`datamapper-indicator-${indicator.id}-${index}-${props.controller.state.profile.profile.geography.code}`} + api={props.api} + controller={props.controller} + categoryName={props.categoryName} + SubCategoryName={props.subcategory.name} + parents={{ + ...props.parents, + subcategory: props.subcategory.name + }} + /> + ) + } + }) + } + + ) + } } const IndicatorCategoryTreeView = (props) => { From cf7d1d6772264dccddd5457a0013357eca604853 Mon Sep 17 00:00:00 2001 From: emre2038 Date: Thu, 22 Jun 2023 08:32:50 +0300 Subject: [PATCH 04/10] rich data is filtered --- src/js/api.js | 36 +++++++++++++++++++++++++-- src/js/profile/profile_loader.js | 2 ++ src/js/versions/version_controller.js | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/js/api.js b/src/js/api.js index dba1658fd..4aa9da079 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -42,6 +42,40 @@ export class API extends Observable { return this.loadUrl(url, this.abortController); } + getProfileWrapper(profileId, areaCode, version) { + const self = this; + + return self.getProfile(profileId, areaCode, version).then(data => { + Object.keys(data.profile.profile_data).forEach(categoryName => { + const subcategories = data.profile.profile_data[categoryName].subcategories; + Object.keys(subcategories).forEach(subcategoryName => { + const indicators = subcategories[subcategoryName].indicators; + if (indicators != null) { + Object.keys(indicators).forEach((indicator) => { + let indicatorData = indicators[indicator]; + + Object.keys(self.restrictValues).forEach(restrictKey => { + indicatorData.data = indicatorData.data.filter(x => self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + + indicatorData.metadata.groups = indicatorData.metadata.groups.map(group => { + if (group.name === restrictKey) { + group.subindicators = group.subindicators.filter(element => self.restrictValues[restrictKey].includes(element)); + } + + return group; + }) + + }) + }); + } + }) + }) + console.log({data}) + + return data; + }); + } + getProfileWithoutVersion(profileId, areaCode) { const url = `${this.baseUrl}/all_details/profile/${profileId}/geography/${areaCode}/?skip-children=true&format=json`; return this.loadUrl(url, this.abortController); @@ -75,8 +109,6 @@ export class API extends Observable { }) }); - console.log({data}) - return data; }); } diff --git a/src/js/profile/profile_loader.js b/src/js/profile/profile_loader.js index bdb8bbef7..bbbc10baf 100644 --- a/src/js/profile/profile_loader.js +++ b/src/js/profile/profile_loader.js @@ -106,6 +106,8 @@ export default class ProfileLoader extends Component { } loadCategories = (profile) => { + console.log({'profileData':profile.profileData}) + let removePrevCategories = true; const categories = profile.profileData; let isFirst = true; diff --git a/src/js/versions/version_controller.js b/src/js/versions/version_controller.js index fd6f119a9..0872b862e 100644 --- a/src/js/versions/version_controller.js +++ b/src/js/versions/version_controller.js @@ -352,7 +352,7 @@ export class VersionController extends Component { } getAllDetails(version) { - const promise = this.api.getProfile(this.profileId, this.areaCode, version.model.name).then(js => { + const promise = this.api.getProfileWrapper(this.profileId, this.areaCode, version.model.name).then(js => { version.model.exists = true; this.versionsRawData.push({ 'version': version, From 20bca90d50dcd6134901807d04c3e22e15f8f42a Mon Sep 17 00:00:00 2001 From: emre2038 Date: Thu, 22 Jun 2023 08:56:10 +0300 Subject: [PATCH 05/10] fix for when the key does not exist --- src/js/api.js | 11 ++++++++--- src/js/profile/profile_loader.js | 2 -- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/js/api.js b/src/js/api.js index 4aa9da079..9ddadb553 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -55,8 +55,13 @@ export class API extends Observable { let indicatorData = indicators[indicator]; Object.keys(self.restrictValues).forEach(restrictKey => { - indicatorData.data = indicatorData.data.filter(x => self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + // does not contain the key + // or + // key value is one of the restrictValue + indicatorData.data = indicatorData.data.filter(x => Object.keys(x).indexOf(restrictKey) < 0 || + self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + // filter metadata indicatorData.metadata.groups = indicatorData.metadata.groups.map(group => { if (group.name === restrictKey) { group.subindicators = group.subindicators.filter(element => self.restrictValues[restrictKey].includes(element)); @@ -70,7 +75,6 @@ export class API extends Observable { } }) }) - console.log({data}) return data; }); @@ -105,7 +109,8 @@ export class API extends Observable { // does not contain the key // or // key value is one of the restrictValue - data[geo] = data[geo].filter(x => Object.keys(x).indexOf(restrictKey) < 0 || self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); + data[geo] = data[geo].filter(x => Object.keys(x).indexOf(restrictKey) < 0 + || self.restrictValues[restrictKey].indexOf(x[restrictKey]) >= 0); }) }); diff --git a/src/js/profile/profile_loader.js b/src/js/profile/profile_loader.js index bbbc10baf..bdb8bbef7 100644 --- a/src/js/profile/profile_loader.js +++ b/src/js/profile/profile_loader.js @@ -106,8 +106,6 @@ export default class ProfileLoader extends Component { } loadCategories = (profile) => { - console.log({'profileData':profile.profileData}) - let removePrevCategories = true; const categories = profile.profileData; let isFirst = true; From dcb5f2389eb5b47b1be17f6b7ea4d2d72f635d68 Mon Sep 17 00:00:00 2001 From: emre2038 Date: Thu, 22 Jun 2023 10:44:28 +0300 Subject: [PATCH 06/10] hiding indicators that do not have any data in rich data panel --- src/js/profile/blocks/indicator.js | 2 ++ src/js/profile/subcategory.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/js/profile/blocks/indicator.js b/src/js/profile/blocks/indicator.js index 2913e1856..4b9f509e0 100644 --- a/src/js/profile/blocks/indicator.js +++ b/src/js/profile/blocks/indicator.js @@ -17,6 +17,7 @@ export class Indicator extends ContentBlock { hiddenIndicators = [] ) { super(parent, container, indicator, title, isLast, geography, hiddenIndicators); + this.chartAttribution = chartAttribution; this._chart = null; this.prepareDomElements(); @@ -63,6 +64,7 @@ export class Indicator extends ContentBlock { const configuration = this.indicator.chartConfiguration; let chartData = this.orderChartData(); + let c = new Chart(this, configuration, chartData, groups, this.container, this.title, this.chartAttribution, addLockButton, restrictValues, defaultFilters); this.bubbleEvents(c, [ 'profile.chart.saveAsPng', 'profile.chart.valueTypeChanged', diff --git a/src/js/profile/subcategory.js b/src/js/profile/subcategory.js index 203559b40..ede2c1201 100644 --- a/src/js/profile/subcategory.js +++ b/src/js/profile/subcategory.js @@ -101,19 +101,19 @@ export class Subcategory extends Component { } set hasKeyMetrics(value) { - this._hasKeyMetrics=value; + this._hasKeyMetrics = value; } updateVisibility = () => { - this.isVisible = Object.values(this._indicators).filter( - indicator => indicator.isVisible - ).length > 0 || this.hasKeyMetrics; - this.parent.updateVisibility(); + this.isVisible = Object.values(this._indicators).filter( + indicator => indicator.isVisible + ).length > 0 || this.hasKeyMetrics; + this.parent.updateVisibility(); } updateDomElements = () => { - $(this._scHeader).parents('.section').find(subcategoryHeaderClass).removeClass('first'); - $(this._scHeader).removeClass('page-break-before').addClass('first'); + $(this._scHeader).parents('.section').find(subcategoryHeaderClass).removeClass('first'); + $(this._scHeader).removeClass('page-break-before').addClass('first'); } addSubCategoryHeaders = (wrapper, subcategory, detail, isFirst) => { @@ -181,7 +181,7 @@ export class Subcategory extends Component { if (!isEmpty) { for (const indicator of sortBy(detail.indicators, "order")) { const title = Object.keys(detail.indicators).filter(k => detail.indicators[k] === indicator)[0]; - if (typeof indicator.data !== 'undefined') { + if (typeof indicator.data !== 'undefined' && indicator.data.length > 0) { let isLast = index === lastIndex; let block = null; From fd2b2cf4402224b339ecc5377d0b6fe868e24c58 Mon Sep 17 00:00:00 2001 From: emre2038 Date: Fri, 23 Jun 2023 09:49:00 +0300 Subject: [PATCH 07/10] added automated test coverage --- __tests__/gui/data_whitelist.feature | 6 ++++-- .../gui/data_whitelist/data_whitelist.js | 21 ++++++++++++++----- __tests__/gui/data_whitelist/profile.json | 9 ++++++++ .../profile_indicator_summary.json | 3 +++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/__tests__/gui/data_whitelist.feature b/__tests__/gui/data_whitelist.feature index 57e3896da..f30b3db5b 100644 --- a/__tests__/gui/data_whitelist.feature +++ b/__tests__/gui/data_whitelist.feature @@ -9,6 +9,7 @@ Feature: Data Whitelist And I click on "Demographics" in Data Mapper And I click on "Language" in Data Mapper And I click on "Language most spoken at home" in Data Mapper + And I confirm that available subindicators are "20-24,30-35" in Data Mapper And I click on "30-35" in Data Mapper And I expand the filter dialog Then I confirm that the choropleth is filtered by "gender:Female" at index 0 @@ -20,7 +21,7 @@ Feature: Data Whitelist # Rich data panel default filter When I expand Rich Data Panel - Then I confirm that "gender:Female" is applied to "Language most spoken at home" as a site-wide filter + Then I confirm that "gender:Female" is applied to "Language most spoken at home" Scenario: Restricting filter values and setting profile-wide filters on a view Given I am on the Wazimap Homepage Test View @@ -31,6 +32,7 @@ Feature: Data Whitelist And I click on "Demographics" in Data Mapper And I click on "Language" in Data Mapper And I click on "Language most spoken at home" in Data Mapper + And I confirm that available subindicators are "15-19,20-24,30-35" in Data Mapper And I click on "30-35" in Data Mapper And I expand the filter dialog Then I confirm that the choropleth is filtered by "gender:Male" at index 0 @@ -42,4 +44,4 @@ Feature: Data Whitelist # Rich data panel default filter When I expand Rich Data Panel - Then I confirm that "gender:Male" is applied to "Language most spoken at home" as a site-wide filter \ No newline at end of file + Then I confirm that "gender:Male" is applied to "Language most spoken at home" \ No newline at end of file diff --git a/__tests__/gui/data_whitelist/data_whitelist.js b/__tests__/gui/data_whitelist/data_whitelist.js index 0cbe96479..996aee155 100644 --- a/__tests__/gui/data_whitelist/data_whitelist.js +++ b/__tests__/gui/data_whitelist/data_whitelist.js @@ -1,10 +1,13 @@ import {Given, Then, When} from "cypress-cucumber-preprocessor/steps"; import { - collapseMyViewPanel, confirmChartIsFiltered, - confirmChoroplethIsFiltered, confirmDropdownOptions, + confirmChartIsFiltered, + confirmChoroplethIsFiltered, + confirmDropdownOptions, expandChoroplethFilterDialog, - expandDataMapper, expandRichDataPanel, - gotoHomepage, selectChoroplethDropdownOption, + expandDataMapper, + expandRichDataPanel, + gotoHomepage, + selectChoroplethDropdownOption, setupInterceptions, waitUntilGeographyIsLoaded } from "../common_cy_functions/general"; @@ -57,7 +60,7 @@ When('I expand Rich Data Panel', () => { expandRichDataPanel(); }) -Then(/^I confirm that "([^"]*)" is applied to "([^"]*)" as a site\-wide filter$/, function (filter, chartTitle) { +Then(/^I confirm that "([^"]*)" is applied to "([^"]*)"$/, function (filter, chartTitle) { const filters = filter.split(':'); confirmChartIsFiltered(filters[0], filters[1], chartTitle); }); @@ -66,3 +69,11 @@ Given('I am on the Wazimap Homepage Test View', () => { setupInterceptions(profiles, all_details, profile, themes, {}, [], profile_indicator_summary, profile_indicator_data); cy.visit("/?view=test"); }) + +When(/^I confirm that available subindicators are "([^"]*)" in Data Mapper$/, function (options) { + let optionsArr = options.split(','); + cy.get('.data-mapper').find('.subIndicator-item').should('have.length', optionsArr.length) + cy.get('.subIndicator-item').each(($div, index) => { + expect($div.text()).equal(optionsArr[index]); + }) +}); \ No newline at end of file diff --git a/__tests__/gui/data_whitelist/profile.json b/__tests__/gui/data_whitelist/profile.json index a8d11bb0a..540871bc0 100644 --- a/__tests__/gui/data_whitelist/profile.json +++ b/__tests__/gui/data_whitelist/profile.json @@ -37,6 +37,10 @@ "English", "Afrikaans", "This is ignored" + ], + "age": [ + "20-24", + "30-35" ] }, "views": { @@ -52,6 +56,11 @@ "English", "Sepedi", "isiXhosa" + ], + "age": [ + "15-19", + "20-24", + "30-35" ] } } diff --git a/__tests__/gui/data_whitelist/profile_indicator_summary.json b/__tests__/gui/data_whitelist/profile_indicator_summary.json index ab0f1f2cf..4af88a86e 100644 --- a/__tests__/gui/data_whitelist/profile_indicator_summary.json +++ b/__tests__/gui/data_whitelist/profile_indicator_summary.json @@ -5,6 +5,7 @@ "subcategories": { "Language": { "order": 242, + "id": 1, "name": "Language", "indicators": { "Language most spoken at home": { @@ -108,6 +109,7 @@ }, "Migration": { "order": 243, + "id": 2, "name": "Migration", "indicators": { "Region of birth": { @@ -211,6 +213,7 @@ }, "South African Citizenship": { "order": 246, + "id": 3, "name": "South African Citizenship", "indicators": { "Citizenship": { From 9129e81794722e9704d03bd2deaaf03f2829aa2d Mon Sep 17 00:00:00 2001 From: emre2038 Date: Mon, 26 Jun 2023 16:55:48 +0300 Subject: [PATCH 08/10] fixed automated tests --- src/js/api.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/js/api.js b/src/js/api.js index 9ddadb553..98b9f483b 100644 --- a/src/js/api.js +++ b/src/js/api.js @@ -270,19 +270,21 @@ export class API extends Observable { let subCategoryData = subCategories[subCategory]; let indicators = subCategoryData.indicators; - Object.keys(indicators).forEach((indicator) => { - let indicatorData = indicators[indicator]; + if (indicators != null) { + Object.keys(indicators).forEach((indicator) => { + let indicatorData = indicators[indicator]; - Object.keys(self.restrictValues).forEach(restrictKey => { - indicatorData.metadata.groups = indicatorData.metadata.groups.map(group => { - if (group.name === restrictKey) { - group.subindicators = group.subindicators.filter(element => self.restrictValues[restrictKey].includes(element)); - } + Object.keys(self.restrictValues).forEach(restrictKey => { + indicatorData.metadata.groups = indicatorData.metadata.groups.map(group => { + if (group.name === restrictKey) { + group.subindicators = group.subindicators.filter(element => self.restrictValues[restrictKey].includes(element)); + } - return group; + return group; + }) }) - }) - }); + }); + } }); }); From bb33e1e06ee18c33b8cbcdc39b0e36a26cb4dbcf Mon Sep 17 00:00:00 2001 From: emre2038 Date: Thu, 6 Jul 2023 10:10:38 +0300 Subject: [PATCH 09/10] showing no data available warning --- .../components/indicator_tree_view.js | 39 +- .../data_mapper/components/styledElements.js | 462 +++++++++--------- 2 files changed, 276 insertions(+), 225 deletions(-) diff --git a/src/js/elements/data_mapper/components/indicator_tree_view.js b/src/js/elements/data_mapper/components/indicator_tree_view.js index b820823e2..1db5b078f 100644 --- a/src/js/elements/data_mapper/components/indicator_tree_view.js +++ b/src/js/elements/data_mapper/components/indicator_tree_view.js @@ -4,10 +4,12 @@ import { StyledCategoryTreeItem, StyledSubCategoryTreeItem, StyledSubindicatorTreeItem, - StyledIndicatorTreeItem + StyledIndicatorTreeItem, + StyledNoSubindicatorTreeItem } from "./styledElements"; import Box from "@mui/material/Box"; import {Typography} from "@mui/material"; +import {checkIfSubIndicatorHasChildren} from "../../../utils"; const LoadingItemView = (props) => { @@ -36,6 +38,24 @@ const getSubIndicators = (indicator) => { return []; } +const NoSubindicatorView = (props) => { + return ( + + + No data available for this indicator in the current view. + + + } + data-test-id={`datamapper-subindicator-${props.indicator.id}-not-available`} + className={"subIndicator-item"} + /> + ) +} + const SubindicatorItemView = (props) => { const onClickSubindicator = useCallback( () => { @@ -119,6 +139,18 @@ const IndicatorItemView = (props) => { } ); + const checkForSubIndicatorData = () => { + let isValid = false; + let indicatorData = props.indicator?.indicatorData; + if (indicatorData !== undefined) { + isValid = Object.keys(indicatorData).some((geo) => { + return indicatorData[geo].length > 0; + }) + } + + return isValid; + } + if (subindicators.length > 0) { return ( { } data-test-id={`datamapper-indicator-${props.indicator.id}`}> {loading && } - {!loading && subindicators.map( + {!loading && checkForSubIndicatorData() && subindicators.map( (subindicator, index) => { return ( { ) }) } + {!loading && !checkForSubIndicatorData() && } ) } diff --git a/src/js/elements/data_mapper/components/styledElements.js b/src/js/elements/data_mapper/components/styledElements.js index 35c90c0c5..7c619534c 100644 --- a/src/js/elements/data_mapper/components/styledElements.js +++ b/src/js/elements/data_mapper/components/styledElements.js @@ -1,260 +1,276 @@ import React from 'react'; -import { styled } from '@mui/system'; +import {styled} from '@mui/system'; import SvgIcon from '@mui/material/SvgIcon'; import Box from '@mui/material/Box'; -import TreeItem, { treeItemClasses } from '@mui/lab/TreeItem'; +import TreeItem, {treeItemClasses} from '@mui/lab/TreeItem'; export const StyledCategoryTreeItem = styled(TreeItem)(() => ({ - [`& .${treeItemClasses.content}`]: { - 'flexDirection': 'row-reverse', - 'marginBottom': '8px', - 'backgroundColor': '#39ad84', - 'borderRadius': '2px', - 'height': '36px', - 'paddingLeft': '8px', - 'transition': 'all .2s ease', - 'color': '#fff', - 'textDecoration': 'none', - 'cursor': 'pointer', - 'fontFamily': 'Roboto,sans-serif', - '&:hover': { - 'backgroundColor': '#39ad84', + [`& .${treeItemClasses.content}`]: { + 'flexDirection': 'row-reverse', + 'marginBottom': '8px', + 'backgroundColor': '#39ad84', + 'borderRadius': '2px', + 'height': '36px', + 'paddingLeft': '8px', + 'transition': 'all .2s ease', + 'color': '#fff', + 'textDecoration': 'none', + 'cursor': 'pointer', + 'fontFamily': 'Roboto,sans-serif', + '&:hover': { + 'backgroundColor': '#39ad84', + }, + '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { + 'backgroundColor': '#39ad84', + }, + '& .MuiTreeItem-iconContainer svg': { + fontSize: '22px' + }, + '& .MuiTreeItem-label': { + paddingLeft: '0px', + }, + '& .MuiBox-root': { + padding: '0px', + }, + [`& .${treeItemClasses.label}`]: { + 'width': '100%', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'fontSize': '1em', + 'fontWeight': '500', + 'letterSpacing': '.3px', + 'marginRight': '12px', + 'paddingLeft': '0px', + }, }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - 'backgroundColor': '#39ad84', - }, - '& .MuiTreeItem-iconContainer svg': { - fontSize: '22px' - }, - '& .MuiTreeItem-label': { - paddingLeft: '0px', - }, - '& .MuiBox-root': { - padding: '0px', - }, - [`& .${treeItemClasses.label}`]: { - 'width': '100%', - 'whiteSpace': 'nowrap', - 'overflow': 'hidden', - 'textOverflow': 'ellipsis', - 'fontSize': '1em', - 'fontWeight': '500', - 'letterSpacing': '.3px', - 'marginRight': '12px', - 'paddingLeft': '0px', - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: '12px', - } + [`& .${treeItemClasses.group}`]: { + marginLeft: '12px', + } })); export const StyledSubCategoryTreeItem = styled(TreeItem)(() => ({ - [`& .${treeItemClasses.content}`]: { - 'flexDirection': 'row-reverse', - 'backgroundColor': '#f0f0f0', - 'borderRadius': '2px', - 'marginBottom': '6px', - 'height': '32px', - 'paddingLeft': '8px', - 'transition': 'all .2s ease', - 'textDecoration': 'none', - 'cursor': 'pointer', - 'fontFamily': 'Roboto,sans-serif', - '&:hover': { - 'backgroundColor': '#f0f0f0', - }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - 'backgroundColor': '#f0f0f0', + [`& .${treeItemClasses.content}`]: { + 'flexDirection': 'row-reverse', + 'backgroundColor': '#f0f0f0', + 'borderRadius': '2px', + 'marginBottom': '6px', + 'height': '32px', + 'paddingLeft': '8px', + 'transition': 'all .2s ease', + 'textDecoration': 'none', + 'cursor': 'pointer', + 'fontFamily': 'Roboto,sans-serif', + '&:hover': { + 'backgroundColor': '#f0f0f0', + }, + '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { + 'backgroundColor': '#f0f0f0', + }, + '& .MuiTreeItem-iconContainer svg': { + fontSize: '18px', + color: '#666' + }, + '& .MuiTreeItem-label': { + paddingLeft: '0px', + }, + '& .MuiBox-root': { + padding: '0px', + }, + [`& .${treeItemClasses.label}`]: { + 'width': '100%', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'fontSize': '.85em', + 'fontWeight': '500', + 'letterSpacing': '.3px', + 'marginRight': '12px', + 'paddingLeft': '0px', + 'color': '#666' + }, }, - '& .MuiTreeItem-iconContainer svg': { - fontSize: '18px', - color: '#666' - }, - '& .MuiTreeItem-label': { - paddingLeft: '0px', - }, - '& .MuiBox-root': { - padding: '0px', - }, - [`& .${treeItemClasses.label}`]: { - 'width': '100%', - 'whiteSpace': 'nowrap', - 'overflow': 'hidden', - 'textOverflow': 'ellipsis', - 'fontSize': '.85em', - 'fontWeight': '500', - 'letterSpacing': '.3px', - 'marginRight': '12px', - 'paddingLeft': '0px', - 'color': '#666' - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: '12px', - } + [`& .${treeItemClasses.group}`]: { + marginLeft: '12px', + } })); export const StyledSubindicatorTreeItem = styled(TreeItem)(() => ({ - [`& .${treeItemClasses.content}`]: { - 'flexDirection': 'row-reverse', - 'backgroundColor': '#f0f0f0', - 'borderRadius': '2px', - 'marginBottom': '6px', - 'height': '32px', - 'paddingLeft': '8px', - 'transition': 'all .2s ease', - 'textDecoration': 'none', - 'cursor': 'pointer', - 'fontFamily': 'Roboto,sans-serif', - '&:hover': { - 'backgroundColor': '#dad7d7', - }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - 'backgroundColor': '#dad7d7', - }, - '& .MuiTreeItem-iconContainer svg': { - fontSize: '18px', - color: '#666' + [`& .${treeItemClasses.content}`]: { + 'flexDirection': 'row-reverse', + 'backgroundColor': '#f0f0f0', + 'borderRadius': '2px', + 'marginBottom': '6px', + 'height': '32px', + 'paddingLeft': '8px', + 'transition': 'all .2s ease', + 'textDecoration': 'none', + 'cursor': 'pointer', + 'fontFamily': 'Roboto,sans-serif', + '&:hover': { + 'backgroundColor': '#dad7d7', + }, + '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { + 'backgroundColor': '#dad7d7', + }, + '& .MuiTreeItem-iconContainer svg': { + fontSize: '18px', + color: '#666' + }, + '& .MuiTreeItem-label': { + paddingLeft: '0px', + }, + '& .MuiBox-root': { + padding: '0px', + }, + '&::after': { + position: 'absolute', + 'left': '24px', + 'zIndex': '1', + 'width': '8px', + 'height': '1px', + marginLeft: '4px', + backgroundColor: 'rgba(0, 0, 0, 0.1)', + content: '""' + }, + [`& .${treeItemClasses.label}`]: { + 'width': '100%', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'fontSize': '.85em', + 'fontWeight': '500', + 'letterSpacing': '.3px', + 'marginRight': '12px', + 'paddingLeft': '0px', + 'color': '#666' + }, }, - '& .MuiTreeItem-label': { - paddingLeft: '0px', - }, - '& .MuiBox-root': { - padding: '0px', - }, - '&::after': { - position: 'absolute', - 'left': '24px', - 'zIndex': '1', - 'width': '8px', - 'height': '1px', - marginLeft: '4px', - backgroundColor: 'rgba(0, 0, 0, 0.1)', - content: '""' - }, - [`& .${treeItemClasses.label}`]: { - 'width': '100%', - 'whiteSpace': 'nowrap', - 'overflow': 'hidden', - 'textOverflow': 'ellipsis', - 'fontSize': '.85em', - 'fontWeight': '500', - 'letterSpacing': '.3px', - 'marginRight': '12px', - 'paddingLeft': '0px', - 'color': '#666' - }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: '12px' - } + [`& .${treeItemClasses.group}`]: { + marginLeft: '12px' + } +})); + +export const StyledNoSubindicatorTreeItem = styled(StyledSubindicatorTreeItem)(() => ({ + [`& .${treeItemClasses.content}`]: { + 'height': '56px', + 'cursor': 'unset', + '&:hover': { + 'backgroundColor': '#f0f0f0', + }, + '&::after': { + backgroundColor: 'unset', + }, + [`& .${treeItemClasses.label}`]: { + 'whiteSpace': 'normal', + } + } })); export const StyledIndicatorTreeItem = styled(TreeItem)(() => ({ - [`& .${treeItemClasses.content}`]: { - 'flexDirection': 'row-reverse', - 'backgroundColor': '#f0f0f0', - 'borderRadius': '2px', - 'marginBottom': '6px', - 'height': '32px', - 'paddingLeft': '8px', - 'transition': 'all .2s ease', - 'textDecoration': 'none', - 'cursor': 'pointer', - 'fontFamily': 'Roboto,sans-serif', - '&:hover': { - 'backgroundColor': '#f0f0f0', - }, - '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { - 'backgroundColor': '#f0f0f0', - }, - '& .MuiTreeItem-iconContainer svg': { - fontSize: '18px', - color: '#666' - }, - '& .MuiTreeItem-label': { - paddingLeft: '0px', - }, - '& .MuiBox-root': { - padding: '0px', - }, - [`& .${treeItemClasses.label}`]: { - 'width': '100%', - 'whiteSpace': 'nowrap', - 'overflow': 'hidden', - 'textOverflow': 'ellipsis', - 'fontSize': '.85em', - 'fontWeight': '500', - 'letterSpacing': '.3px', - 'marginRight': '12px', - 'paddingLeft': '0px', - 'color': '#666' + [`& .${treeItemClasses.content}`]: { + 'flexDirection': 'row-reverse', + 'backgroundColor': '#f0f0f0', + 'borderRadius': '2px', + 'marginBottom': '6px', + 'height': '32px', + 'paddingLeft': '8px', + 'transition': 'all .2s ease', + 'textDecoration': 'none', + 'cursor': 'pointer', + 'fontFamily': 'Roboto,sans-serif', + '&:hover': { + 'backgroundColor': '#f0f0f0', + }, + '&.Mui-focused, &.Mui-selected, &.Mui-selected.Mui-focused': { + 'backgroundColor': '#f0f0f0', + }, + '& .MuiTreeItem-iconContainer svg': { + fontSize: '18px', + color: '#666' + }, + '& .MuiTreeItem-label': { + paddingLeft: '0px', + }, + '& .MuiBox-root': { + padding: '0px', + }, + [`& .${treeItemClasses.label}`]: { + 'width': '100%', + 'whiteSpace': 'nowrap', + 'overflow': 'hidden', + 'textOverflow': 'ellipsis', + 'fontSize': '.85em', + 'fontWeight': '500', + 'letterSpacing': '.3px', + 'marginRight': '12px', + 'paddingLeft': '0px', + 'color': '#666' + }, }, - }, - [`& .${treeItemClasses.group}`]: { - marginLeft: '12px', - borderLeft: '1px solid rgba(0, 0, 0, 0.11)', - marginLeft: '0px', - paddingLeft: '11px', - marginLeft: '3px', - } + [`& .${treeItemClasses.group}`]: { + marginLeft: '12px', + borderLeft: '1px solid rgba(0, 0, 0, 0.11)', + marginLeft: '0px', + paddingLeft: '11px', + marginLeft: '3px', + } })); -export const ParentContainer = styled(Box)(({ theme }) => ({ - display: 'grid', - gridAutoColumns: '1fr', - borderRadius: 2, - backgroundColor: '#f0f0f0', - padding: '6px', - marginTop: '20px', - paddingLeft: '0px', +export const ParentContainer = styled(Box)(({theme}) => ({ + display: 'grid', + gridAutoColumns: '1fr', + borderRadius: 2, + backgroundColor: '#f0f0f0', + padding: '6px', + marginTop: '20px', + paddingLeft: '0px', })); -export const IconContainer = styled(Box)(({ theme }) => ({ - gridRow: '1', - gridColumn: '1/2' +export const IconContainer = styled(Box)(({theme}) => ({ + gridRow: '1', + gridColumn: '1/2' })); -export const TextContainer = styled(Box)(({ theme }) => ({ - gridRow: '1', - gridColumn: 'span 5', - '& p': { - color: '#666', - fontSize: '0.9em', - fontWeight: '500', - lineHeight: '20px', - marginBottom: '0' - } +export const TextContainer = styled(Box)(({theme}) => ({ + gridRow: '1', + gridColumn: 'span 5', + '& p': { + color: '#666', + fontSize: '0.9em', + fontWeight: '500', + lineHeight: '20px', + marginBottom: '0' + } })); -export const Link = styled('a')(({ theme }) => ({ - color: '#39ad84' +export const Link = styled('a')(({theme}) => ({ + color: '#39ad84' })); const PanelIcon = (props) => { - return ( - - - - ); + return ( + + + + ); } -export const PanelIconSidebar = styled(PanelIcon)(({ theme }) => ({ - fontSize: '30px', - margin: 'auto', - display: 'block', - marginTop: '5px', +export const PanelIconSidebar = styled(PanelIcon)(({theme}) => ({ + fontSize: '30px', + margin: 'auto', + display: 'block', + marginTop: '5px', })); -export const PanelIconLink = styled(PanelIcon)(({ theme }) => ({ - fontSize: '18px', - verticalAlign: 'bottom', - color: '#39ad84' +export const PanelIconLink = styled(PanelIcon)(({theme}) => ({ + fontSize: '18px', + verticalAlign: 'bottom', + color: '#39ad84' })); From f9915aaa6b8e5e538ef0581b8182cd64ed5fa8e1 Mon Sep 17 00:00:00 2001 From: Gaurav Goyal Date: Mon, 17 Jul 2023 13:54:37 +0530 Subject: [PATCH 10/10] Fixed issue in failing test due to whitelisting of view data --- .../profile_default_filters/profile.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/__tests__/gui/filter_comparison_snackbar/profile_default_filters/profile.json b/__tests__/gui/filter_comparison_snackbar/profile_default_filters/profile.json index ececd2fe1..f66e305a4 100644 --- a/__tests__/gui/filter_comparison_snackbar/profile_default_filters/profile.json +++ b/__tests__/gui/filter_comparison_snackbar/profile_default_filters/profile.json @@ -35,7 +35,8 @@ "age": [ "15-35 (ZA)", "15-24 (Intl)", - "30-35" + "30-35", + "15-19", ], "race": [ "Coloured",