From da2c1bdc2c29bbbd7d78d852639d478e5c969bef Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Thu, 4 Mar 2021 09:48:30 +0100 Subject: [PATCH 01/30] download empty user template --- src/List/List.component.js | 3 +- src/components/ImportExport.component.js | 34 ++++++++++++++++++-- src/components/MultipleSelector.component.js | 4 +-- src/i18n/i18n_module_ar.properties | 1 + src/i18n/i18n_module_en.properties | 1 + src/i18n/i18n_module_es.properties | 1 + src/i18n/i18n_module_fr.properties | 1 + src/models/userHelpers.js | 1 - 8 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/List/List.component.js b/src/List/List.component.js index f93b9b2..d6e1312 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -91,7 +91,7 @@ const List = React.createClass({ mixins: [ObserverRegistry, Translate, Auth], - maxImportUsers: 200, + maxImportUsers: 1000, styles: { dataTableWrap: { @@ -508,6 +508,7 @@ const List = React.createClass({ { + const { allColumns } = this.props; + this.setState({ isProcessing: true }); + + try { + const labeledColumns = allColumns.map(column => column.text); + const blob = new Blob([labeledColumns], { type: "text/plain;charset=utf-8" }); + const datetime = moment().format("YYYY-MM-DD_HH-mm-ss"); + const filename = `empty-user-template-${datetime}.csv`; + FileSaver.saveAs(blob, filename); + snackActions.show({ message: `${this.t("table_exported")}: ${filename}` }); + } finally { + this.closeMenu(); + this.setState({ isProcessing: false }); + } + }; + importFromCsv = () => { const { onImport, maxUsers, settings } = this.props; const orgUnitsField = settings.get("organisationUnitsField"); @@ -95,7 +112,13 @@ class ImportExport extends React.Component { render() { const { d2 } = this.props; const { isMenuOpen, anchorEl, isProcessing } = this.state; - const { popoverConfig, closeMenu, importFromCsv, exportToCsvAndSave } = this; + const { + popoverConfig, + closeMenu, + importFromCsv, + exportToCsvAndSave, + exportEmptyTemplate, + } = this; const { t } = this; return ( @@ -119,15 +142,20 @@ class ImportExport extends React.Component { > } + leftIcon={} primaryText={t("export")} onClick={exportToCsvAndSave} /> } + leftIcon={} primaryText={t("import")} onClick={importFromCsv} /> + } + primaryText={t("export_empty_template")} + onClick={exportEmptyTemplate} + /> diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index be48081..3a076ec 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -81,7 +81,7 @@ class MultipleSelector extends React.Component { organisationUnitsCapture: "assignToOrgUnitsCapture", dataViewOrganisationUnits: "assignToOrgUnitsOutput", }; - + //organisationUnitsCapture renderForm() { const { field, options, orgUnitRoots } = this.props; const { selected } = this.state; @@ -102,7 +102,7 @@ class MultipleSelector extends React.Component { onChange={this.onMultiSelectChange} /> ); - case "organisationUnitsCapture": + case "organisationUnits": case "dataViewOrganisationUnits": return ( (_(["TEXT", "DATE", "URL"]).includes(value.type) ? key : null)) .compact() From f3988a4bf8534a21884c4e11b9660d46cd37046b Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Thu, 4 Mar 2021 09:57:20 +0100 Subject: [PATCH 02/30] prettify and fix some small things --- src/List/List.component.js | 2 +- src/components/ImportExport.component.js | 2 +- src/components/MultipleSelector.component.js | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/List/List.component.js b/src/List/List.component.js index d6e1312..ddda98e 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -91,7 +91,7 @@ const List = React.createClass({ mixins: [ObserverRegistry, Translate, Auth], - maxImportUsers: 1000, + maxImportUsers: 200, styles: { dataTableWrap: { diff --git a/src/components/ImportExport.component.js b/src/components/ImportExport.component.js index 28640c2..4767b8c 100644 --- a/src/components/ImportExport.component.js +++ b/src/components/ImportExport.component.js @@ -75,7 +75,7 @@ class ImportExport extends React.Component { } }; - exportEmptyTemplate = async () => { + exportEmptyTemplate = () => { const { allColumns } = this.props; this.setState({ isProcessing: true }); diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index 3a076ec..18656a5 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -81,7 +81,6 @@ class MultipleSelector extends React.Component { organisationUnitsCapture: "assignToOrgUnitsCapture", dataViewOrganisationUnits: "assignToOrgUnitsOutput", }; - //organisationUnitsCapture renderForm() { const { field, options, orgUnitRoots } = this.props; const { selected } = this.state; @@ -102,7 +101,7 @@ class MultipleSelector extends React.Component { onChange={this.onMultiSelectChange} /> ); - case "organisationUnits": + case "organisationUnitsCapture": case "dataViewOrganisationUnits": return ( Date: Fri, 5 Mar 2021 10:32:31 +0100 Subject: [PATCH 03/30] Abstract ImportExport.saveCsv --- src/components/ImportExport.component.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/ImportExport.component.js b/src/components/ImportExport.component.js index 4767b8c..f027988 100644 --- a/src/components/ImportExport.component.js +++ b/src/components/ImportExport.component.js @@ -64,11 +64,7 @@ class ImportExport extends React.Component { try { const csvString = await exportToCsv(d2, columns, filterOptions, { orgUnitsField }); - const blob = new Blob([csvString], { type: "text/plain;charset=utf-8" }); - const datetime = moment().format("YYYY-MM-DD_HH-mm-ss"); - const filename = `users-${datetime}.csv`; - FileSaver.saveAs(blob, filename); - snackActions.show({ message: `${this.t("table_exported")}: ${filename}` }); + this.saveCsv(csvString, "users"); } finally { this.closeMenu(); this.setState({ isProcessing: false }); @@ -81,17 +77,21 @@ class ImportExport extends React.Component { try { const labeledColumns = allColumns.map(column => column.text); - const blob = new Blob([labeledColumns], { type: "text/plain;charset=utf-8" }); - const datetime = moment().format("YYYY-MM-DD_HH-mm-ss"); - const filename = `empty-user-template-${datetime}.csv`; - FileSaver.saveAs(blob, filename); - snackActions.show({ message: `${this.t("table_exported")}: ${filename}` }); + this.saveCsv(labeledColumns, "empty-user-template"); } finally { this.closeMenu(); this.setState({ isProcessing: false }); } }; + saveCsv = (contents, name) => { + const blob = new Blob([contents], { type: "text/plain;charset=utf-8" }); + const datetime = moment().format("YYYY-MM-DD_HH-mm-ss"); + const filename = `${name}-${datetime}.csv`; + FileSaver.saveAs(blob, filename); + snackActions.show({ message: `${this.t("table_exported")}: ${filename}` }); + }; + importFromCsv = () => { const { onImport, maxUsers, settings } = this.props; const orgUnitsField = settings.get("organisationUnitsField"); From 82ba2bba1ab78f750ef42bf810ccc8f5edbc6df9 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 10:39:13 +0100 Subject: [PATCH 04/30] Reinstate blank lines --- src/components/MultipleSelector.component.js | 1 + src/models/userHelpers.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index 18656a5..be48081 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -81,6 +81,7 @@ class MultipleSelector extends React.Component { organisationUnitsCapture: "assignToOrgUnitsCapture", dataViewOrganisationUnits: "assignToOrgUnitsOutput", }; + renderForm() { const { field, options, orgUnitRoots } = this.props; const { selected } = this.state; diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 04a1298..e9de25d 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -266,6 +266,7 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { : _(csv.data) .drop(1) .value(); + const plainUserAttributes = _(d2.models.users.modelValidations) .map((value, key) => (_(["TEXT", "DATE", "URL"]).includes(value.type) ? key : null)) .compact() From 30701d394205d2f2b73ea4f3fcd4e4c7dbd90b57 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 5 Mar 2021 10:43:52 +0100 Subject: [PATCH 05/30] Add ImportExport prop allColumns --- src/components/ImportExport.component.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/ImportExport.component.js b/src/components/ImportExport.component.js index f027988..6e32543 100644 --- a/src/components/ImportExport.component.js +++ b/src/components/ImportExport.component.js @@ -23,6 +23,12 @@ class ImportExport extends React.Component { onImport: PropTypes.func.isRequired, maxUsers: PropTypes.number.isRequired, settings: PropTypes.object.isRequired, + allColumns: PropTypes.arrayOf( + React.PropTypes.shape({ + text: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }) + ).isRequired, }; state = { isMenuOpen: false, anchorEl: null, isProcessing: false }; From 18c71c49f48644bd4e329afae384164d611182a7 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 5 Mar 2021 11:04:07 +0100 Subject: [PATCH 06/30] add disable column in spreadsheet --- src/List/Filters.component.js | 2 +- src/List/List.component.js | 2 +- src/models/userHelpers.js | 25 ++++++++++++++----------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index a668184..c8a11a2 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -118,7 +118,7 @@ export default class Filters extends React.Component { } = this.state; const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); - + console.log(inFilter(userRoles)); return { ...(showOnlyManagedUsers ? { canManage: "true" } : {}), ...(searchString ? { query: searchString } : {}), diff --git a/src/List/List.component.js b/src/List/List.component.js index f93b9b2..d2b6ad5 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -91,7 +91,7 @@ const List = React.createClass({ mixins: [ObserverRegistry, Translate, Auth], - maxImportUsers: 200, + maxImportUsers: 250, styles: { dataTableWrap: { diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index e9de25d..731e25c 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -43,9 +43,7 @@ const requiredPropertiesOnImport = ["username", "password", "firstName", "surnam const propertiesIgnoredOnImport = ["id", "created", "lastUpdated", "lastLogin"]; -const columnsIgnoredOnExport = ["disabled"]; - -const userCredentialsFields = ["username", "password", "userRoles"]; +const userCredentialsFields = ["username", "password", "userRoles", "disabled"]; const columnNameFromPropertyMapping = { id: "ID", @@ -63,6 +61,7 @@ const columnNameFromPropertyMapping = { userGroups: "Groups", organisationUnits: "OUCapture", dataViewOrganisationUnits: "OUOutput", + disabled: "disabled", }; const propertyFromColumnNameMapping = _.invert(columnNameFromPropertyMapping); @@ -214,6 +213,7 @@ function getPlainUser(user, { orgUnitsField }) { user.dataViewOrganisationUnits, orgUnitsField ), + disabled: userCredentials.disabled, }; } @@ -266,7 +266,6 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { : _(csv.data) .drop(1) .value(); - const plainUserAttributes = _(d2.models.users.modelValidations) .map((value, key) => (_(["TEXT", "DATE", "URL"]).includes(value.type) ? key : null)) .compact() @@ -337,7 +336,14 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { const data = userRows.map((userRow, rowIndex) => getPlainUserFromRow(userRow, modelValuesByField, rowIndex + 2) ); - const users = data.map(o => o.user); + const trueOptions = ["TRUE", "true", "1"]; + const users = data.map(o => { + let newUser = o.user; + if (newUser.disabled) { + newUser.disabled = trueOptions.includes(newUser.disabled) ? true : false; + } + return newUser; + }); const userWarnings = _(data) .flatMap(o => o.warnings) .value(); @@ -510,6 +516,7 @@ async function updateUsers(d2, users, mapper) { async function getUserGroupsToSaveAndPostMetadata(api, users, existingUsersToUpdate) { const userGroupsToSave = await getUserGroupsToSave(api, users, existingUsersToUpdate); const payload = { users: users, userGroups: userGroupsToSave }; + console.log(payload); return postMetadata(api, payload); } @@ -558,7 +565,6 @@ function getList(d2, filters, listOptions) { .pickBy() .toPairs() .value(); - /* Filtering over nested fields (table[.table].field) in N-to-N relationships (for example: userCredentials.userRoles.id), fails in dhis2 < v2.30. So we need to make separate calls to the API for those filters and use the returned IDs to build @@ -611,13 +617,10 @@ function getList(d2, filters, listOptions) { async function exportToCsv(d2, columns, filterOptions, { orgUnitsField }) { const { filters, ...listOptions } = { ...filterOptions, paging: false }; const users = await getList(d2, filters, listOptions); - const columnsToExport = _(columns) - .without(...columnsIgnoredOnExport) - .value(); const userRows = users .toArray() - .map(user => _.at(getPlainUser(user, { orgUnitsField }), columnsToExport)); - const header = columnsToExport.map(getColumnNameFromProperty); + .map(user => _.at(getPlainUser(user, { orgUnitsField }), columns)); + const header = columns.map(getColumnNameFromProperty); const table = [header, ...userRows]; return Papa.unparse(table); From 0a68eaed52942a9bfb2c33ddafac43b017832045 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 5 Mar 2021 11:22:31 +0100 Subject: [PATCH 07/30] removed console.logs and add boolean converter --- src/List/Filters.component.js | 1 - src/List/List.component.js | 2 +- src/models/userHelpers.js | 4 +--- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index c8a11a2..01f54e5 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -118,7 +118,6 @@ export default class Filters extends React.Component { } = this.state; const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); - console.log(inFilter(userRoles)); return { ...(showOnlyManagedUsers ? { canManage: "true" } : {}), ...(searchString ? { query: searchString } : {}), diff --git a/src/List/List.component.js b/src/List/List.component.js index d2b6ad5..f93b9b2 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -91,7 +91,7 @@ const List = React.createClass({ mixins: [ObserverRegistry, Translate, Auth], - maxImportUsers: 250, + maxImportUsers: 200, styles: { dataTableWrap: { diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 731e25c..4d70019 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -336,11 +336,10 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { const data = userRows.map((userRow, rowIndex) => getPlainUserFromRow(userRow, modelValuesByField, rowIndex + 2) ); - const trueOptions = ["TRUE", "true", "1"]; const users = data.map(o => { let newUser = o.user; if (newUser.disabled) { - newUser.disabled = trueOptions.includes(newUser.disabled) ? true : false; + newUser.disabled = Boolean(newUser.disabled); } return newUser; }); @@ -516,7 +515,6 @@ async function updateUsers(d2, users, mapper) { async function getUserGroupsToSaveAndPostMetadata(api, users, existingUsersToUpdate) { const userGroupsToSave = await getUserGroupsToSave(api, users, existingUsersToUpdate); const payload = { users: users, userGroups: userGroupsToSave }; - console.log(payload); return postMetadata(api, payload); } From 48e73e3e220ff4b4c84478fa47b22f8ff4bd71b5 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 14 May 2021 13:30:58 +0200 Subject: [PATCH 08/30] had OU capture and output be assigned in the right place, got the OU Capture and output user data to be inserted in teh replicate and see the assign option --- src/List/Filters.component.js | 2 +- src/List/List.component.js | 1 - src/List/context.actions.js | 17 ++++++------ .../OrgUnitDialog.component.js | 10 +++++++ src/components/CopyInUserDialog.component.js | 2 +- src/components/ImportTable.component.js | 3 +-- src/components/MultipleSelector.component.js | 23 ++++++++++++---- src/components/OrgUnitForm.js | 19 ++++++++++--- src/components/OrgUnitsFilter.component.js | 10 +++++++ .../ReplicateUserFromTable.component.js | 6 +++-- .../ReplicateUserFromTemplate.component.js | 2 +- src/components/UserGroupsDialog.component.js | 2 +- src/components/UserRolesDialog.component.js | 2 +- src/i18n/i18n_module_ar.properties | 18 +++++++------ src/i18n/i18n_module_en.properties | 21 ++++++++------- src/i18n/i18n_module_es.properties | 27 ++++++++++--------- src/i18n/i18n_module_fr.properties | 23 +++++++++------- 17 files changed, 122 insertions(+), 66 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index a668184..2f1b1b1 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -116,7 +116,7 @@ export default class Filters extends React.Component { orgUnits, orgUnitsOutput, } = this.state; - + console.log(orgUnits); const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); return { diff --git a/src/List/List.component.js b/src/List/List.component.js index f93b9b2..9dda192 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -496,7 +496,6 @@ const List = React.createClass({ .at(settings.get("visibleTableColumns")) .compact() .value(); - return (
diff --git a/src/List/context.actions.js b/src/List/context.actions.js index 9e44997..72653b9 100644 --- a/src/List/context.actions.js +++ b/src/List/context.actions.js @@ -98,36 +98,37 @@ const contextActions = [ primary: true, }, { - name: "copyInUser", + name: "copy_in_user", multiple: false, icon: "content_copy", onClick: user => copyInUserStore.setState({ user, open: true }), allowed: checkAccess(["update"]), }, { - name: "assignToOrgUnitsCapture", + name: "assign_to_org_units_capture", multiple: true, icon: "business", - onClick: users => assignToOrgUnits(users, "organisationUnits", "assignToOrgUnitsCapture"), + onClick: users => + assignToOrgUnits(users, "organisationUnits", "assign_to_org_units_capture"), allowed: checkAccess(["update"]), }, { - name: "assignToOrgUnitsOutput", + name: "assign_to_org_units_output", multiple: true, icon: "business", onClick: users => - assignToOrgUnits(users, "dataViewOrganisationUnits", "assignToOrgUnitsOutput"), + assignToOrgUnits(users, "dataViewOrganisationUnits", "assign_to_org_units_output"), allowed: checkAccess(["update"]), }, { - name: "assignRoles", + name: "assign_roles", multiple: true, icon: "assignment", onClick: users => userRolesAssignmentDialogStore.setState({ users, open: true }), allowed: checkAccess(["update"]), }, { - name: "assignGroups", + name: "assign_groups", icon: "group_add", multiple: true, onClick: users => userGroupsAssignmentDialogStore.setState({ users, open: true }), @@ -161,7 +162,7 @@ const contextActions = [ onClick: datasets => deleteUserStore.delete(datasets), }, { - name: "replicateUser", + name: "replicate_user", icon: "content_copy", multiple: false, allowed: hasReplicateAuthority, diff --git a/src/List/organisation-unit-dialog/OrgUnitDialog.component.js b/src/List/organisation-unit-dialog/OrgUnitDialog.component.js index 8200adc..4ad3e2f 100644 --- a/src/List/organisation-unit-dialog/OrgUnitDialog.component.js +++ b/src/List/organisation-unit-dialog/OrgUnitDialog.component.js @@ -144,6 +144,16 @@ class OrgUnitDialog extends React.Component { roots={this.props.roots} selected={this.state.selected} intersectionPolicy={true} + filteringByNameLabel={ + title.includes("organisation units capture") + ? "filter_organisation_units_capture_by_name" + : "filter_organisation_units_output_by_name" + } + orgUnitsSelectedLabel={ + title.includes("organisation units capture") + ? "organisation_units_capture_selected" + : "organisation_units_output_selected" + } /> ); diff --git a/src/components/CopyInUserDialog.component.js b/src/components/CopyInUserDialog.component.js index bbf3a20..5ed163e 100644 --- a/src/components/CopyInUserDialog.component.js +++ b/src/components/CopyInUserDialog.component.js @@ -8,7 +8,7 @@ import { getPayload } from "../models/userHelpers"; function getTitle(getTranslation, users) { const usernames = users && users.map(user => user.userCredentials.username); const info = usernames ? _m.joinString(getTranslation, usernames, 3, ", ") : "..."; - return getTranslation("copyInUser") + ": " + info; + return getTranslation("copy_in_user") + ": " + info; } function CopyInUserDialog(props, context) { diff --git a/src/components/ImportTable.component.js b/src/components/ImportTable.component.js index 9a3f131..2d61aff 100644 --- a/src/components/ImportTable.component.js +++ b/src/components/ImportTable.component.js @@ -438,7 +438,6 @@ class ImportTable extends React.Component { field === "organisationUnits" || field === "dataViewOrganisationUnits" ? orgUnitsField : "displayName"; - if (isMultipleValue) { const values = value || []; const compactValue = _(values).isEmpty() @@ -453,7 +452,7 @@ class ImportTable extends React.Component { .join(", "); const onClick = this.getOnTextFieldClicked(user.id, field); - return this.getTextField(field, compactValue, { + return this.getTextField(displayField, compactValue, { validators, component: props => ( ); - case "organisationUnitsCapture": + case "organisationUnits": + return ( + + ); case "dataViewOrganisationUnits": return ( ); default: diff --git a/src/components/OrgUnitForm.js b/src/components/OrgUnitForm.js index 26026c7..9b8b56d 100644 --- a/src/components/OrgUnitForm.js +++ b/src/components/OrgUnitForm.js @@ -19,6 +19,8 @@ class OrgUnitForm extends React.Component { searchValue: "", originalRoots: this.props.roots, rootOrgUnits: this.props.roots, + filteringByNameLabel: this.props.filteringByNameLabel, + orgUnitsSelectedLabel: this.props.orgUnitsSelectedLabel, groups: [], levels: [], loading: false, @@ -83,10 +85,12 @@ class OrgUnitForm extends React.Component { async onChange(orgUnitsPaths) { const { d2 } = this.context; const orgUnitIds = orgUnitsPaths.map(path => _.last(path.split("/"))); + console.log(orgUnitIds); const newSelected = await listWithInFilter(d2.models.organisationUnits, "id", orgUnitIds, { paging: false, fields: "id,displayName,shortName,path", }); + console.log(newSelected); this.props.onChange(newSelected); } @@ -131,7 +135,13 @@ class OrgUnitForm extends React.Component { return
this.context.d2.i18n.getTranslation('determining_your_root_orgunits')
; } - const { root, models, intersectionPolicy } = this.props; + const { + root, + models, + intersectionPolicy, + filteringByNameLabel, + orgUnitsSelectedLabel, + } = this.props; const styles = { wrapper: { position: "relative", @@ -160,7 +170,7 @@ class OrgUnitForm extends React.Component { }; const selectedPaths = this.props.selected.map(ou => ou.path); - + console.log(selectedPaths); return (
{this.state.loading ? ( @@ -174,7 +184,7 @@ class OrgUnitForm extends React.Component { this._searchOrganisationUnits(event.target.value)} floatingLabelText={this.context.d2.i18n.getTranslation( - "filter_organisation_units_capture_by_name" + `${filteringByNameLabel}` )} fullWidth /> @@ -201,7 +211,7 @@ class OrgUnitForm extends React.Component {
{`${this.props.selected.length} ${this.getTranslation( - "organisation_units_capture_selected" + `${orgUnitsSelectedLabel}` )}`}
{this.renderRoots()} @@ -215,6 +225,7 @@ OrgUnitForm.propTypes = { roots: PropTypes.arrayOf(PropTypes.object).isRequired, selected: PropTypes.arrayOf(PropTypes.object).isRequired, intersectionPolicy: PropTypes.bool, + filteringByNameLabel: PropTypes.string, }; OrgUnitForm.defaultProps = { diff --git a/src/components/OrgUnitsFilter.component.js b/src/components/OrgUnitsFilter.component.js index b4f5105..ce3a87f 100644 --- a/src/components/OrgUnitsFilter.component.js +++ b/src/components/OrgUnitsFilter.component.js @@ -120,6 +120,16 @@ class OrgUnitsFilter extends React.Component { roots={this.state.roots} selected={this.state.selected} intersectionPolicy={true} + filteringByNameLabel={ + title.includes("organisation units capture") + ? "filter_organisation_units_capture_by_name" + : "filter_organisation_units_output_by_name" + } + orgUnitsSelectedLabel={ + title.includes("organisation units capture") + ? "organisation_units_capture_selected" + : "organisation_units_output_selected" + } /> diff --git a/src/components/ReplicateUserFromTable.component.js b/src/components/ReplicateUserFromTable.component.js index ccdf6a9..f264fc3 100644 --- a/src/components/ReplicateUserFromTable.component.js +++ b/src/components/ReplicateUserFromTable.component.js @@ -13,7 +13,7 @@ class ReplicateUserFromTable extends React.Component { "firstName", "surname", "email", - "organisationUnitsCapture", + "organisationUnits", "dataViewOrganisationUnits", ]; @@ -30,6 +30,7 @@ class ReplicateUserFromTable extends React.Component { async componentDidMount() { const { userToReplicateId } = this.props; const userToReplicate = await User.getById(d2, userToReplicateId); + console.log(userToReplicate); this.setState({ userToReplicate }); } @@ -52,11 +53,12 @@ class ReplicateUserFromTable extends React.Component { render() { const { onRequestClose } = this.props; const { userToReplicate } = this.state; - const title = this.t("replicate_user", { + const title = this.t("replicate_user_title", { user: userToReplicate ? `${userToReplicate.displayName} (${userToReplicate.username})` : "", }); + console.log(title); return !userToReplicate ? ( diff --git a/src/components/ReplicateUserFromTemplate.component.js b/src/components/ReplicateUserFromTemplate.component.js index f732281..5453668 100644 --- a/src/components/ReplicateUserFromTemplate.component.js +++ b/src/components/ReplicateUserFromTemplate.component.js @@ -172,7 +172,7 @@ class ReplicateUserFromTemplate extends React.Component { isValid, validate, } = this.state; - const title = this.getTranslation("replicate_user", { + const title = this.getTranslation("replicate_user_title", { user: userToReplicate ? `${userToReplicate.displayName} (${userToReplicate.username})` : "", diff --git a/src/components/UserGroupsDialog.component.js b/src/components/UserGroupsDialog.component.js index 03eceb3..c2050be 100644 --- a/src/components/UserGroupsDialog.component.js +++ b/src/components/UserGroupsDialog.component.js @@ -42,7 +42,7 @@ function getPayload(allUserGroups, pairs) { function getTitle(getTranslation, users) { const usernames = users && users.map(user => user.userCredentials.username); const info = usernames ? _m.joinString(getTranslation, usernames, 3, ", ") : "..."; - return getTranslation("assignGroups") + ": " + info; + return getTranslation("assign_groups") + ": " + info; } function UserGroupsDialog(props, context) { diff --git a/src/components/UserRolesDialog.component.js b/src/components/UserRolesDialog.component.js index 5cf7f87..3199855 100644 --- a/src/components/UserRolesDialog.component.js +++ b/src/components/UserRolesDialog.component.js @@ -19,7 +19,7 @@ function getPayload(allUserRoles, pairs) { function getTitle(getTranslation, users) { const usernames = users && users.map(user => user.userCredentials.username); const info = usernames ? _m.joinString(getTranslation, usernames, 3, ", ") : "..."; - return getTranslation("assignRoles") + ": " + info; + return getTranslation("assign_roles") + ": " + info; } function UserRolesDialog(props, context) { diff --git a/src/i18n/i18n_module_ar.properties b/src/i18n/i18n_module_ar.properties index 00e95b3..f51d6ba 100644 --- a/src/i18n/i18n_module_ar.properties +++ b/src/i18n/i18n_module_ar.properties @@ -1,13 +1,13 @@ actions=Actions assign_all=Assign all -assignGroups=Assign to groups -assignRoles=Assign roles -assignToOrgUnitsCapture=Assign to organisation units cature -assignToOrgUnitsOutput=Assign to organisation units output -replicateUser=Replicate user +assign_groups=Assign to groups +assign_roles=Assign roles +assign_to_org_units_capture=Assign to organisation units cature +assign_to_org_units_output=Assign to organisation units output +replicate_user=Replicate user cancel=CANCEL close=Close -copyInUser=Copy in User +copy_in_user=Copy in user remove=Remove enable=Enable disable=Disable @@ -30,7 +30,8 @@ enable_error=Error enabling users: $$error$$ disable_error=Error disabling users: $$error$$ edit=Edit filter_group=Filter by group -filter_organisation_units_capture_by_name=Filtering organisation units capture by name +filter_organisation_units_capture_by_name=Filter organisation units capture by name +filter_organisation_units_output_by_name=Filter organisation units output by name filter_role=Filter by role filter_by_organisation_units_capture=Filter by organisation units capture filter_by_organisation_units_output=Filter by organisation units output @@ -52,6 +53,7 @@ organisation_unit_group=مجموعة وحدة المنظمة organisation_unit_level=Organisation unit level organisation_units=OU Capture organisation_units_capture_selected=Organisation units capture selected +organisation_units_output_selected=Organisation units output selected save=SAVE search=Search search_by_name=Search by name @@ -87,7 +89,7 @@ value_required=Value is required replicate=Replicate replicate_user_from_template=From template replicate_user_from_table=From table -replicate_user=Replicate $$user$$ +replicate_user_title=Replicate $$user$$ metadata_error=Error on metadata action metadata_error_description=There was an error while posting metadata. Those are the details: replicate_successful=User '$$user$$' replicated successfully $$n$$ times diff --git a/src/i18n/i18n_module_en.properties b/src/i18n/i18n_module_en.properties index 9aa931a..ea92580 100644 --- a/src/i18n/i18n_module_en.properties +++ b/src/i18n/i18n_module_en.properties @@ -1,13 +1,13 @@ actions=Actions assign_all=Assign all -assignGroups=Assign to groups -assignRoles=Assign roles -assignToOrgUnitsCapture=Assign to organisation units capture -assignToOrgUnitsOutput=Assign to organisation units output -replicateUser=Replicate user +assign_groups=Assign to groups +assign_roles=Assign roles +assign_to_org_units_capture=Assign to organisation units capture +assign_to_org_units_output=Assign to organisation units output +replicate_user=Replicate user cancel=CANCEL close=Close -copyInUser=Copy in User +copy_in_user=Copy in user remove=Remove enable=Enable disable=Disable @@ -30,7 +30,8 @@ enable_error=Error enabling users: $$error$$ disable_error=Error disabling users: $$error$$ edit=Edit filter_group=Filter by group -filter_organisation_units_capture_by_name=Filtering organisation units capture by name +filter_organisation_units_capture_by_name=Filter organisation units capture by name +filter_organisation_units_output_by_name=Filter organisation units output by name filter_role=Filter by role filter_by_organisation_units_capture=Filter by organisation units capture filter_by_organisation_units_output=Filter by organisation units output @@ -48,10 +49,12 @@ disabled=Disabled no_roots_found=No roots found of_page=of org_unit_assignment=Organisation unit assignment -organisation_unit_group=Organisation Unit Group +organisation_unit_group=Organisation unit group organisation_unit_level=Organisation unit level organisation_units=OU Capture +organisation_units_capture=OU Capture organisation_units_capture_selected=Organisation units capture selected +organisation_units_output_selected=Organisation units output selected save=SAVE search=Search search_by_name=Search by name @@ -87,7 +90,7 @@ value_required=Value is required replicate=Replicate replicate_user_from_template=From template replicate_user_from_table=From table -replicate_user=Replicate $$user$$ +replicate_user_title=Replicate $$user$$ metadata_error=Error on metadata action metadata_error_description=There was an error while posting metadata. Those are the details: replicate_successful=User '$$user$$' replicated successfully $$n$$ times diff --git a/src/i18n/i18n_module_es.properties b/src/i18n/i18n_module_es.properties index 69e17b7..7c837a0 100644 --- a/src/i18n/i18n_module_es.properties +++ b/src/i18n/i18n_module_es.properties @@ -1,13 +1,13 @@ actions=Acciones assign_all=Assignar todos -assignGroups=Asignar a grupos -assignRoles=Asignar a roles -assignToOrgUnitsCapture=Asignar a unidades organizativas -assignToOrgUnitsOutput=Asignar a unidades organizativas de salida -replicateUser=Replicar usuario +assign_groups=Asignar a grupos +assign_roles=Asignar a roles +assign_to_org_units_capture=Asignar a la captura de unidades organizativas +assign_to_org_units_output=Asignar a la salida de las unidades de la organización +replicate_user=Replicar usuario cancel=CANCELAR close=Cerrar -copyInUser=Copiar en usuario +copy_in_user=Copiar en usuario remove=Eliminar enable=Habilitar disable=Deshabilitar @@ -30,10 +30,11 @@ enable_error=Error habilitando usuarios: $$error$$ disable_error=Error deshabilitando usuarios: $$error$$ edit=Editar filter_group=Filtrar por grupo -filter_organisation_units_capture_by_name=Filtrar por unidad organizativa +filter_organisation_units_capture_by_name=Filtrar las unidades organizativas capturadas por nombre +filter_organisation_units_output_by_name=Filtrar la salida de las unidades organizativas por nombre filter_role=Filtrar rol -filter_by_organisation_units_capture=Filtrar por unidad organizativa -filter_by_organisation_units_output=Filtrar por unidad organizativa de salida +filter_by_organisation_units_capture=Filtrar por captura de unidades organizativas +filter_by_organisation_units_output=Filtrar por salida de unidades organizativas href=API Url hidden_by_filters=ocultos por filtros id=Id @@ -50,8 +51,10 @@ of_page=de org_unit_assignment=Asignació de unidades organizativas organisation_unit_group=Grupo de unidades organizativas organisation_unit_level=Nivel de unidad organizativa -organisation_units=Unidades organizativas -organisation_units_capture_selected=Unidades organizativas seleccionadas +organisation_units=Captura UO +organisation_units_capture=Captura UO +organisation_units_capture_selected=Captura de unidades organizativas seleccionada +organisation_units_output_selected=Salida de unidades organizativas seleccionada save=GUARDAR search=Search search_by_name=Buscar por nombre @@ -87,7 +90,7 @@ value_required=Valor obligatorio replicate=Replicar replicate_user_from_template=Con plantilla replicate_user_from_table=Con tabla -replicate_user=Replicar $$user$$ +replicate_user_title=Replicar $$user$$ metadata_error=Error al enviar metadatos metadata_error_description=Se ha producido un error al enviar metadatos. Detalles: replicate_successful=Usuario '$$user$$' replicado con éxito $$n$$ veces diff --git a/src/i18n/i18n_module_fr.properties b/src/i18n/i18n_module_fr.properties index 1db9632..5b18a22 100644 --- a/src/i18n/i18n_module_fr.properties +++ b/src/i18n/i18n_module_fr.properties @@ -1,13 +1,13 @@ actions=Actions assign_all=Assign all -assignGroups=Assign to groups -assignRoles=Assign roles -assignToOrgUnitsCapture=Assign to organisation units capture -assignToOrgUnitsOutput=Assign to organisation units output -replicateUser=Replicate user +assign_groups=Assign to groups +assign_roles=Assign roles +assign_to_org_units_capture=Assign to organisation units capture +assign_to_org_units_output=Assign to organisation units output +replicate_user=Replicate user cancel=CANCEL close=Close -copyInUser=Copy in User +copy_in_user=Copy in user remove=Remove enable=Enable disable=Disable @@ -30,7 +30,8 @@ enable_error=Error enabling users: $$error$$ disable_error=Error disabling users: $$error$$ edit=Edit filter_group=Filter by group -filter_organisation_units_capture_by_name=Filtering organisation units capture by name +filter_organisation_units_capture_by_name=Filter organisation units capture by name +filter_organisation_units_output_by_name=Filter organisation units output by name filter_role=Filter by role filter_by_organisation_units_capture=Filter by organisation units capture filter_by_organisation_units_output=Filter by organisation units output @@ -50,8 +51,10 @@ of_page=of org_unit_assignment=Organisation unit assignment organisation_unit_group=Groupe d'Unités d'Organisation organisation_unit_level=Niveau d'unité d'organisation -organisation_units=unités d'organisation -organisation_units_capture_selected=Organisation units selected +organisation_units=Capture d'UO +organisation_units_capture_selected=Organisation units capture selected +organisation_units_output_selected=Organisation units output selected + save=SAVE search=Search search_by_name=Rechercher par nom @@ -87,7 +90,7 @@ value_required=Value is required replicate=Replicate replicate_user_from_template=From template replicate_user_from_table=From table -replicate_user=Replicate $$user$$ +replicate_user_title=Replicate $$user$$ metadata_error=Error on metadata action metadata_error_description=There was an error while posting metadata. Those are the details: replicate_successful=User '$$user$$' replicated successfully $$n$$ times From 0be5d6f0f9cfdb30f93cec6d4cd705da2d363fea Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 14 May 2021 14:17:05 +0200 Subject: [PATCH 09/30] finalized the issue, took out console logs and cleaned up things --- src/List/Filters.component.js | 1 - src/List/List.component.js | 1 + src/components/ImportTable.component.js | 3 ++- src/components/MultipleSelector.component.js | 2 +- src/components/OrgUnitForm.js | 4 +--- src/components/ReplicateUserFromTable.component.js | 2 -- src/i18n/i18n_module_ar.properties | 2 +- 7 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index 2f1b1b1..4da4aac 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -116,7 +116,6 @@ export default class Filters extends React.Component { orgUnits, orgUnitsOutput, } = this.state; - console.log(orgUnits); const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); return { diff --git a/src/List/List.component.js b/src/List/List.component.js index 9dda192..f93b9b2 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -496,6 +496,7 @@ const List = React.createClass({ .at(settings.get("visibleTableColumns")) .compact() .value(); + return (
diff --git a/src/components/ImportTable.component.js b/src/components/ImportTable.component.js index 2d61aff..9a3f131 100644 --- a/src/components/ImportTable.component.js +++ b/src/components/ImportTable.component.js @@ -438,6 +438,7 @@ class ImportTable extends React.Component { field === "organisationUnits" || field === "dataViewOrganisationUnits" ? orgUnitsField : "displayName"; + if (isMultipleValue) { const values = value || []; const compactValue = _(values).isEmpty() @@ -452,7 +453,7 @@ class ImportTable extends React.Component { .join(", "); const onClick = this.getOnTextFieldClicked(user.id, field); - return this.getTextField(displayField, compactValue, { + return this.getTextField(field, compactValue, { validators, component: props => ( _.last(path.split("/"))); - console.log(orgUnitIds); const newSelected = await listWithInFilter(d2.models.organisationUnits, "id", orgUnitIds, { paging: false, fields: "id,displayName,shortName,path", }); - console.log(newSelected); this.props.onChange(newSelected); } @@ -170,7 +168,6 @@ class OrgUnitForm extends React.Component { }; const selectedPaths = this.props.selected.map(ou => ou.path); - console.log(selectedPaths); return (
{this.state.loading ? ( @@ -226,6 +223,7 @@ OrgUnitForm.propTypes = { selected: PropTypes.arrayOf(PropTypes.object).isRequired, intersectionPolicy: PropTypes.bool, filteringByNameLabel: PropTypes.string, + orgUnitsSelectedLabel: PropTypes.string, }; OrgUnitForm.defaultProps = { diff --git a/src/components/ReplicateUserFromTable.component.js b/src/components/ReplicateUserFromTable.component.js index f264fc3..12baf74 100644 --- a/src/components/ReplicateUserFromTable.component.js +++ b/src/components/ReplicateUserFromTable.component.js @@ -30,7 +30,6 @@ class ReplicateUserFromTable extends React.Component { async componentDidMount() { const { userToReplicateId } = this.props; const userToReplicate = await User.getById(d2, userToReplicateId); - console.log(userToReplicate); this.setState({ userToReplicate }); } @@ -58,7 +57,6 @@ class ReplicateUserFromTable extends React.Component { ? `${userToReplicate.displayName} (${userToReplicate.username})` : "", }); - console.log(title); return !userToReplicate ? ( diff --git a/src/i18n/i18n_module_ar.properties b/src/i18n/i18n_module_ar.properties index f51d6ba..a7c7c12 100644 --- a/src/i18n/i18n_module_ar.properties +++ b/src/i18n/i18n_module_ar.properties @@ -2,7 +2,7 @@ actions=Actions assign_all=Assign all assign_groups=Assign to groups assign_roles=Assign roles -assign_to_org_units_capture=Assign to organisation units cature +assign_to_org_units_capture=Assign to organisation units capture assign_to_org_units_output=Assign to organisation units output replicate_user=Replicate user cancel=CANCEL From 78711afa6a8f50850553d84477bf2f9330a87e83 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 14 May 2021 14:25:41 +0200 Subject: [PATCH 10/30] spacing --- src/List/Filters.component.js | 1 + src/components/MultipleSelector.component.js | 2 +- src/components/OrgUnitForm.js | 1 + src/i18n/i18n_module_en.properties | 1 - src/i18n/i18n_module_es.properties | 1 - src/i18n/i18n_module_fr.properties | 1 - 6 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index 4da4aac..a668184 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -116,6 +116,7 @@ export default class Filters extends React.Component { orgUnits, orgUnitsOutput, } = this.state; + const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); return { diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index b9b7a74..eb73283 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -85,7 +85,7 @@ class MultipleSelector extends React.Component { renderForm() { const { field, options, orgUnitRoots } = this.props; const { selected } = this.state; - console.log(orgUnitRoots); + switch (field) { case "userGroups": case "userRoles": diff --git a/src/components/OrgUnitForm.js b/src/components/OrgUnitForm.js index 3b84ef5..c2e6c8d 100644 --- a/src/components/OrgUnitForm.js +++ b/src/components/OrgUnitForm.js @@ -168,6 +168,7 @@ class OrgUnitForm extends React.Component { }; const selectedPaths = this.props.selected.map(ou => ou.path); + return (
{this.state.loading ? ( diff --git a/src/i18n/i18n_module_en.properties b/src/i18n/i18n_module_en.properties index ea92580..1d71916 100644 --- a/src/i18n/i18n_module_en.properties +++ b/src/i18n/i18n_module_en.properties @@ -52,7 +52,6 @@ org_unit_assignment=Organisation unit assignment organisation_unit_group=Organisation unit group organisation_unit_level=Organisation unit level organisation_units=OU Capture -organisation_units_capture=OU Capture organisation_units_capture_selected=Organisation units capture selected organisation_units_output_selected=Organisation units output selected save=SAVE diff --git a/src/i18n/i18n_module_es.properties b/src/i18n/i18n_module_es.properties index 7c837a0..eb983cc 100644 --- a/src/i18n/i18n_module_es.properties +++ b/src/i18n/i18n_module_es.properties @@ -52,7 +52,6 @@ org_unit_assignment=Asignació de unidades organizativas organisation_unit_group=Grupo de unidades organizativas organisation_unit_level=Nivel de unidad organizativa organisation_units=Captura UO -organisation_units_capture=Captura UO organisation_units_capture_selected=Captura de unidades organizativas seleccionada organisation_units_output_selected=Salida de unidades organizativas seleccionada save=GUARDAR diff --git a/src/i18n/i18n_module_fr.properties b/src/i18n/i18n_module_fr.properties index 5b18a22..143c3b3 100644 --- a/src/i18n/i18n_module_fr.properties +++ b/src/i18n/i18n_module_fr.properties @@ -54,7 +54,6 @@ organisation_unit_level=Niveau d'unité d'organisation organisation_units=Capture d'UO organisation_units_capture_selected=Organisation units capture selected organisation_units_output_selected=Organisation units output selected - save=SAVE search=Search search_by_name=Rechercher par nom From 307638811a4f41db1fd12dbf2055a2d0bf978591 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Wed, 19 May 2021 12:30:59 +0200 Subject: [PATCH 11/30] addressed PR comments --- src/List/Filters.component.js | 1 + src/models/userHelpers.js | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/List/Filters.component.js b/src/List/Filters.component.js index 01f54e5..a668184 100644 --- a/src/List/Filters.component.js +++ b/src/List/Filters.component.js @@ -118,6 +118,7 @@ export default class Filters extends React.Component { } = this.state; const inFilter = field => (_(field).isEmpty() ? null : ["in", field]); + return { ...(showOnlyManagedUsers ? { canManage: "true" } : {}), ...(searchString ? { query: searchString } : {}), diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 4d70019..f529092 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -61,7 +61,7 @@ const columnNameFromPropertyMapping = { userGroups: "Groups", organisationUnits: "OUCapture", dataViewOrganisationUnits: "OUOutput", - disabled: "disabled", + disabled: "Disabled", }; const propertyFromColumnNameMapping = _.invert(columnNameFromPropertyMapping); @@ -336,13 +336,9 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { const data = userRows.map((userRow, rowIndex) => getPlainUserFromRow(userRow, modelValuesByField, rowIndex + 2) ); - const users = data.map(o => { - let newUser = o.user; - if (newUser.disabled) { - newUser.disabled = Boolean(newUser.disabled); - } - return newUser; - }); + const users = data.map(o => + o.user.disabled ? { ...o.user, disabled: o.user.disabled.toLowerCase() } : o.user + ); const userWarnings = _(data) .flatMap(o => o.warnings) .value(); @@ -563,6 +559,7 @@ function getList(d2, filters, listOptions) { .pickBy() .toPairs() .value(); + /* Filtering over nested fields (table[.table].field) in N-to-N relationships (for example: userCredentials.userRoles.id), fails in dhis2 < v2.30. So we need to make separate calls to the API for those filters and use the returned IDs to build From 866c7c80f649e9a5f4cb0f6928692719d5705494 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Wed, 19 May 2021 18:38:58 +0100 Subject: [PATCH 12/30] change order of import/export options --- src/components/ImportExport.component.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ImportExport.component.js b/src/components/ImportExport.component.js index 6e32543..b2c4e8b 100644 --- a/src/components/ImportExport.component.js +++ b/src/components/ImportExport.component.js @@ -147,16 +147,16 @@ class ImportExport extends React.Component { onRequestClose={closeMenu} > - } - primaryText={t("export")} - onClick={exportToCsvAndSave} - /> } primaryText={t("import")} onClick={importFromCsv} /> + } + primaryText={t("export")} + onClick={exportToCsvAndSave} + /> } primaryText={t("export_empty_template")} From d1064090d448261faea0e84e00ae2601fadc8474 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Thu, 20 May 2021 14:40:48 +0200 Subject: [PATCH 13/30] applying PR comments --- src/List/List.component.js | 29 ++++++++++++++++++- .../OrgUnitDialog.component.js | 17 ++++------- src/components/MultipleSelector.component.js | 8 +++-- src/components/OrgUnitForm.js | 8 ++--- src/i18n/i18n_module_es.properties | 2 +- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/src/List/List.component.js b/src/List/List.component.js index f93b9b2..f436052 100644 --- a/src/List/List.component.js +++ b/src/List/List.component.js @@ -551,7 +551,8 @@ const List = React.createClass({ ) : null}
- {this.state.orgunitassignment.open ? ( + {this.state.orgunitassignment.open && + this.state.orgunitassignment.field === "organisationUnits" ? ( + ) : null} + + {this.state.orgunitassignment.open && + this.state.orgunitassignment.field === "dataViewOrganisationUnits" ? ( + ) : null} diff --git a/src/List/organisation-unit-dialog/OrgUnitDialog.component.js b/src/List/organisation-unit-dialog/OrgUnitDialog.component.js index 4ad3e2f..c80ab4e 100644 --- a/src/List/organisation-unit-dialog/OrgUnitDialog.component.js +++ b/src/List/organisation-unit-dialog/OrgUnitDialog.component.js @@ -101,7 +101,8 @@ class OrgUnitDialog extends React.Component { } render() { - const { root, title, models } = this.props; + const { root, title, models, filteringByNameLabel, orgUnitsSelectedLabel } = this.props; + const styles = { dialog: { minWidth: 875, @@ -144,16 +145,8 @@ class OrgUnitDialog extends React.Component { roots={this.props.roots} selected={this.state.selected} intersectionPolicy={true} - filteringByNameLabel={ - title.includes("organisation units capture") - ? "filter_organisation_units_capture_by_name" - : "filter_organisation_units_output_by_name" - } - orgUnitsSelectedLabel={ - title.includes("organisation units capture") - ? "organisation_units_capture_selected" - : "organisation_units_output_selected" - } + filteringByNameLabel={filteringByNameLabel} + orgUnitsSelectedLabel={orgUnitsSelectedLabel} /> ); @@ -168,6 +161,8 @@ OrgUnitDialog.propTypes = { title: PropTypes.string.isRequired, onOrgUnitAssignmentSaved: PropTypes.func.isRequired, onOrgUnitAssignmentError: PropTypes.func.isRequired, + filteringByNameLabel: PropTypes.string.isRequired, + orgUnitsSelectedLabel: PropTypes.string.isRequired, }; OrgUnitDialog.contextTypes = { diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index eb73283..67f39aa 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -110,8 +110,12 @@ class MultipleSelector extends React.Component { roots={orgUnitRoots} selected={selected} intersectionPolicy={false} - filteringByNameLabel="filter_organisation_units_capture_by_name" - orgUnitsSelectedLabel="organisation_units_capture_selected" + filteringByNameLabel={this.getTranslation( + "filter_organisation_units_capture_by_name" + )} + orgUnitsSelectedLabel={this.getTranslation( + "organisation_units_capture_selected" + )} /> ); case "dataViewOrganisationUnits": diff --git a/src/components/OrgUnitForm.js b/src/components/OrgUnitForm.js index c2e6c8d..1ecbabc 100644 --- a/src/components/OrgUnitForm.js +++ b/src/components/OrgUnitForm.js @@ -181,9 +181,7 @@ class OrgUnitForm extends React.Component { this._searchOrganisationUnits(event.target.value)} - floatingLabelText={this.context.d2.i18n.getTranslation( - `${filteringByNameLabel}` - )} + floatingLabelText={filteringByNameLabel} fullWidth /> @@ -208,9 +206,7 @@ class OrgUnitForm extends React.Component {
- {`${this.props.selected.length} ${this.getTranslation( - `${orgUnitsSelectedLabel}` - )}`} + {`${this.props.selected.length} ${orgUnitsSelectedLabel}`}
{this.renderRoots()}
diff --git a/src/i18n/i18n_module_es.properties b/src/i18n/i18n_module_es.properties index eb983cc..d15aca6 100644 --- a/src/i18n/i18n_module_es.properties +++ b/src/i18n/i18n_module_es.properties @@ -3,7 +3,7 @@ assign_all=Assignar todos assign_groups=Asignar a grupos assign_roles=Asignar a roles assign_to_org_units_capture=Asignar a la captura de unidades organizativas -assign_to_org_units_output=Asignar a la salida de las unidades de la organización +assign_to_org_units_output=Asignar a la salida de las unidades organizativas replicate_user=Replicar usuario cancel=CANCELAR close=Cerrar From 211afcea1162428a9b4c496bbced8bd8489249c6 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 21 May 2021 09:46:31 +0200 Subject: [PATCH 14/30] Translate MultipleSelector dataViewOrganisationUnits props --- src/components/MultipleSelector.component.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/MultipleSelector.component.js b/src/components/MultipleSelector.component.js index 67f39aa..94fc6ac 100644 --- a/src/components/MultipleSelector.component.js +++ b/src/components/MultipleSelector.component.js @@ -85,6 +85,7 @@ class MultipleSelector extends React.Component { renderForm() { const { field, options, orgUnitRoots } = this.props; const { selected } = this.state; + const t = this.getTranslation.bind(this); switch (field) { case "userGroups": @@ -110,12 +111,8 @@ class MultipleSelector extends React.Component { roots={orgUnitRoots} selected={selected} intersectionPolicy={false} - filteringByNameLabel={this.getTranslation( - "filter_organisation_units_capture_by_name" - )} - orgUnitsSelectedLabel={this.getTranslation( - "organisation_units_capture_selected" - )} + filteringByNameLabel={t("filter_organisation_units_capture_by_name")} + orgUnitsSelectedLabel={t("organisation_units_capture_selected")} /> ); case "dataViewOrganisationUnits": @@ -126,8 +123,8 @@ class MultipleSelector extends React.Component { roots={orgUnitRoots} selected={selected} intersectionPolicy={false} - filteringByNameLabel="filter_organisation_units_output_by_name" - orgUnitsSelectedLabel="organisation_units_output_selected" + filteringByNameLabel={t("filter_organisation_units_output_by_name")} + orgUnitsSelectedLabel={t("organisation_units_output_selected")} /> ); default: From ee6397fcd6b378aa5530dea4d5a790a3fac0d614 Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 21 May 2021 12:01:36 +0200 Subject: [PATCH 15/30] trying to add toggle to disabled column --- src/components/ImportTable.component.js | 14 ++++++++++++++ src/models/userHelpers.js | 3 ++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/ImportTable.component.js b/src/components/ImportTable.component.js index 9a3f131..5825b5f 100644 --- a/src/components/ImportTable.component.js +++ b/src/components/ImportTable.component.js @@ -238,6 +238,7 @@ class ImportTable extends React.Component { ? { [importField]: { ...user[importField], hasDuplicates: false } } : {}), }); + const validators = (this.getFieldsInfo()[name] || {}).validators || []; // Force re-render if validations change so new error messages are shown const shouldRender = @@ -465,6 +466,18 @@ class ImportTable extends React.Component { /> ), }); + } else if (field === "disabled") { + return { + name: field, + value, + component: Toggle, + props: { + name: field, + type: "boolean", + style: { width: "100%" }, + }, + validators, + }; } else { const extraProps = { changeEvent: "onBlur" }; return this.getTextField(field, value, { @@ -496,6 +509,7 @@ class ImportTable extends React.Component { organisationUnits: templateUser.attributes.organisationUnits, dataViewOrganisationUnits: templateUser.attributes.dataViewOrganisationUnits, email: templateUser.attributes.email, + disabled: false, }; } else { newUser = { diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index f529092..2b2bc20 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -266,6 +266,7 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { : _(csv.data) .drop(1) .value(); + const plainUserAttributes = _(d2.models.users.modelValidations) .map((value, key) => (_(["TEXT", "DATE", "URL"]).includes(value.type) ? key : null)) .compact() @@ -337,7 +338,7 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { getPlainUserFromRow(userRow, modelValuesByField, rowIndex + 2) ); const users = data.map(o => - o.user.disabled ? { ...o.user, disabled: o.user.disabled.toLowerCase() } : o.user + o.user.disabled ? { ...o.user, disabled: eval(o.user.disabled.toLowerCase()) } : o.user ); const userWarnings = _(data) .flatMap(o => o.warnings) From e0988c62491e65bff99e10a73e96101f41105f4e Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Fri, 21 May 2021 12:08:43 +0200 Subject: [PATCH 16/30] removed unnecessary space and made new user have the templateUser disabled property --- src/components/ImportTable.component.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ImportTable.component.js b/src/components/ImportTable.component.js index 5825b5f..255f365 100644 --- a/src/components/ImportTable.component.js +++ b/src/components/ImportTable.component.js @@ -238,7 +238,6 @@ class ImportTable extends React.Component { ? { [importField]: { ...user[importField], hasDuplicates: false } } : {}), }); - const validators = (this.getFieldsInfo()[name] || {}).validators || []; // Force re-render if validations change so new error messages are shown const shouldRender = @@ -509,7 +508,7 @@ class ImportTable extends React.Component { organisationUnits: templateUser.attributes.organisationUnits, dataViewOrganisationUnits: templateUser.attributes.dataViewOrganisationUnits, email: templateUser.attributes.email, - disabled: false, + disabled: templateUser.attributes.disabled, }; } else { newUser = { From 1215392a30a3732408450c22bcd6a10244822922 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 21 May 2021 12:48:31 +0200 Subject: [PATCH 17/30] Use toggle in import table as uncontrolled component --- src/components/ImportTable.component.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/ImportTable.component.js b/src/components/ImportTable.component.js index 255f365..3b39d74 100644 --- a/src/components/ImportTable.component.js +++ b/src/components/ImportTable.component.js @@ -468,11 +468,13 @@ class ImportTable extends React.Component { } else if (field === "disabled") { return { name: field, - value, component: Toggle, props: { name: field, - type: "boolean", + defaultToggled: value, + onToggle: (event, isInputChecked) => { + this.onUpdateField(user.id, field, isInputChecked); + }, style: { width: "100%" }, }, validators, From c8ceb823288ce5e5741d549c184be6579172dac9 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 21 May 2021 12:48:49 +0200 Subject: [PATCH 18/30] Refactor userHelper disabled processing --- src/models/userHelpers.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 2b2bc20..22f1691 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -337,9 +337,10 @@ async function getUsersFromCsv(d2, file, csv, { maxUsers, orgUnitsField }) { const data = userRows.map((userRow, rowIndex) => getPlainUserFromRow(userRow, modelValuesByField, rowIndex + 2) ); - const users = data.map(o => - o.user.disabled ? { ...o.user, disabled: eval(o.user.disabled.toLowerCase()) } : o.user - ); + const users = data.map(o => { + const disableStr = (o.user.disabled || "").toLowerCase(); + return { ...o.user, disabled: disableStr === "true" }; + }); const userWarnings = _(data) .flatMap(o => o.warnings) .value(); @@ -378,7 +379,7 @@ function parseResponse(response, payload) { } function getUserPayloadFromPlainAttributes(baseUser, userFields) { - const clean = obj => _.omitBy(obj, value => !value); + const clean = obj => _.omitBy(obj, value => value === undefined || value === null); const userRoot = { ...baseUser, From b895f975fe7cfa7b18cc8cb704ede200fa893f7a Mon Sep 17 00:00:00 2001 From: Jocelyn Dunkley Date: Wed, 26 May 2021 11:38:38 +0200 Subject: [PATCH 19/30] added the userCrendetials field to the getAllChildren function in CopyInUserBatchModelsMultiSelect.model --- .../CopyInUserBatchModelsMultiSelect.component.js | 3 ++- .../CopyInUserBatchModelsMultiSelect.model.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/batch-models-multi-select/CopyInUserBatchModelsMultiSelect.component.js b/src/components/batch-models-multi-select/CopyInUserBatchModelsMultiSelect.component.js index 69aca4e..b9050d6 100644 --- a/src/components/batch-models-multi-select/CopyInUserBatchModelsMultiSelect.component.js +++ b/src/components/batch-models-multi-select/CopyInUserBatchModelsMultiSelect.component.js @@ -210,9 +210,10 @@ export default class CopyInUserBatchModelsMultiSelectComponent extends React.Com const parentName = this.props.parents[0].name; const options = _(allChildren || []) .sortBy("name") - .map(obj => ({ value: obj.id, text: obj.name })) + .map(obj => ({ value: obj.id, text: `${obj.name} (${obj.userCredentials.username})` })) .filter(obj => obj.text !== parentName) .value(); + return ( toArray(getChildren(parent)), - childrenFields: childrenFields || "id,name", + childrenFields: childrenFields || "id,name,userCredentials[username]", getPayload, }); } From 05268c9e4b4d19b242bc9ede14efca7e97bcb5f2 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Tue, 8 Jun 2021 12:26:53 +0200 Subject: [PATCH 20/30] Remove unused fields in user query --- src/models/userHelpers.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 22f1691..24929ab 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -21,13 +21,8 @@ const queryFields = [ "id", "userCredentials[username,disabled,userRoles[id,displayName],lastLogin]", "lastUpdated", - "created", - "displayDescription", - "code", - "publicAccess", "access", "href", - "level", "userGroups[id,displayName,publicAccess]", "organisationUnits[id,code,shortName,displayName]", "dataViewOrganisationUnits[id,code,shortName,displayName]", From 3cf0cd18ee782041a23153c5e21dfaf0b4fbd7a8 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 9 Jun 2021 09:17:28 +0200 Subject: [PATCH 21/30] Refactor mapPromise with a for-loop so async errors are not swallowed --- src/utils/dhis2Helpers.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/utils/dhis2Helpers.js b/src/utils/dhis2Helpers.js index fdf09c8..7e6e5b1 100644 --- a/src/utils/dhis2Helpers.js +++ b/src/utils/dhis2Helpers.js @@ -10,10 +10,13 @@ function getOrgUnitsRoots() { .toPromise(); } -function mapPromise(items, mapper) { - const reducer = (promise, item) => - promise.then(mappedItems => mapper(item).then(res => mappedItems.concat([res]))); - return items.reduce(reducer, Promise.resolve([])); +async function mapPromise(inputValues, mapper) { + const output = []; + for (const value of inputValues) { + const res = await mapper(value); + output.push(res); + } + return output; } /* Perform a model.list with a filter=FIELD:in:[VALUE1,VALUE2,...], breaking values to From 5ca795206d3f9468a2e30d98ad0d08a8d1002af6 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 9 Jun 2021 09:17:55 +0200 Subject: [PATCH 22/30] Move userList feature to custom model and completely refactor it --- src/List/list.store.js | 10 +-- src/models/userHelpers.js | 89 +------------------- src/models/userList.js | 169 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 90 deletions(-) create mode 100644 src/models/userList.js diff --git a/src/List/list.store.js b/src/List/list.store.js index 7b17794..6b36f1d 100644 --- a/src/List/list.store.js +++ b/src/List/list.store.js @@ -4,7 +4,7 @@ import Store from "d2-ui/lib/store/Store"; import _ from "lodash"; import appState from "../App/appStateStore"; -import { getList } from "../models/userHelpers"; +import { getUserList } from "../models/userList"; const orderForQuery = modelName => modelName === "organisationUnitLevel" ? "level:ASC" : "name:iasc"; @@ -40,11 +40,11 @@ export default Store.create({ this.listSourceSubject .concatAll() .combineLatest(columnObservable) - .subscribe(([modelCollection, columns]) => { + .subscribe(([usersResponse, columns]) => { this.setState({ tableColumns: columns, - pager: modelCollection.pager, - list: modelCollection.toArray().map(user => ({ + pager: usersResponse.pager, + list: usersResponse.users.map(user => ({ ...user, ...(!user.userCredentials ? {} @@ -98,7 +98,7 @@ export default Store.create({ filter(options, complete, error) { getD2().then(d2 => { const { filters, ...listOptions } = options; - const listSearchPromise = getList(d2, filters, listOptions); + const listSearchPromise = getUserList(d2, filters, listOptions); this.listSourceSubject.onNext(Observable.fromPromise(listSearchPromise)); complete(`list with filters '${filters}' is loading`); }); diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 24929ab..454a4a5 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -5,6 +5,7 @@ import Papa from "papaparse"; import { generateUid } from "d2/lib/uid"; import { mapPromise, listWithInFilter } from "../utils/dhis2Helpers"; +import { getUserList } from "./userList"; // Delimiter to use in multiple-value fields (roles, groups, orgUnits) const fieldSplitChar = "||"; @@ -28,11 +29,6 @@ const queryFields = [ "dataViewOrganisationUnits[id,code,shortName,displayName]", ].join(","); -/* - Limit Uids to avoid 413 Request too large - maxUids = (maxSize - urlAndOtherParamsSize) / (uidSize + encodedCommaSize) -*/ -const maxUids = (4096 - 1000) / (11 + 3); const requiredPropertiesOnImport = ["username", "password", "firstName", "surname"]; @@ -126,20 +122,6 @@ async function getAssociations(d2, objs, { orgUnitsField }) { return _.fromPairs(pairs); } -function buildD2Filter(filters) { - return filters.map(([key, [operator, value]]) => - [ - key, - operator, - _.isArray(value) - ? `[${_(value) - .take(maxUids) - .join(",")}]` - : value, - ].join(":") - ); -} - function getColumnNameFromProperty(property) { return columnNameFromPropertyMapping[property] || property; } @@ -545,73 +527,11 @@ async function saveCopyInUsers(d2, users, copyUserGroups) { } } -/* Return an array of users from DHIS2 API. - - filters: Object with `field` as keys, `[operator, value]` as values. - listOptions: Object to be passed directory to d2.models.users.list(...) -*/ -function getList(d2, filters, listOptions) { - const model = d2.models.user; - const activeFilters = _(filters) - .pickBy() - .toPairs() - .value(); - - /* Filtering over nested fields (table[.table].field) in N-to-N relationships (for - example: userCredentials.userRoles.id), fails in dhis2 < v2.30. So we need to make - separate calls to the API for those filters and use the returned IDs to build - the final, paginated call. */ - - const [preliminarFilters, normalFilters] = _.partition( - activeFilters, - ([key, [operator, value]]) => operator === "in" && key.match(/\./) - ); - - if (d2.system.version.minor >= 30) { - const listFilters = buildD2Filter(normalFilters.concat(preliminarFilters)); - return model.list({ - paging: true, - fields: queryFields, - filter: _(listFilters).isEmpty() ? "name:ne:default" : listFilters, - ...listOptions, - }); - } - - const preliminarD2Filters$ = preliminarFilters.map(preliminarFilter => - model - .list({ - paging: false, - fields: "id", - filter: buildD2Filter([preliminarFilter]), - }) - .then(collection => collection.toArray().map(obj => obj.id)) - .then( - ids => - `id:in:[${_(ids) - .take(maxUids) - .join(",")}]` - ) - ); - - return Promise.all(preliminarD2Filters$).then(preliminarD2Filters => { - const filters = buildD2Filter(normalFilters).concat(preliminarD2Filters); - - return model.list({ - paging: true, - fields: queryFields, - filter: _(filters).isEmpty() ? "name:ne:default" : filters, - ...listOptions, - }); - }); -} - /* Get users from Dhis2 API and export given columns to a CSV string */ async function exportToCsv(d2, columns, filterOptions, { orgUnitsField }) { - const { filters, ...listOptions } = { ...filterOptions, paging: false }; - const users = await getList(d2, filters, listOptions); - const userRows = users - .toArray() - .map(user => _.at(getPlainUser(user, { orgUnitsField }), columns)); + const { filters, ...listOptions } = { ...filterOptions, pageSize: 1e6 }; + const { users } = await getUserList(d2, filters, listOptions); + const userRows = users.map(user => _.at(getPlainUser(user, { orgUnitsField }), columns)); const header = columns.map(getColumnNameFromProperty); const table = [header, ...userRows]; @@ -701,7 +621,6 @@ function getPayload(parentUser, destUsers, fields, updateStrategy) { } export { - getList, exportToCsv, importFromCsv, updateUsers, diff --git a/src/models/userList.js b/src/models/userList.js new file mode 100644 index 0000000..06229e2 --- /dev/null +++ b/src/models/userList.js @@ -0,0 +1,169 @@ +import _ from "lodash"; +import { mapPromise } from "../utils/dhis2Helpers"; + +/* Get user lists with support for DHIS2 API bugs and to avoid 414-URI-too-long errors */ + +const queryFields = [ + "id", + "email", + "displayName|rename(name)", + "shortName", + "firstName", + "surname", + "created", + "lastUpdated", + "access", + "href", + "userCredentials[username,disabled,userRoles[id,displayName],lastLogin]", + "userGroups[id,displayName,publicAccess]", + "organisationUnits[id,code,shortName,displayName]", + "dataViewOrganisationUnits[id,code,shortName,displayName]", +]; + +// (maxSize - urlAndOtherParamsSize) / (uidSize + encodedCommaSize) +const maxUids = Math.floor((4096 - 1000) / (11 + 3)); + +// type FiltersObject = Record +// type ListOptions = {canManage?: boolean, query?: string, order?: string, page: number, pageSize: number } + +/* Return an array of users from DHIS2 API. */ +export async function getUserList(d2, filtersObject, listOptions) { + // Note these problems/shortcomings when using the /users API endpoint: + // + // 1) When passing simultaneously params `query` and `filter`, only the first page is returned. + // + // 2) Except for the first page, only one record is returned. + // + // 3) After a user update, the pager does not reflect the change for some time (10/20 seconds). + // + // 4) As we have to use a GET request, so we can easily hit the 414-URI-too-long errors when filtering. + // + // So we need to perform a custom filtering and pagination. Do separate queries for params + // `query` and `filter`, and manually paginate the intersection of users. Also, split the requests + // when necessary to avoid a 414 error. + + const query = (listOptions.query || "").trim(); + const filters = getFiltersFromObject(filtersObject); + const { canManage } = listOptions; + const hasQuery = query !== "" || canManage !== undefined; + const hasFilters = !_.isEmpty(filters); + + const usersByQuery = hasQuery ? await getD2Users(d2, { query, canManage, fields: "id" }) : null; + const usersByFilters = hasFilters ? await getFilteredUsers(d2, filters) : null; + const allUsers = !hasQuery && !hasFilters ? await getD2Users(d2, { fields: "id" }) : null; + + const groupOfUserIds = [usersByQuery, usersByFilters, allUsers] + .filter(users => users !== null) + .map(users => users.map(user => user.id)); + + const allIds = _.intersection(...groupOfUserIds); + const { pager, objects: ids } = paginate(allIds, listOptions); + + const usersLists = await mapPromise(_.chunk(ids, maxUids), idsGroup => { + return getD2Users(d2, { + order: listOptions.order, + fields: queryFields.join(","), + filters: [{ field: "id", operator: "in", value: idsGroup }], + paging: false, + }); + }); + + return { pager, users: _.flatten(usersLists) }; +} + +// Record -> Array<{field: string, operator: Operator, Value: value}> +function getFiltersFromObject(filtersObject) { + return _(filtersObject) + .pickBy() + .toPairs() + .map(([field, [operator, value]]) => ({ field, operator, value })) + .value(); +} + +// To be used when DHIS2 fixes all the API bugs */ +async function getUserListStandard(d2, filtersObject, listOptions) { + const collection = await d2.models.user.list({ + ..._.pick(listOptions, ["order", "page", "pageSize", "query"]), + filter: buildD2Filter(getFiltersFromObject(filtersObject)), + fields: queryFields, + paging: true, + }); + + return { pager: collection.pager, users: collection.toArray() }; +} + +/* Manually paginate a collection of objects */ +function paginate(objects, options) { + const { page = 1, pageSize = 50 } = options; + const start = (page - 1) * pageSize; + const pageCount = Math.ceil(objects.length / options.pageSize); + const paginatedObjects = _.sortBy(objects.slice(start, start + pageSize)); + + const pager = { + page, + pageSize, + pageCount, + total: objects.length, + hasNextPage: () => page < pageCount, + hasPreviousPage: () => page > 1, + }; + + return { pager, objects: paginatedObjects }; +} + +// Check if in:[...] filters size exceed the limit. Assume the values are a collection of UIDs */ +function filtersExceedLimit(filters) { + const inFilters = filters.filter(({ operator }) => operator === "in"); + const totalValues = _.sum(inFilters.map(({ value }) => value.length)); + return totalValues > maxUids; +} + +async function getD2Users(d2, options) { + const { fields, paging = false, query, filters = [], ...otherOptions } = options; + + const listOptions = { + ...otherOptions, + fields, + paging, + ...(query ? { query } : {}), + ...(!_.isEmpty(filters) ? { filter: buildD2Filter(filters) } : {}), + }; + + const collection = await d2.models.user.list(listOptions); + + return collection.toArray(); +} + +async function getFilteredUsers(d2, filters) { + if (!filtersExceedLimit(filters)) { + return getD2Users(d2, { filters, fields: "id" }); + } else { + // We have too many in-filters values, make a one request per active filter and intersect the result. + const [inFilters, nonInFilters] = _.partition(filters, ({ operator }) => operator === "in"); + const groupsOfUserIds = []; + + for (const inFilter of inFilters) { + const userIdsForFilter = []; + + // For each filter, we can still have too many UIDs, split the requests and perform a union + for (const valuesGroup of _.chunk(inFilter.value, maxUids)) { + const filtersForGroup = [...nonInFilters, { ...inFilter, value: valuesGroup }]; + const usersForGroup = await getD2Users(d2, { + filters: filtersForGroup, + fields: "id", + }); + userIdsForFilter.push(usersForGroup.map(u => u.id)); + } + groupsOfUserIds.push(_.union(...userIdsForFilter)); + } + const ids = _.intersection(...groupsOfUserIds); + return ids.map(id => ({ id })); + } +} + +function buildD2Filter(filtersList) { + return filtersList.map(({ field, operator, value }) => { + const filterValue = _.isArray(value) ? `[${value.join(",")}]` : value; + return [field, operator, filterValue].join(":"); + }); +} From 312d666fe7a84388958c985fd8915bec788b87c3 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 9 Jun 2021 09:38:32 +0200 Subject: [PATCH 23/30] Use default fields in userList model --- src/models/userList.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/models/userList.js b/src/models/userList.js index 06229e2..b6d1ce7 100644 --- a/src/models/userList.js +++ b/src/models/userList.js @@ -48,9 +48,9 @@ export async function getUserList(d2, filtersObject, listOptions) { const hasQuery = query !== "" || canManage !== undefined; const hasFilters = !_.isEmpty(filters); - const usersByQuery = hasQuery ? await getD2Users(d2, { query, canManage, fields: "id" }) : null; + const usersByQuery = hasQuery ? await getD2Users(d2, { query, canManage }) : null; const usersByFilters = hasFilters ? await getFilteredUsers(d2, filters) : null; - const allUsers = !hasQuery && !hasFilters ? await getD2Users(d2, { fields: "id" }) : null; + const allUsers = !hasQuery && !hasFilters ? await getD2Users(d2, {}) : null; const groupOfUserIds = [usersByQuery, usersByFilters, allUsers] .filter(users => users !== null) @@ -62,7 +62,7 @@ export async function getUserList(d2, filtersObject, listOptions) { const usersLists = await mapPromise(_.chunk(ids, maxUids), idsGroup => { return getD2Users(d2, { order: listOptions.order, - fields: queryFields.join(","), + fields: queryFields, filters: [{ field: "id", operator: "in", value: idsGroup }], paging: false, }); @@ -83,9 +83,9 @@ function getFiltersFromObject(filtersObject) { // To be used when DHIS2 fixes all the API bugs */ async function getUserListStandard(d2, filtersObject, listOptions) { const collection = await d2.models.user.list({ - ..._.pick(listOptions, ["order", "page", "pageSize", "query"]), + ..._.pick(listOptions, ["order", "page", "pageSize", "query", "canManage"]), filter: buildD2Filter(getFiltersFromObject(filtersObject)), - fields: queryFields, + fields: queryFields.join(","), paging: true, }); @@ -119,11 +119,11 @@ function filtersExceedLimit(filters) { } async function getD2Users(d2, options) { - const { fields, paging = false, query, filters = [], ...otherOptions } = options; + const { fields = ["id"], paging = false, query, filters = [], ...otherOptions } = options; const listOptions = { ...otherOptions, - fields, + fields: fields.join(","), paging, ...(query ? { query } : {}), ...(!_.isEmpty(filters) ? { filter: buildD2Filter(filters) } : {}), @@ -136,7 +136,7 @@ async function getD2Users(d2, options) { async function getFilteredUsers(d2, filters) { if (!filtersExceedLimit(filters)) { - return getD2Users(d2, { filters, fields: "id" }); + return getD2Users(d2, { filters }); } else { // We have too many in-filters values, make a one request per active filter and intersect the result. const [inFilters, nonInFilters] = _.partition(filters, ({ operator }) => operator === "in"); @@ -148,10 +148,7 @@ async function getFilteredUsers(d2, filters) { // For each filter, we can still have too many UIDs, split the requests and perform a union for (const valuesGroup of _.chunk(inFilter.value, maxUids)) { const filtersForGroup = [...nonInFilters, { ...inFilter, value: valuesGroup }]; - const usersForGroup = await getD2Users(d2, { - filters: filtersForGroup, - fields: "id", - }); + const usersForGroup = await getD2Users(d2, { filters: filtersForGroup }); userIdsForFilter.push(usersForGroup.map(u => u.id)); } groupsOfUserIds.push(_.union(...userIdsForFilter)); From 90508e11f0f55f374e514c8bbfa64930be10fdc8 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Wed, 9 Jun 2021 09:24:52 +0100 Subject: [PATCH 24/30] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4c822c..4470781 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "d2-user-extended-app", - "version": "0.6.0", + "version": "0.7.0", "description": "DHIS2 Extended User app", "main": "src/index.html", "license": "GPL-3.0", From 971aebcdf10867ac5a1efe834dfb92e046e3c9f2 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Wed, 9 Jun 2021 19:47:58 +0200 Subject: [PATCH 25/30] Refactor users sorting in listing --- src/models/userList.js | 53 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/models/userList.js b/src/models/userList.js index b6d1ce7..fd89f70 100644 --- a/src/models/userList.js +++ b/src/models/userList.js @@ -39,12 +39,12 @@ export async function getUserList(d2, filtersObject, listOptions) { // 4) As we have to use a GET request, so we can easily hit the 414-URI-too-long errors when filtering. // // So we need to perform a custom filtering and pagination. Do separate queries for params - // `query` and `filter`, and manually paginate the intersection of users. Also, split the requests - // when necessary to avoid a 414 error. + // `query` and `filter`, and manually sort and paginate the intersection of users. + // Also, split the requests whenever necessary to avoid 414 errors. + const { canManage } = listOptions; const query = (listOptions.query || "").trim(); const filters = getFiltersFromObject(filtersObject); - const { canManage } = listOptions; const hasQuery = query !== "" || canManage !== undefined; const hasFilters = !_.isEmpty(filters); @@ -57,18 +57,51 @@ export async function getUserList(d2, filtersObject, listOptions) { .map(users => users.map(user => user.id)); const allIds = _.intersection(...groupOfUserIds); - const { pager, objects: ids } = paginate(allIds, listOptions); + const sortedUsers = await getSortedUsers(d2, allIds, listOptions.order); + const { pager, objects: pageObjects } = paginate(sortedUsers, listOptions); - const usersLists = await mapPromise(_.chunk(ids, maxUids), idsGroup => { + const usersLists = await mapPromise(getChunks(pageObjects), pageObjectsGroup => { return getD2Users(d2, { - order: listOptions.order, fields: queryFields, - filters: [{ field: "id", operator: "in", value: idsGroup }], + filters: [{ field: "id", operator: "in", value: pageObjectsGroup.map(u => u.id) }], paging: false, }); }); - return { pager, users: _.flatten(usersLists) }; + return { pager, users: sortObjectsByReference(pageObjects, _.flatten(usersLists), "id") }; +} + +function getChunks(objs) { + return _(objs) + .chunk(maxUids) + .take(10) // Limit the total chunks + .value(); +} + +function sortObjectsByReference(referenceObjects, unsortedObjects, idField) { + const ids = referenceObjects.map(obj => obj[idField]); + + return _(unsortedObjects) + .keyBy(obj => obj[idField]) + .at(...ids) + .compact() + .value(); +} + +async function getSortedUsers(d2, userIds, order) { + if (order) { + const [orderField = "name", d2Direction = "asc"] = order.split(":"); + const direction = d2Direction.toLowerCase().includes("desc") ? "desc" : "asc"; + + const users = await getD2Users(d2, { + fields: ["id", orderField], + filters: [{ field: "id", operator: "in", value: userIds }], + }); + + return _.orderBy(users, [u => (u[orderField] || "").toString().toLowerCase()], [direction]); + } else { + return userIds.map(id => ({ id })); + } } // Record -> Array<{field: string, operator: Operator, Value: value}> @@ -81,7 +114,7 @@ function getFiltersFromObject(filtersObject) { } // To be used when DHIS2 fixes all the API bugs */ -async function getUserListStandard(d2, filtersObject, listOptions) { +async function _getUserListStandard(d2, filtersObject, listOptions) { const collection = await d2.models.user.list({ ..._.pick(listOptions, ["order", "page", "pageSize", "query", "canManage"]), filter: buildD2Filter(getFiltersFromObject(filtersObject)), @@ -146,7 +179,7 @@ async function getFilteredUsers(d2, filters) { const userIdsForFilter = []; // For each filter, we can still have too many UIDs, split the requests and perform a union - for (const valuesGroup of _.chunk(inFilter.value, maxUids)) { + for (const valuesGroup of getChunks(inFilter.value)) { const filtersForGroup = [...nonInFilters, { ...inFilter, value: valuesGroup }]; const usersForGroup = await getD2Users(d2, { filters: filtersForGroup }); userIdsForFilter.push(usersForGroup.map(u => u.id)); From df5255c53c74eeba4c8225027f8598e8e15b32bb Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Jun 2021 20:08:27 +0200 Subject: [PATCH 26/30] Reduce extra 414-uri reserved space --- src/models/userList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/userList.js b/src/models/userList.js index fd89f70..5f133f6 100644 --- a/src/models/userList.js +++ b/src/models/userList.js @@ -21,7 +21,7 @@ const queryFields = [ ]; // (maxSize - urlAndOtherParamsSize) / (uidSize + encodedCommaSize) -const maxUids = Math.floor((4096 - 1000) / (11 + 3)); +const maxUids = Math.floor((4096 - 200) / (11 + 3)); // type FiltersObject = Record // type ListOptions = {canManage?: boolean, query?: string, order?: string, page: number, pageSize: number } From bde088e2a82085a31de64ebd327bb69d34339a4a Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Jun 2021 20:08:55 +0200 Subject: [PATCH 27/30] Split request in sorting --- src/models/userList.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/models/userList.js b/src/models/userList.js index 5f133f6..838e35e 100644 --- a/src/models/userList.js +++ b/src/models/userList.js @@ -60,15 +60,23 @@ export async function getUserList(d2, filtersObject, listOptions) { const sortedUsers = await getSortedUsers(d2, allIds, listOptions.order); const { pager, objects: pageObjects } = paginate(sortedUsers, listOptions); - const usersLists = await mapPromise(getChunks(pageObjects), pageObjectsGroup => { + const users = await request(pageObjects, usersGroup => { return getD2Users(d2, { fields: queryFields, - filters: [{ field: "id", operator: "in", value: pageObjectsGroup.map(u => u.id) }], + filters: [{ field: "id", operator: "in", value: usersGroup.map(u => u.id) }], paging: false, }); }); - return { pager, users: sortObjectsByReference(pageObjects, _.flatten(usersLists), "id") }; + return { pager, users: sortObjectsByReference(pageObjects, users, "id") }; +} + +async function request(objects, getRequest) { + const objectsList = await mapPromise(getChunks(objects), objectsGroup => { + return getRequest(objectsGroup); + }); + + return _.flatten(objectsList); } function getChunks(objs) { @@ -93,9 +101,11 @@ async function getSortedUsers(d2, userIds, order) { const [orderField = "name", d2Direction = "asc"] = order.split(":"); const direction = d2Direction.toLowerCase().includes("desc") ? "desc" : "asc"; - const users = await getD2Users(d2, { - fields: ["id", orderField], - filters: [{ field: "id", operator: "in", value: userIds }], + const users = await request(userIds, userIdsGroup => { + return getD2Users(d2, { + fields: ["id", orderField], + filters: [{ field: "id", operator: "in", value: userIdsGroup }], + }); }); return _.orderBy(users, [u => (u[orderField] || "").toString().toLowerCase()], [direction]); From 25d562be11fb45d2f37c775f11baae05471d4a1b Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 11 Jun 2021 22:03:58 +0200 Subject: [PATCH 28/30] Use getUserListStandard when no filters enabled --- src/models/userList.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/models/userList.js b/src/models/userList.js index 838e35e..00bf5f5 100644 --- a/src/models/userList.js +++ b/src/models/userList.js @@ -48,6 +48,10 @@ export async function getUserList(d2, filtersObject, listOptions) { const hasQuery = query !== "" || canManage !== undefined; const hasFilters = !_.isEmpty(filters); + if (!hasFilters) { + return getUserListStandard(d2, filtersObject, listOptions); + } + const usersByQuery = hasQuery ? await getD2Users(d2, { query, canManage }) : null; const usersByFilters = hasFilters ? await getFilteredUsers(d2, filters) : null; const allUsers = !hasQuery && !hasFilters ? await getD2Users(d2, {}) : null; @@ -82,7 +86,7 @@ async function request(objects, getRequest) { function getChunks(objs) { return _(objs) .chunk(maxUids) - .take(10) // Limit the total chunks + .take(50) // Limit the total chunks .value(); } @@ -124,10 +128,11 @@ function getFiltersFromObject(filtersObject) { } // To be used when DHIS2 fixes all the API bugs */ -async function _getUserListStandard(d2, filtersObject, listOptions) { +async function getUserListStandard(d2, filtersObject, listOptions) { + const filter = buildD2Filter(getFiltersFromObject(filtersObject)); const collection = await d2.models.user.list({ ..._.pick(listOptions, ["order", "page", "pageSize", "query", "canManage"]), - filter: buildD2Filter(getFiltersFromObject(filtersObject)), + ...(!_.isEmpty(filter) ? { filter } : {}), fields: queryFields.join(","), paging: true, }); From 5d91c087a8095b9626649e60e8c0a3696afede16 Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Tue, 15 Jun 2021 13:24:22 +0200 Subject: [PATCH 29/30] Refactor exportTemplateToCsv with custom columns --- src/List/list.store.js | 2 +- src/components/ImportExport.component.js | 9 ++++----- src/models/userHelpers.js | 20 +++++++++++++++++++- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/List/list.store.js b/src/List/list.store.js index 6b36f1d..7311f1e 100644 --- a/src/List/list.store.js +++ b/src/List/list.store.js @@ -9,7 +9,7 @@ import { getUserList } from "../models/userList"; const orderForQuery = modelName => modelName === "organisationUnitLevel" ? "level:ASC" : "name:iasc"; -const columns = [ +export const columns = [ { name: "username", sortable: false }, { name: "firstName", sortable: true }, { name: "surname", sortable: true }, diff --git a/src/components/ImportExport.component.js b/src/components/ImportExport.component.js index b2c4e8b..535424b 100644 --- a/src/components/ImportExport.component.js +++ b/src/components/ImportExport.component.js @@ -11,7 +11,7 @@ import FileSaver from "file-saver"; import moment from "moment"; import fileDialog from "file-dialog"; -import { exportToCsv, importFromCsv } from "../models/userHelpers"; +import { exportToCsv, exportTemplateToCsv, importFromCsv } from "../models/userHelpers"; import snackActions from "../Snackbar/snack.actions"; import ModalLoadingMask from "./ModalLoadingMask.component"; @@ -77,13 +77,12 @@ class ImportExport extends React.Component { } }; - exportEmptyTemplate = () => { - const { allColumns } = this.props; + exportEmptyTemplate = async () => { this.setState({ isProcessing: true }); try { - const labeledColumns = allColumns.map(column => column.text); - this.saveCsv(labeledColumns, "empty-user-template"); + const csvString = await exportTemplateToCsv(d2); + this.saveCsv(csvString, "empty-user-template"); } finally { this.closeMenu(); this.setState({ isProcessing: false }); diff --git a/src/models/userHelpers.js b/src/models/userHelpers.js index 454a4a5..021e8cd 100644 --- a/src/models/userHelpers.js +++ b/src/models/userHelpers.js @@ -6,6 +6,7 @@ import { generateUid } from "d2/lib/uid"; import { mapPromise, listWithInFilter } from "../utils/dhis2Helpers"; import { getUserList } from "./userList"; +import { columns } from "../List/list.store"; // Delimiter to use in multiple-value fields (roles, groups, orgUnits) const fieldSplitChar = "||"; @@ -29,7 +30,6 @@ const queryFields = [ "dataViewOrganisationUnits[id,code,shortName,displayName]", ].join(","); - const requiredPropertiesOnImport = ["username", "password", "firstName", "surname"]; const propertiesIgnoredOnImport = ["id", "created", "lastUpdated", "lastLogin"]; @@ -538,6 +538,23 @@ async function exportToCsv(d2, columns, filterOptions, { orgUnitsField }) { return Papa.unparse(table); } +async function exportTemplateToCsv(d2) { + const columnsAdded = ["password"]; + const columnsRemoved = ["lastUpdated", "created", "lastLogin"]; + const columnKeysToExport = _(columns) + .map(column => column.name) + .difference(columnsRemoved) + .union(columnsAdded) + .value(); + const header = _(columnKeysToExport) + .map(getColumnNameFromProperty) + .compact() + .value(); + const table = [header]; + + return Papa.unparse(table); +} + async function importFromCsv(d2, file, { maxUsers, orgUnitsField }) { return new Promise((resolve, reject) => { Papa.parse(file, { @@ -622,6 +639,7 @@ function getPayload(parentUser, destUsers, fields, updateStrategy) { export { exportToCsv, + exportTemplateToCsv, importFromCsv, updateUsers, saveUsers, From 781f0c5a25604b1628ab0313b4e5f6671df0b3ca Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Tue, 15 Jun 2021 13:33:29 +0200 Subject: [PATCH 30/30] Fix missing traslation function in OrgUnitsFilter.component.js --- src/components/OrgUnitsFilter.component.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/OrgUnitsFilter.component.js b/src/components/OrgUnitsFilter.component.js index ce3a87f..66befd5 100644 --- a/src/components/OrgUnitsFilter.component.js +++ b/src/components/OrgUnitsFilter.component.js @@ -102,6 +102,7 @@ class OrgUnitsFilter extends React.Component { render() { const { title, styles } = this.props; const { dialogOpen, selected } = this.state; + const t = this.getTranslation.bind(this); return (
@@ -122,13 +123,13 @@ class OrgUnitsFilter extends React.Component { intersectionPolicy={true} filteringByNameLabel={ title.includes("organisation units capture") - ? "filter_organisation_units_capture_by_name" - : "filter_organisation_units_output_by_name" + ? t("filter_organisation_units_capture_by_name") + : t("filter_organisation_units_output_by_name") } orgUnitsSelectedLabel={ title.includes("organisation units capture") - ? "organisation_units_capture_selected" - : "organisation_units_output_selected" + ? t("organisation_units_capture_selected") + : t("organisation_units_output_selected") } />