Skip to content

Commit

Permalink
Release 1.2.0
Browse files Browse the repository at this point in the history
Merge remote-tracking branch 'upstream/master'
  • Loading branch information
ifoche committed Feb 15, 2023
2 parents 55f9b66 + 98e6be4 commit 46c1c30
Show file tree
Hide file tree
Showing 17 changed files with 210 additions and 62 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,7 @@ cypress/fixtures/

# IntelliJ
.idea/*

# VSCode
.vscode
remote-debug-profile
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "user-extended-app",
"description": "DHIS2 Extended User app",
"version": "1.1.1",
"version": "1.2.0",
"license": "GPL-3.0",
"author": "EyeSeeTea team",
"homepage": ".",
Expand Down
4 changes: 3 additions & 1 deletion public/old-i18n/i18n_module_ar.properties
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ import_export=Import/Export
import=Import
export=Export
table_exported=Table exported
export_to_CSV=Export to CSV
export_to_JSON=Export to JSON
layout_settings=Layout settings
warnings=$$n$$ warning(s) while importing CSV
warnings=$$n$$ warning(s) while importing file
and_n_more_warnings=[... and $$n$$ more warning(s) ...]
errors_on_table=$$n$$ invalid users found, check in-line errors in table
import_successful=$$n$$ users successfully imported
Expand Down
4 changes: 3 additions & 1 deletion public/old-i18n/i18n_module_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,10 @@ import_export=Import/Export
import=Import
export=Export
table_exported=Table exported
export_to_CSV=Export to CSV
export_to_JSON=Export to JSON
layout_settings=Layout settings
warnings=$$n$$ warning(s) while importing CSV
warnings=$$n$$ warning(s) while importing file
and_n_more_warnings=[... and $$n$$ more warning(s) ...]
errors_on_table=$$n$$ invalid users found, check in-line errors in table
import_successful=$$n$$ users successfully imported
Expand Down
4 changes: 3 additions & 1 deletion public/old-i18n/i18n_module_es.properties
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ import_export=Importar/Exportar
import=Importar
export=Exportar
table_exported=Tabla exportada
export_to_CSV=Exportar a CSV
export_to_JSON=Exportar a JSON
layout_settings=Opciones de columnas
warnings=$$n$$ avisos(s) al importar el CSV
warnings=$$n$$ avisos(s) al importar el archivo
and_n_more_warnings=[... y $$n$$ avisos más ...]
errors_on_table=$$n$$ usuarios inválidos, compruebe los errores marcados en la tabla
import_successful=$$n$$ usuarios creados con éxito
Expand Down
4 changes: 3 additions & 1 deletion public/old-i18n/i18n_module_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,11 @@ multiple_matches=Multiple matches found
import_export=Import/Export
import=Import
export=Export
export_to_CSV=Exporter vers CSV
export_to_JSON=Exporter vers JSON
table_exported=Table exported
layout_settings=Layout settings
warnings=$$n$$ warning(s) while importing CSV
warnings=$$n$$ warning(s) while importing file
and_n_more_warnings=[... and $$n$$ more warning(s) ...]
errors_on_table=$$n$$ invalid users, check in-line errors in table
import_successful=$$n$$ users successfully imported
Expand Down
5 changes: 4 additions & 1 deletion src/data/repositories/UserD2ApiRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class UserD2ApiRepository implements UserRepository {
pageSize,
search,
sorting = { field: "firstName", order: "asc" },
canManage,
rootJunction,
filters,
} = options;
Expand All @@ -46,6 +47,7 @@ export class UserD2ApiRepository implements UserRepository {
page,
pageSize,
query: search !== "" ? search : undefined,
canManage: canManage === "true" ? "true" : undefined,
filter: otherFilters,
rootJunction: areFiltersEnabled ? rootJunction : undefined,
order: `${sorting.field}:${sorting.order}`,
Expand All @@ -57,14 +59,15 @@ export class UserD2ApiRepository implements UserRepository {
}

public listAllIds(options: ListOptions): FutureData<string[]> {
const { search, sorting = { field: "firstName", order: "asc" }, filters } = options;
const { search, sorting = { field: "firstName", order: "asc" }, filters, canManage } = options;
const otherFilters = _.mapValues(filters, items => (items ? { [items[0]]: items[1] } : undefined));

return apiToFuture(
this.api.models.users.get({
fields: { id: true },
paging: false,
query: search !== "" ? search : undefined,
canManage: canManage === "true" ? "true" : undefined,
filter: otherFilters,
order: `${sorting.field}:${sorting.order}`,
})
Expand Down
1 change: 1 addition & 0 deletions src/domain/repositories/UserRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface ListOptions {
search?: string;
sorting?: { field: string; order: "asc" | "desc" };
filters?: ListFilters;
canManage?: string;
rootJunction?: "AND" | "OR";
}

Expand Down
20 changes: 14 additions & 6 deletions src/legacy/List/List.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ export class ListHybrid extends React.Component {
this.setState({ disableUsers: { open: true, users: existingUsers, action } });
});

const deleteUserStoreDisposable = deleteUserStore.subscribe(async ({ datasets }) => {
if (datasets !== undefined) {
const deleteUserStoreDisposable = deleteUserStore.subscribe(async ({ users }) => {
if (users !== undefined) {
const existingUsers = await getExistingUsers(this.context.d2, {
fields: ":owner",
filter: "id:in:[" + datasets.join(",") + "]",
fields: ":owner,userCredentials",
filter: "id:in:[" + users.join(",") + "]",
});
this.setState({ removeUsers: { open: true, users: existingUsers } });
}
Expand All @@ -158,7 +158,13 @@ export class ListHybrid extends React.Component {
setUsersEnableState = async (users, action) => {
const newValue = action === "disable";
const response = await updateUsers(this.context.d2, users, user => {
return user.userCredentials.disabled !== newValue ? set("userCredentials.disabled", newValue, user) : null;
if (user?.userCredentials?.disabled !== newValue) {
return set("userCredentials.disabled", newValue, user);
} else if (user?.disabled !== newValue) {
return set("disabled", newValue, user);
} else {
return null;
}
});

if (response.success) {
Expand Down Expand Up @@ -293,7 +299,8 @@ export class ListHybrid extends React.Component {
};

_onFiltersChange = filters => {
this.setState({ filters }, this.filterList);
const canManage = filters.canManage;
this.setState({ filters, canManage }, this.filterList);
};

_disableUsersSaved = () => this.setUsersEnableState(this.state.disableUsers.users, this.state.disableUsers.action);
Expand Down Expand Up @@ -329,6 +336,7 @@ export class ListHybrid extends React.Component {
loading={this.state.isLoading}
openSettings={this._openSettings}
filters={this.state.filters?.filters}
canManage={this.state?.canManage}
rootJunction={this.state.filters?.rootJunction}
onChangeVisibleColumns={this._updateVisibleColumns}
onChangeSearch={this._updateQuery}
Expand Down
2 changes: 1 addition & 1 deletion src/legacy/List/context.actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export async function assignToOrgUnits(userIds, field, titleKey) {
filter: `id:in:[${userIds.join(",")}]`,
};
const users = (await d2.models.users.list(listOptions)).toArray();
const usernames = users.map(user => user.userCredentials.username);
const usernames = users.map(user => (user.userCredentials ? user.userCredentials.username : user.username));
const info = _m.joinString(d2.i18n.getTranslation.bind(d2.i18n), usernames, 3, ", ");
const userOrgUnitRoots = await getOrgUnitsRoots();

Expand Down
64 changes: 49 additions & 15 deletions src/legacy/components/ImportExport.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import Popover from "material-ui/Popover/Popover";
import Menu from "material-ui/Menu/Menu";
import MenuItem from "material-ui/MenuItem/MenuItem";
import ImportExportIcon from "material-ui/svg-icons/communication/import-export";
import ExportIcon from "material-ui/svg-icons/navigation/arrow-upward";
import ImportIcon from "material-ui/svg-icons/navigation/arrow-downward";
import ImportIcon from "material-ui/svg-icons/navigation/arrow-upward";
import ExportIcon from "material-ui/svg-icons/navigation/arrow-downward";
import FileSaver from "file-saver";
import moment from "moment";
import fileDialog from "file-dialog";

import { exportToCsv, exportTemplateToCsv, importFromCsv } from "../models/userHelpers";
import { exportTemplateToCsv, importFromCsv, importFromJson, exportUsers } from "../models/userHelpers";
import snackActions from "../Snackbar/snack.actions";
import ModalLoadingMask from "./ModalLoadingMask.component";

Expand Down Expand Up @@ -63,8 +63,22 @@ class ImportExport extends React.Component {
this.setState({ isProcessing: true });

try {
const csvString = await exportToCsv(d2, columns, filterOptions, { orgUnitsField });
this.saveCsv(csvString, "users");
const csvString = await exportUsers(d2, columns, filterOptions, { orgUnitsField }, false);
this.saveFile(csvString, "users", "csv");
} finally {
this.closeMenu();
this.setState({ isProcessing: false });
}
};

exportToJsonAndSave = async () => {
const { d2, columns, filterOptions, settings } = this.props;
const orgUnitsField = settings.get("organisationUnitsField");
this.setState({ isProcessing: true });

try {
const jsonString = await exportUsers(d2, columns, filterOptions, { orgUnitsField }, true);
this.saveFile(jsonString, "users", "json");
} finally {
this.closeMenu();
this.setState({ isProcessing: false });
Expand All @@ -76,29 +90,33 @@ class ImportExport extends React.Component {

try {
const csvString = await exportTemplateToCsv(this.props.d2);
this.saveCsv(csvString, "empty-user-template");
this.saveFile(csvString, "empty-user-template", "csv");
} finally {
this.closeMenu();
this.setState({ isProcessing: false });
}
};

saveCsv = (contents, name) => {
saveFile = (contents, name, fileType) => {
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`;
const filename = `${name}-${datetime}.${fileType}`;
FileSaver.saveAs(blob, filename);
snackActions.show({ message: `${this.t("table_exported")}: ${filename}` });
};

importFromCsv = () => {
importFromFile = () => {
const { onImport, maxUsers, settings } = this.props;
const orgUnitsField = settings.get("organisationUnitsField");

fileDialog({ accept: ".csv" })
fileDialog({ accept: ["text/csv", "application/json"] })
.then(files => {
this.setState({ isProcessing: true });
return importFromCsv(this.props.d2, files[0], { maxUsers, orgUnitsField });
if (files[0].type === "text/csv") {
return importFromCsv(this.props.d2, files[0], { maxUsers, orgUnitsField });
} else if (files[0].type === "application/json") {
return importFromJson(this.props.d2, files.item(0), { maxUsers, orgUnitsField });
}
})
.then(result => onImport(result))
.catch(err => snackActions.show({ message: err.toString() }))
Expand All @@ -110,7 +128,14 @@ class ImportExport extends React.Component {

render() {
const { isMenuOpen, anchorEl, isProcessing } = this.state;
const { popoverConfig, closeMenu, importFromCsv, exportToCsvAndSave, exportEmptyTemplate } = this;
const {
popoverConfig,
closeMenu,
importFromFile,
exportToCsvAndSave,
exportEmptyTemplate,
exportToJsonAndSave,
} = this;
const { t } = this;

return (
Expand All @@ -129,10 +154,19 @@ class ImportExport extends React.Component {
onRequestClose={closeMenu}
>
<Menu>
<MenuItem leftIcon={<ExportIcon />} primaryText={t("import")} onClick={importFromCsv} />
<MenuItem leftIcon={<ImportIcon />} primaryText={t("export")} onClick={exportToCsvAndSave} />
<MenuItem leftIcon={<ImportIcon />} primaryText={t("import")} onClick={importFromFile} />
<MenuItem
leftIcon={<ExportIcon />}
primaryText={t("export_to_CSV")}
onClick={exportToCsvAndSave}
/>
<MenuItem
leftIcon={<ExportIcon />}
primaryText={t("export_to_JSON")}
onClick={exportToJsonAndSave}
/>
<MenuItem
leftIcon={<ImportIcon />}
leftIcon={<ExportIcon />}
primaryText={t("export_empty_template")}
onClick={exportEmptyTemplate}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class CopyInUserBatchModelsMultiSelectModel {
}
async getUserInfo(ids) {
const users = await getExistingUsers(this.d2, {
fields: ":owner,userGroups[id]",
fields: ":owner,userCredentials,userGroups[id]",
filter: "id:in:[" + ids.join(",") + "]",
});
return users;
Expand All @@ -48,6 +48,7 @@ export default class CopyInUserBatchModelsMultiSelectModel {
copyAccessElements,
updateStrategy
);
if (!payload.success) throw new Error(`${payload.error}`);
return payload;
}

Expand Down
24 changes: 19 additions & 5 deletions src/legacy/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,42 @@ class User {

const newUsersAttributes = newUserFields.map(userFields => ({
id: userFields.id,
username: userFields.username,
email: optional(userFields.email),
firstName: optional(userFields.firstName),
surname: optional(userFields.surname),
userCredentials: {
id: generateUid(),
openId: nullable(userFields.openId),
ldapId: nullable(userFields.ldapId),
code: nullable(userFields.code),
userInfo: { id: userFields.id },
username: userFields.username,
password: userFields.password,
},
organisationUnits: optional(userFields.organisationUnits),
dataViewOrganisationUnits: optional(userFields.dataViewOrganisationUnits),
organisationUnits: userFields.organisationUnits?.map(item => ({ id: optional(item.id) })),
dataViewOrganisationUnits: userFields.dataViewOrganisationUnits?.map(item => ({ id: optional(item.id) })),
}));

return this.replicate(newUsersAttributes);
}

async replicate(newUsersAttributes) {
const ownedProperties = this.d2.models.user.getOwnedPropertyNames();
/*
NOTE:
externalAuth makes the replicate function fail because the IDs has to be unique
lastLogin, createdBy and created should not be copied from original user
*/
const unusedProperties = ["externalAuth", "openId", "ldapId", "lastLogin", "created", "createdBy"];
const ownedProperties = this.d2.models.user
.getOwnedPropertyNames()
.filter(item => !unusedProperties.includes(item));
if (!ownedProperties.includes("userCredentials")) ownedProperties.push("userCredentials");
const userJson = pick(ownedProperties, this.attributes);

if (userJson.userCredentials?.lastLogin !== undefined) delete userJson.userCredentials.lastLogin;
if (userJson.userCredentials?.lastUpdatedBy !== undefined) delete userJson.userCredentials.lastUpdatedBy;
if (userJson.userCredentials?.createdBy !== undefined) delete userJson.userCredentials.createdBy;
if (userJson.userCredentials?.user !== undefined) delete userJson.userCredentials.user;

const newUsers = newUsersAttributes.map(newUserAttributes => merge(userJson, newUserAttributes));
const userGroupIds = this.attributes.userGroups.map(userGroup => userGroup.id);
const { userGroups } = await this.api.get("/userGroups", {
Expand Down
Loading

0 comments on commit 46c1c30

Please sign in to comment.