From b9a5cc1d7fce46d47d1120235b752d5a9fda0413 Mon Sep 17 00:00:00 2001 From: Mozafar Date: Tue, 5 Mar 2024 17:21:28 +0000 Subject: [PATCH] feat(DHIS2-16132): add ability to transpose/pivot a section form (#2802) * feat(DHIS2-16132): add ability to transpose/pivot a section form * refactor: apply code review comments --- src/EditModel/SectionDialog.component.js | 404 +++++++++++++++++------ src/i18n/i18n_module_en.properties | 5 + 2 files changed, 307 insertions(+), 102 deletions(-) diff --git a/src/EditModel/SectionDialog.component.js b/src/EditModel/SectionDialog.component.js index d4e21165c..c595e01c5 100644 --- a/src/EditModel/SectionDialog.component.js +++ b/src/EditModel/SectionDialog.component.js @@ -5,6 +5,7 @@ import Dialog from 'material-ui/Dialog/Dialog'; import FlatButton from 'material-ui/FlatButton/FlatButton'; import RaisedButton from 'material-ui/RaisedButton/RaisedButton'; import TextField from 'material-ui/TextField/TextField'; +import {RadioButton, RadioButtonGroup} from 'material-ui/RadioButton'; import GroupEditor from 'd2-ui/lib/group-editor/GroupEditorWithOrdering.component'; import Store from 'd2-ui/lib/store/Store'; @@ -14,6 +15,8 @@ import DropDown from '../forms/form-fields/drop-down'; import snackActions from '../Snackbar/snack.actions'; import modelToEditStore from './modelToEditStore'; +import SelectField from 'material-ui/SelectField'; +import { MenuItem } from 'material-ui/DropDownMenu'; const dataElementStore = Store.create(); const assignedDataElementStore = Store.create(); @@ -26,72 +29,122 @@ class SectionDialog extends React.Component { this.state = { categoryCombo: false, - disableDataElementAutoGroup: false + disableDataElementAutoGroup: false, + displayOptions: { + pivotMode: null, + pivotedCategory: null, + }, + categories: [], }; dataElementStore.setState([]); assignedDataElementStore.setState([]); indicatorStore.setState([]); assignedIndicatorStore.setState([]); - this.getTranslation = context.d2.i18n.getTranslation.bind(context.d2.i18n); + this.getTranslation = context.d2.i18n.getTranslation.bind( + context.d2.i18n + ); } componentDidMount() { this.subscriptions = []; - this.subscriptions.push(assignedDataElementStore.subscribe(() => { - this.forceUpdate(); - })); + this.subscriptions.push( + assignedDataElementStore.subscribe(() => { + this.forceUpdate(); + }) + ); + + const d2 = this.context.d2; + const categoryCombos = this.props.categoryCombos + .map(coc => coc.value) + .join(','); + + return d2.models.categories + .list({ + filter: `categoryCombos.id:in:[${categoryCombos}]`, + paging: false, + fields: [ + 'id,displayName', + 'categoryCombos[id]', + ].join(','), + }) + .then(response => { + const categories = response.toArray(); + + this.setState({ + categories, + }); + }).catch(err => { + snackActions.show({ message: 'Something went wrong.' + err, action: 'ok' }); + }) } componentWillReceiveProps(props) { if (props.sectionModel) { const currentSectionId = props.sectionModel.id; const sections = modelToEditStore.state.sections; - const sectionArray = Array.isArray(sections) ? sections : sections.toArray(); - const otherSections = sectionArray.filter(s => s.id !== currentSectionId); - const filterDataElementIds = otherSections - .reduce((elements, section) => elements.concat((Array.isArray(section.dataElements) - ? section.dataElements - : section.dataElements.toArray() - ).map(de => de.id)), []); + const sectionArray = Array.isArray(sections) + ? sections + : sections.toArray(); + const otherSections = sectionArray.filter( + s => s.id !== currentSectionId + ); + const filterDataElementIds = otherSections.reduce( + (elements, section) => + elements.concat( + (Array.isArray(section.dataElements) + ? section.dataElements + : section.dataElements.toArray() + ).map(de => de.id) + ), + [] + ); // Default category combo filter = no filter const categoryComboId = false; - assignedDataElementStore.setState( props.sectionModel.dataElements ? props.sectionModel.dataElements.toArray().map(de => de.id) - : [], + : [] ); indicatorStore.setState( modelToEditStore.state.indicators .toArray() .map(i => ({ value: i.id, text: i.displayName })) - .sort((a, b) => a.text.localeCompare(b.text)), + .sort((a, b) => a.text.localeCompare(b.text)) ); assignedIndicatorStore.setState( props.sectionModel.indicators ? props.sectionModel.indicators.toArray().map(i => i.id) - : [], + : [] ); - this.setState({ - name: props.sectionModel.name, - code: props.sectionModel.code, - nameError: '', - codeError: '', - description: props.sectionModel.description, - showRowTotals: props.sectionModel.showRowTotals, - showColumnTotals: props.sectionModel.showColumnTotals, - disableDataElementAutoGroup: props.sectionModel.disableDataElementAutoGroup, - filterText: '', - filterDataElementIds, - }, () => { - this.handleCategoryComboChange({ target: { value: categoryComboId } }); - this.forceUpdate(); - }); + this.setState( + { + name: props.sectionModel.name, + code: props.sectionModel.code, + nameError: '', + codeError: '', + description: props.sectionModel.description, + showRowTotals: props.sectionModel.showRowTotals, + showColumnTotals: props.sectionModel.showColumnTotals, + displayOptions: props.sectionModel.displayOptions + ? JSON.parse(props.sectionModel.displayOptions) + : {}, + disableDataElementAutoGroup: + props.sectionModel.disableDataElementAutoGroup, + filterText: '', + filterDataElementIds, + }, + () => { + this.handleCategoryComboChange({ + target: { value: categoryComboId }, + }); + this.forceUpdate(); + } + ); } } @@ -99,41 +152,47 @@ class SectionDialog extends React.Component { this.subscriptions.forEach(disposable => disposable.unsubscribe()); } - setAssignedDataElements = (dataElements) => { + setAssignedDataElements = dataElements => { assignedDataElementStore.setState(dataElements); - } + }; - setAssignedIndicators = (indicators) => { + setAssignedIndicators = indicators => { assignedIndicatorStore.setState(indicators); - } + }; - removeIndicators = (indicators) => { - assignedIndicatorStore.setState(assignedIndicatorStore.state.filter(i => indicators.indexOf(i) === -1)); + removeIndicators = indicators => { + assignedIndicatorStore.setState( + assignedIndicatorStore.state.filter( + i => indicators.indexOf(i) === -1 + ) + ); return Promise.resolve(); - } + }; - assignIndicators = (indicators) => { - assignedIndicatorStore.setState(assignedIndicatorStore.state.concat(indicators)); + assignIndicators = indicators => { + assignedIndicatorStore.setState( + assignedIndicatorStore.state.concat(indicators) + ); return Promise.resolve(); - } + }; handleRowTotalsChange = (e, value) => { this.setState({ showRowTotals: value }); - } + }; handleColumnTotalsChange = (e, value) => { this.setState({ showColumnTotals: value }); - } + }; handleDisableDataElementAutoGroupChange = (e, value) => { - this.setState({ disableDataElementAutoGroup: value }) - } + this.setState({ disableDataElementAutoGroup: value }); + }; - handleFilterChange = (e) => { + handleFilterChange = e => { this.setState({ filterText: e.target.value }); - } + }; - handleNameChange = (e) => { + handleNameChange = e => { const sectionArray = Array.isArray(modelToEditStore.getState().sections) ? modelToEditStore.getState().sections : modelToEditStore.getState().sections.toArray(); @@ -141,61 +200,84 @@ class SectionDialog extends React.Component { .filter(s => s.id !== this.props.sectionModel.id) .reduce((res, s) => res || s.name === e.target.value, false); - this.setState({ name: e.target.value, nameError: nameDupe ? this.getTranslation('value_not_unique') : '' }); - } + this.setState({ + name: e.target.value, + nameError: nameDupe ? this.getTranslation('value_not_unique') : '', + }); + }; - handleCodeChange = (e) => { + handleCodeChange = e => { const sectionArray = Array.isArray(modelToEditStore.getState().sections) ? modelToEditStore.getState().sections : modelToEditStore.getState().sections.toArray(); const codeDupe = sectionArray .filter(s => s.id !== this.props.sectionModel.id) - .reduce((res, s) => res || (s.code && s.code === e.target.value), false); + .reduce( + (res, s) => res || (s.code && s.code === e.target.value), + false + ); - this.setState({ code: e.target.value, codeError: codeDupe ? this.getTranslation('value_not_unique') : '' }); - } + this.setState({ + code: e.target.value, + codeError: codeDupe ? this.getTranslation('value_not_unique') : '', + }); + }; - handleDescriptionChange = (e) => { + handleDescriptionChange = e => { this.setState({ description: e.target.value }); - } + }; - assignDataElements = (dataElements) => { - assignedDataElementStore.setState(assignedDataElementStore.state.concat(dataElements)); + assignDataElements = dataElements => { + assignedDataElementStore.setState( + assignedDataElementStore.state.concat(dataElements) + ); return Promise.resolve(); - } + }; - removeDataElements = (dataElements) => { - assignedDataElementStore.setState(assignedDataElementStore.state.filter(de => dataElements.indexOf(de) === -1)); + removeDataElements = dataElements => { + assignedDataElementStore.setState( + assignedDataElementStore.state.filter( + de => dataElements.indexOf(de) === -1 + ) + ); return Promise.resolve(); - } + }; - handleCategoryComboChange = (event) => { + handleCategoryComboChange = event => { const categoryComboId = event.target.value; if (modelToEditStore.state.dataSetElements) { dataElementStore.setState( modelToEditStore.state.dataSetElements - .filter((dse) => { + .filter(dse => { if (categoryComboId) { return dse.categoryCombo ? dse.categoryCombo.id === categoryComboId - : dse.dataElement.categoryCombo.id === categoryComboId; + : dse.dataElement.categoryCombo.id === + categoryComboId; } return true; }) - .filter(dse => (this.state.filterDataElementIds - ? !this.state.filterDataElementIds.includes(dse.dataElement.id) - : true), + .filter( + dse => + this.state.filterDataElementIds + ? !this.state.filterDataElementIds.includes( + dse.dataElement.id + ) + : true ) - .map(dse => ({ value: dse.dataElement.id, text: dse.dataElement.displayName })) - .sort((a, b) => a.text.localeCompare(b.text)), + .map(dse => ({ + value: dse.dataElement.id, + text: dse.dataElement.displayName, + })) + .sort((a, b) => a.text.localeCompare(b.text)) ); } this.setState({ categoryCombo: categoryComboId, }); - } + }; saveSection = () => { if (!this.state.name || this.state.name.trim().length === 0) { @@ -216,40 +298,136 @@ class SectionDialog extends React.Component { description: this.state.description, showRowTotals: this.state.showRowTotals, showColumnTotals: this.state.showColumnTotals, + displayOptions: JSON.stringify(this.state.displayOptions), disableDataElementAutoGroup: this.state.disableDataElementAutoGroup, - dataElements: assignedDataElementStore.state.map(de => ({ id: de })), + dataElements: assignedDataElementStore.state.map(de => ({ + id: de, + })), indicators: assignedIndicatorStore.state.map(i => ({ id: i })), - sortOrder: this.props.sectionModel.sortOrder || modelToEditStore - .state - .sections - .toArray() - .reduce((prev, s) => Math.max(prev, s.sortOrder + 1), 0), + sortOrder: + this.props.sectionModel.sortOrder || + modelToEditStore.state.sections + .toArray() + .reduce((prev, s) => Math.max(prev, s.sortOrder + 1), 0), }); - sectionModel.save() - .then((res) => { - snackActions.show({ message: this.getTranslation('section_saved') }); - this.context.d2.models.sections.get(res.response.uid, { - fields: [ - ':all,dataElements[id,categoryCombo[id,displayName]]', - 'greyedFields[categoryOptionCombo,dataElement]', - ].join(','), - }) - .then((section) => { + sectionModel + .save() + .then(res => { + snackActions.show({ + message: this.getTranslation('section_saved'), + }); + this.context.d2.models.sections + .get(res.response.uid, { + fields: [ + ':all,dataElements[id,categoryCombo[id,displayName]]', + 'greyedFields[categoryOptionCombo,dataElement]', + ].join(','), + }) + .then(section => { this.props.onSaveSection(section); }); }) - .catch((err) => { + .catch(err => { log.warn('Failed to save section:', err); snackActions.show({ message: this.getTranslation('failed_to_save_section'), action: this.getTranslation('ok'), }); }); - } + }; + + handleChoosePivotMode = (e, pivotMode) => { + const [firstCategory] = this.state.categories || []; + const categoryId = firstCategory ? firstCategory.id : null; + const pivotedCategory = + pivotMode === 'move_categories' ? categoryId : null; + this.setState({ + displayOptions: { + ...this.state.displayOptions, + pivotMode, + pivotedCategory, + }, + }); + }; + + handlePivotCategoryChoice = (event, i, pivotedCategoryValue) => { + this.setState({ + displayOptions: { + ...this.state.displayOptions, + pivotedCategory: pivotedCategoryValue, + }, + }); + }; + + renderSectionDisplayConfigurationOptions = () => { + const pivotOptions = [ + { + value: 'n/a', + text: this.getTranslation('pivot_default'), + }, + { + value: 'pivot', + text: this.getTranslation('pivot_move_data_elements_to_column'), + }, + { + value: 'move_categories', + text: this.getTranslation('pivot_move_category_to_row'), + }, + ]; + const isMoveCategoryMode = + this.state.displayOptions.pivotMode === 'move_categories'; + + return ( +
+ + + {pivotOptions.map(({ value, text }) => { + return ( + + ); + })} + + {isMoveCategoryMode && ( + + {this.state.categories.map(cat => { + return ( + + ); + })} + + )} +
+ ); + }; renderFilters = () => { - const catCombos = [{ value: false, text: this.getTranslation('no_filter') }] - .concat(this.props.categoryCombos.sort((a, b) => a.text.localeCompare(b.text))); + const catCombos = [ + { value: false, text: this.getTranslation('no_filter') }, + ].concat( + this.props.categoryCombos.sort((a, b) => + a.text.localeCompare(b.text) + ) + ); return (
@@ -264,13 +442,15 @@ class SectionDialog extends React.Component { />
); - } + }; renderAvailableOptions = () => { const labelStyle = { @@ -290,7 +470,9 @@ class SectionDialog extends React.Component { return (
- + {indicatorStore.state.length ? (
- + ); - } + }; render() { let title = this.getTranslation('add_section'); @@ -326,15 +510,28 @@ class SectionDialog extends React.Component { if (this.props.sectionModel.id) { title = this.getTranslation('edit_section'); sectionIdDiv = ( -
+
{this.getTranslation('section_id')}: - {this.props.sectionModel.id} + + {this.props.sectionModel.id} +
); } - const validateName = (e) => { - this.setState({ nameError: e.target.value.trim().length > 0 ? '' : this.getTranslation('value_required') }); + const validateName = e => { + this.setState({ + nameError: + e.target.value.trim().length > 0 + ? '' + : this.getTranslation('value_required'), + }); }; return ( @@ -390,14 +587,17 @@ class SectionDialog extends React.Component { style={{ margin: '16px 0' }} onCheck={this.handleColumnTotalsChange} /> - {this.renderFilters()} {this.renderAvailableOptions()} + {this.renderSectionDisplayConfigurationOptions()} ); } diff --git a/src/i18n/i18n_module_en.properties b/src/i18n/i18n_module_en.properties index 265a1ef97..dab7a881e 100644 --- a/src/i18n/i18n_module_en.properties +++ b/src/i18n/i18n_module_en.properties @@ -1779,6 +1779,11 @@ att_opt_combo=Complete registration att opt combo open_user_guide=Open user guide section_id=Section ID show_row_totals=Show row totals +pivot_options=Pivot options +pivot_default=Default: data elements as rows, categories as columns +pivot_move_data_elements_to_column=Pivot: categories as rows, data elements as columns +pivot_move_category_to_row=Move a category to rows: default mode with one category moved to rows +pivot_category_to_move_to_row=Category to move to rows show_column_totals=Show column totals category_combo_filter=Category combo filter no_filter=