diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..02d3b5e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,69 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "development", "master" ] + pull_request: + branches: [ "development", "master" ] + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: self-hosted + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" diff --git a/i18n/en.pot b/i18n/en.pot index d8685d4..920fbcd 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2024-09-12T09:58:13.550Z\n" -"PO-Revision-Date: 2024-09-12T09:58:13.550Z\n" +"POT-Creation-Date: 2024-12-17T16:04:30.663Z\n" +"PO-Revision-Date: 2024-12-17T16:04:30.663Z\n" msgid "Events - Create/update" msgstr "" @@ -150,6 +150,15 @@ msgid "" "update the app." msgstr "" +msgid "Cancel" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Info" +msgstr "" + msgid "Autogenerated" msgstr "" @@ -197,9 +206,6 @@ msgstr "" msgid "Accept" msgstr "" -msgid "Cancel" -msgstr "" - msgid "All users allowed" msgstr "" @@ -1063,16 +1069,38 @@ msgstr "" msgid "" "There are {{totalExisting}} data values in the database for this " -"organisation unit and periods. If you proceed, all those data values will " -"be deleted and only the ones in the spreadsheet will be saved. Are you sure?" +"organisation unit and periods. Would you like to import only the new data " +"values, import new values and update existing ones, or completely delete " +"existing values before importing the data?" msgstr "" -msgid "Proceed" +msgid "Delete and Import" msgstr "" msgid "Import only new data values" msgstr "" +msgid "Import and Update" +msgstr "" + +msgid "" +"All data values in the spreadsheet will be imported to the system, but any " +"data that was existing for such organisation unit and periods in the system " +"will be deleted first, so none will be kept before doing the import." +msgstr "" + +msgid "" +"Import only new data values, without updating nor deleting any existing " +"one. Only values in the spreadsheet that do not currently exist in the " +"system will be imported" +msgstr "" + +msgid "" +"Import new data values and also update existing ones. All data values in " +"the spreadsheet will be imported to the system, but other data values " +"present in the system that are not provided in the spreadsheet will be kept." +msgstr "" + msgid "Warning: Your upload may result in the generation of duplicates" msgstr "" @@ -1100,6 +1128,9 @@ msgid "" "You can still download them and send them to your administrator." msgstr "" +msgid "Proceed" +msgstr "" + msgid "Download data values with invalid organisation units" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index b592ad8..c53b31c 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Bulk Load\n" -"POT-Creation-Date: 2024-09-12T09:58:13.550Z\n" +"POT-Creation-Date: 2024-12-17T16:04:30.663Z\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -170,6 +170,15 @@ msgstr "" "de la app ({{appVersion}}), no puede continuar. Póngase en contacto con el " "administrador para actualizar la app." +msgid "Cancel" +msgstr "Cancelar" + +msgid "Save" +msgstr "" + +msgid "Info" +msgstr "" + msgid "Autogenerated" msgstr "" @@ -219,9 +228,6 @@ msgstr "" msgid "Accept" msgstr "Aceptar" -msgid "Cancel" -msgstr "Cancelar" - msgid "All users allowed" msgstr "Permitir todos los usuarios" @@ -1101,22 +1107,46 @@ msgstr "Importando datos..." msgid "Existing data values" msgstr "Existen valores para estos elementos de datos" +#, fuzzy msgid "" "There are {{totalExisting}} data values in the database for this " -"organisation unit and periods. If you proceed, all those data values will be " -"deleted and only the ones in the spreadsheet will be saved. Are you sure?" +"organisation unit and periods. Would you like to import only the new data " +"values, import new values and update existing ones, or completely delete " +"existing values before importing the data?" msgstr "" "Existen {{totalExisting}} valores en la base de datos para esta unidad " "organizativa y periodos. Si continua, todos los valores se borrarán y se " "remplazarán por aquellos presentes en el archivo. ¿Está seguro que desea " "continuar?" -msgid "Proceed" -msgstr "Continuar" +msgid "Delete and Import" +msgstr "" msgid "Import only new data values" msgstr "Importar solo nuevos valores de datos" +#, fuzzy +msgid "Import and Update" +msgstr "Importar datos" + +msgid "" +"All data values in the spreadsheet will be imported to the system, but any " +"data that was existing for such organisation unit and periods in the system " +"will be deleted first, so none will be kept before doing the import." +msgstr "" + +msgid "" +"Import only new data values, without updating nor deleting any existing one. " +"Only values in the spreadsheet that do not currently exist in the system " +"will be imported" +msgstr "" + +msgid "" +"Import new data values and also update existing ones. All data values in the " +"spreadsheet will be imported to the system, but other data values present in " +"the system that are not provided in the spreadsheet will be kept." +msgstr "" + msgid "Warning: Your upload may result in the generation of duplicates" msgstr "Alerta: Su importación puede generar duplicados" @@ -1150,6 +1180,9 @@ msgstr "" "válida que serán ignorados durante la importación.\n" "Aun así, puede descargarlos y enviarlos a su administrador." +msgid "Proceed" +msgstr "Continuar" + msgid "Download data values with invalid organisation units" msgstr "Descargar datos con unidades organizativas no válidas" diff --git a/i18n/fr.po b/i18n/fr.po index b443449..6f04da2 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Bulk Load App\n" -"POT-Creation-Date: 2024-09-12T09:58:13.550Z\n" +"POT-Creation-Date: 2024-12-17T16:04:30.663Z\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -155,6 +155,15 @@ msgid "" "update the app." msgstr "" +msgid "Cancel" +msgstr "Annuler" + +msgid "Save" +msgstr "" + +msgid "Info" +msgstr "" + msgid "Autogenerated" msgstr "" @@ -204,9 +213,6 @@ msgstr "" msgid "Accept" msgstr "" -msgid "Cancel" -msgstr "Annuler" - msgid "All users allowed" msgstr "" @@ -1124,22 +1130,46 @@ msgstr "Importation de données en cours ..." msgid "Existing data values" msgstr "Valeurs de données existantes" +#, fuzzy msgid "" "There are {{totalExisting}} data values in the database for this " -"organisation unit and periods. If you proceed, all those data values will be " -"deleted and only the ones in the spreadsheet will be saved. Are you sure?" +"organisation unit and periods. Would you like to import only the new data " +"values, import new values and update existing ones, or completely delete " +"existing values before importing the data?" msgstr "" "Il y a {{totalExisting}} valeurs de données dans la base de données pour " "cette unité d'organisation et ces périodes. Si vous continuez, toutes ces " "valeurs de données seront supprimées et seules celles de la feuille de " "calcul seront enregistrées. Êtes-vous sur de vouloir continuer?" -msgid "Proceed" -msgstr "Procéder" +msgid "Delete and Import" +msgstr "" msgid "Import only new data values" msgstr "Importer uniquement de nouvelles données" +#, fuzzy +msgid "Import and Update" +msgstr "Importer des données" + +msgid "" +"All data values in the spreadsheet will be imported to the system, but any " +"data that was existing for such organisation unit and periods in the system " +"will be deleted first, so none will be kept before doing the import." +msgstr "" + +msgid "" +"Import only new data values, without updating nor deleting any existing one. " +"Only values in the spreadsheet that do not currently exist in the system " +"will be imported" +msgstr "" + +msgid "" +"Import new data values and also update existing ones. All data values in the " +"spreadsheet will be imported to the system, but other data values present in " +"the system that are not provided in the spreadsheet will be kept." +msgstr "" + msgid "Warning: Your upload may result in the generation of duplicates" msgstr "" "Avertissement: votre téléchargement peut provoquer la génération de doublons" @@ -1172,6 +1202,9 @@ msgid "" "You can still download them and send them to your administrator." msgstr "" +msgid "Proceed" +msgstr "Procéder" + msgid "Download data values with invalid organisation units" msgstr "" "Télécharger des valeurs de données avec des unités d'organisation non valides" diff --git a/i18n/pt.po b/i18n/pt.po index ae1d341..dcefb9e 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Bulk Load\n" -"POT-Creation-Date: 2024-09-12T09:58:13.550Z\n" +"POT-Creation-Date: 2024-12-17T16:04:30.663Z\n" "Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -174,6 +174,15 @@ msgstr "" "aplicativo ({{appVersion}}), não pode continuar. Por favor contacte o " "administrador para actualizar a aplicação." +msgid "Cancel" +msgstr "Cancelar" + +msgid "Save" +msgstr "" + +msgid "Info" +msgstr "" + msgid "Autogenerated" msgstr "" @@ -225,9 +234,6 @@ msgstr "" msgid "Accept" msgstr "Aceite" -msgid "Cancel" -msgstr "Cancelar" - msgid "All users allowed" msgstr "Todos os usuários permitidos" @@ -1158,22 +1164,46 @@ msgstr "Importando dados ..." msgid "Existing data values" msgstr "Valores de dados existentes" +#, fuzzy msgid "" "There are {{totalExisting}} data values in the database for this " -"organisation unit and periods. If you proceed, all those data values will be " -"deleted and only the ones in the spreadsheet will be saved. Are you sure?" +"organisation unit and periods. Would you like to import only the new data " +"values, import new values and update existing ones, or completely delete " +"existing values before importing the data?" msgstr "" "Existem {{totalExisting}} valores de dados no banco de dados para esta " "unidade e períodos da organização. Se você continuar, todos esses valores de " "dados serão excluídos e remplazados pelos dados da planilha. Tem certeza que " "deseja continuar?" -msgid "Proceed" -msgstr "Continuar" +msgid "Delete and Import" +msgstr "" msgid "Import only new data values" msgstr "Importar apenas novos valores de dados" +#, fuzzy +msgid "Import and Update" +msgstr "Importar dados" + +msgid "" +"All data values in the spreadsheet will be imported to the system, but any " +"data that was existing for such organisation unit and periods in the system " +"will be deleted first, so none will be kept before doing the import." +msgstr "" + +msgid "" +"Import only new data values, without updating nor deleting any existing one. " +"Only values in the spreadsheet that do not currently exist in the system " +"will be imported" +msgstr "" + +msgid "" +"Import new data values and also update existing ones. All data values in the " +"spreadsheet will be imported to the system, but other data values present in " +"the system that are not provided in the spreadsheet will be kept." +msgstr "" + msgid "Warning: Your upload may result in the generation of duplicates" msgstr "Aviso: seu upload pode resultar na geração de duplicados" @@ -1207,6 +1237,9 @@ msgstr "" "inválida que serão ignorados durante a importação.\n" "Você ainda pode baixá-los e enviá-los ao seu administrador." +msgid "Proceed" +msgstr "Continuar" + msgid "Download data values with invalid organisation units" msgstr "Descarregar dados com unidades organizacionais inválidas" diff --git a/i18n/ru.po b/i18n/ru.po index 49ef8a6..04f4816 100644 --- a/i18n/ru.po +++ b/i18n/ru.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: Bulk Load\n" -"POT-Creation-Date: 2024-09-12T09:58:13.550Z\n" +"POT-Creation-Date: 2024-12-17T16:04:30.663Z\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -174,6 +174,15 @@ msgstr "" "({{appVersion}}), продолжение невозможно. Пожалуйста, свяжитесь с " "администратором для обновления приложения." +msgid "Cancel" +msgstr "Отмена" + +msgid "Save" +msgstr "" + +msgid "Info" +msgstr "" + msgid "Autogenerated" msgstr "" @@ -226,9 +235,6 @@ msgstr "" msgid "Accept" msgstr "Принять" -msgid "Cancel" -msgstr "Отмена" - msgid "All users allowed" msgstr "Разрешено всем пользователям" @@ -1162,22 +1168,46 @@ msgstr "Импорт данных..." msgid "Existing data values" msgstr "Существующие значения данных" +#, fuzzy msgid "" "There are {{totalExisting}} data values in the database for this " -"organisation unit and periods. If you proceed, all those data values will be " -"deleted and only the ones in the spreadsheet will be saved. Are you sure?" +"organisation unit and periods. Would you like to import only the new data " +"values, import new values and update existing ones, or completely delete " +"existing values before importing the data?" msgstr "" "В базе данных имеются значения данных {{totalExisting}} для этой " "организационной единицы и периодов. Если вы продолжите, все эти значения " "данных будут удалены, и будут сохранены только те, которые находятся в " "электронной таблице. Вы уверены?" -msgid "Proceed" -msgstr "Действуйте" +msgid "Delete and Import" +msgstr "" msgid "Import only new data values" msgstr "Импортируйте только новые значения данных" +#, fuzzy +msgid "Import and Update" +msgstr "Импортные данные" + +msgid "" +"All data values in the spreadsheet will be imported to the system, but any " +"data that was existing for such organisation unit and periods in the system " +"will be deleted first, so none will be kept before doing the import." +msgstr "" + +msgid "" +"Import only new data values, without updating nor deleting any existing one. " +"Only values in the spreadsheet that do not currently exist in the system " +"will be imported" +msgstr "" + +msgid "" +"Import new data values and also update existing ones. All data values in the " +"spreadsheet will be imported to the system, but other data values present in " +"the system that are not provided in the spreadsheet will be kept." +msgstr "" + msgid "Warning: Your upload may result in the generation of duplicates" msgstr "Предупреждение: Ваша загрузка может привести к созданию дубликатов" @@ -1211,6 +1241,9 @@ msgstr "" "единицей, которые будут проигнорированы при импорте.\n" "Вы все еще можете загрузить их и отправить администратору." +msgid "Proceed" +msgstr "Действуйте" + msgid "Download data values with invalid organisation units" msgstr "Загрузка значений данных с недопустимыми организационными единицами" diff --git a/package.json b/package.json index 6a36ea1..b634951 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bulk-load", "description": "Bulk importing made easy", - "version": "3.24.3", + "version": "3.25.0", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", diff --git a/src/data/InstanceDhisRepository.ts b/src/data/InstanceDhisRepository.ts index 2f79a94..872302c 100644 --- a/src/data/InstanceDhisRepository.ts +++ b/src/data/InstanceDhisRepository.ts @@ -14,6 +14,7 @@ import { BuilderMetadata, GetDataFormsParams, GetDataPackageParams, + ImportDataPackageOptions, InstanceRepository, } from "../domain/repositories/InstanceRepository"; import i18n from "../locales"; @@ -196,10 +197,17 @@ export class InstanceDhisRepository implements InstanceRepository { return this.importAggregatedData("DELETE", dataPackage); } - public async importDataPackage(dataPackage: DataPackage): Promise { + public async importDataPackage( + dataPackage: DataPackage, + options: ImportDataPackageOptions + ): Promise { + const { createAndUpdate } = options; switch (dataPackage.type) { case "dataSets": { - const result = await this.importAggregatedData("CREATE_AND_UPDATE", dataPackage); + const result = await this.importAggregatedData( + createAndUpdate ? "CREATE_AND_UPDATE" : "CREATE", + dataPackage + ); return [result]; } case "programs": { diff --git a/src/data/templates/nrc/NRCModule1.01.ts b/src/data/templates/nrc/NRCModule1.01.ts index d60c54b..5b051ed 100644 --- a/src/data/templates/nrc/NRCModule1.01.ts +++ b/src/data/templates/nrc/NRCModule1.01.ts @@ -14,6 +14,7 @@ import { InstanceRepository } from "../../../domain/repositories/InstanceReposit import { ModulesRepositories } from "../../../domain/repositories/ModulesRepositories"; import { NRCModuleMetadataRepository } from "../../../domain/repositories/templates/NRCModuleMetadataRepository"; import { Workbook } from "../../../webapp/logic/Workbook"; +import { Maybe } from "../../../types/utils"; export class NRCModule101 implements CustomTemplateWithUrl { public readonly type = "custom"; @@ -86,12 +87,10 @@ class DownloadCustomization { await this.excelRepository.getOrCreateSheet(this.id, name); } - private getValidationCells(metadata: NRCModuleMetadata) { - const { categories } = metadata.categoryCombo; - const projectCategoryOption = categories.project.categoryOption; - - const getCells = (items: Ref[], options: { column: string; useRef?: boolean }) => { + private getValidationCells(metadata: NRCModuleMetadata): Cell[] { + const getCells = (items: Maybe, options: { column: string; useRef?: boolean }) => { const { useRef = true } = options; + if (!items) return []; return items.map((item, idx) => { return cell({ @@ -103,31 +102,66 @@ class DownloadCustomization { }); }; + const categoryOptions = this.getCategoryOptionsObj(metadata); + return _.flatten([ getCells(metadata.organisationUnits, { column: "A" }), - getCells(categories.phasesOfEmergency.categoryOptions, { column: "B" }), - getCells(categories.targetActual.categoryOptions, { column: "C" }), + getCells(categoryOptions.projects, { column: "E" }), + getCells(categoryOptions.phasesOfEmergency, { column: "B" }), + getCells(categoryOptions.targetActual, { column: "C" }), getCells(metadata.dataElements, { column: "D" }), getCells([metadata.dataSet], { column: "G" }), - getCells([projectCategoryOption], { column: "E" }), getCells(metadata.periods, { column: "F", useRef: false }), ]); } - private getSheetData(metadata: NRCModuleMetadata): WorkbookData { - const validationCells = this.getValidationCells(metadata); - const metadataCells = this.getMetadataCells(metadata); - const aocCells = this.getAttributeOptionComboCells(metadata); - const cocCells = this.getCategoryOptionComboCells(metadata); - const miscCells = [ - cell({ sheet: this.sheets.dataEntry, column: "F", row: 1, value: referenceToId(metadata.dataSet.id) }), - ]; + private getCategoryOptionsObj(metadata: NRCModuleMetadata): CategoryOptions { + const { categories } = metadata.categoryCombo; return { - cells: _.concat(miscCells, validationCells, metadataCells, aocCells, cocCells), + projects: categories.projects?.categoryOptions, + phasesOfEmergency: categories.phasesOfEmergency?.categoryOptions, + targetActual: categories.targetActual?.categoryOptions, }; } + private getSheetData(metadata: NRCModuleMetadata): WorkbookData { + const cellGroups = [ + this.getValidationCells(metadata), + this.getMetadataCells(metadata), + this.getAttributeOptionComboCells(metadata), + this.getCategoryOptionComboCells(metadata), + this.getDataSetCells(metadata), + this.getClearedOutCellsByCategories(metadata), + ]; + + return { cells: _.flatten(cellGroups) }; + } + + private getDataSetCells(metadata: NRCModuleMetadata) { + return [ + cell({ + sheet: this.sheets.dataEntry, + column: "F", + row: 1, + value: referenceToId(metadata.dataSet.id), + }), + ]; + } + + private getClearedOutCellsByCategories(metadata: NRCModuleMetadata): Cell[] { + const { categories } = metadata.categoryCombo; + + const emptyCell = (options: { row: number; column: string }) => + cell({ row: options.row, column: options.column, sheet: this.sheets.dataEntry, value: "" }); + + return _.flatten([ + categories.projects ? [] : [emptyCell({ column: "A", row: 1 }), emptyCell({ column: "B", row: 1 })], + categories.phasesOfEmergency ? [] : [emptyCell({ column: "B", row: 3 })], + categories.targetActual ? [] : [emptyCell({ column: "E", row: 3 })], + ]); + } + private getCategoryOptionComboCells(metadata: NRCModuleMetadata): Cell[] { const initialColumnIndex = Workbook.getColumnIndex("M"); @@ -159,22 +193,20 @@ class DownloadCustomization { private getAttributeOptionComboCells(metadata: NRCModuleMetadata) { const { categories } = metadata.categoryCombo; - const projectCategoryOption = categories.project.categoryOption; + const projectCategoryOptions = categories.projects?.categoryOptions; + const empty: NamedRef[] = [{ id: "", name: "EMPTY" }]; - const categoryOptionsProduct = _.product( - [categories.project.categoryOption], - categories.phasesOfEmergency.categoryOptions, - categories.targetActual.categoryOptions - ); + const groups = [ + projectCategoryOptions ? projectCategoryOptions : empty, + categories.phasesOfEmergency?.categoryOptions || empty, + categories.targetActual?.categoryOptions || empty, + ]; + + const categoryOptionsProduct = _.product(...groups); const cocsByKey = _.keyBy(metadata.categoryCombo.categoryOptionCombos, getCocKey); - const projectCategoryOptionCell = cell({ - sheet: this.sheets.dataEntry, - column: "B", - row: 1, - value: referenceToId(projectCategoryOption.id), - }); + const projectCategoryOptionCell = this.getProjectCategoryOptionCell(projectCategoryOptions); const aocCells = _(categoryOptionsProduct) .map(categoryOptions => { @@ -184,41 +216,46 @@ class DownloadCustomization { console.error(`Category option combo not found: categoryOptionIds=${key}`); return null; } else { - return { categoryOptions, categoryOptionCombo: coc }; + return { categoryOptions: categoryOptions, categoryOptionCombo: coc }; } }) .compact() .flatMap((obj, idx) => { const row = this.initialValidationRow + idx; const sum = { id: "=" + [`I${row}`, `J${row}`, `K${row}`].join(" & ") }; + const objs = [obj.categoryOptionCombo, ...obj.categoryOptions, sum]; + const sheet = this.sheets.validation; - return _.zip([obj.categoryOptionCombo, ...obj.categoryOptions, sum], ["H", "I", "J", "K", "L"]).map( - ([obj, column]) => { - if (!obj || !column) return null; - - return cell({ - sheet: this.sheets.validation, - column: column, - row: row, - value: obj.id, - }); - } - ); + return _.zip(objs, ["H", "I", "J", "K", "L"]).map(([obj, column]) => { + return !obj || !column ? null : cell({ sheet, column, row, value: obj.id }); + }); }) .compact() - .concat([projectCategoryOptionCell]) + .concat(projectCategoryOptionCell ? [projectCategoryOptionCell] : []) .value(); return aocCells; } + private getProjectCategoryOptionCell(projectCategoryOptions: NamedRef[] | undefined) { + const firstProject = _.first(projectCategoryOptions); + const onlyOneCategoryOption = projectCategoryOptions?.length === 1; + + return cell({ + sheet: this.sheets.dataEntry, + column: "B", + row: 1, + value: firstProject && onlyOneCategoryOption ? referenceToId(firstProject.id) : "", + }); + } + private getMetadataCells(metadata: NRCModuleMetadata) { - const { categories } = metadata.categoryCombo; - const projectCategoryOption = categories.project.categoryOption; + const categoryOptionsObj = this.getCategoryOptionsObj(metadata); - const categoryOptions = _([projectCategoryOption]) - .concat(categories.phasesOfEmergency.categoryOptions) - .concat(categories.targetActual.categoryOptions) + const categoryOptions = _([{ id: "", name: "" }]) // Blank option to match empty categoryOption + .concat(categoryOptionsObj.projects || []) + .concat(categoryOptionsObj.phasesOfEmergency || []) + .concat(categoryOptionsObj.targetActual || []) .value(); const categoryOptionCombos = _(metadata.dataElements) @@ -262,14 +299,6 @@ class DownloadCustomization { }; const value = `Validation!$${columns.validation}$${row}:$${columns.validation}$${row + objects.length - 1}`; - /* - await excelRepository.defineName(this.id, nameForId(cell.id), { - type: "cell" as const, - sheet: cell.sheet, - ref: cell.ref, - }); - */ - await this.excelRepository.setDataValidation(this.id, range, value); } @@ -285,15 +314,6 @@ class DownloadCustomization { private async fillWorkbook(_metadata: NRCModuleMetadata, sheetData: WorkbookData) { const { excelRepository } = this; - /* - const { categories } = metadata.categoryCombo; - - await this.setDropdown({ data: "A", validation: "A" }, metadata.organisationUnits); - await this.setDropdown({ data: "B", validation: "B" }, categories.phasesOfEmergency.categoryOptions); - await this.setDropdown({ data: "C", validation: "D" }, metadata.dataElements); - await this.setDropdown({ data: "E", validation: "C" }, categories.targetActual.categoryOptions); - await this.setDropdownCell("D1", { validation: "F" }, metadata.periods); - */ for (const cell of sheetData.cells) { await excelRepository.writeCell( @@ -319,9 +339,20 @@ class DownloadCustomization { }); } + this.hideIdCells(excelRepository); + excelRepository.protectSheet(this.id, this.sheets.validation, this.password); excelRepository.protectSheet(this.id, this.sheets.metadata, this.password); } + + private hideIdCells(excelRepository: ExcelRepository) { + const idColumns = ["G", "H", "I", "J"]; + const sheet = this.sheets.dataEntry; + + idColumns.forEach(idColumn => { + excelRepository.hideCells(this.id, { sheet: sheet, type: "column", ref: idColumn }, true); + }); + } } interface Cell { @@ -347,6 +378,7 @@ function getCocKey(categoryOptionCombo: { categoryOptions: Ref[] }): string { return _(categoryOptionCombo.categoryOptions) .map(co => co.id) .sortBy() + .reject(id => id === "") .join("."); } @@ -358,3 +390,9 @@ function cell(options: { sheet: string; column: string; row: number; value: stri id: options.id, }; } + +type CategoryOptions = { + projects?: NamedRef[]; + phasesOfEmergency?: NamedRef[]; + targetActual?: NamedRef[]; +}; diff --git a/src/data/templates/nrc/NRCModuleMetadataD2Repository.ts b/src/data/templates/nrc/NRCModuleMetadataD2Repository.ts index 326b93b..9867d5c 100644 --- a/src/data/templates/nrc/NRCModuleMetadataD2Repository.ts +++ b/src/data/templates/nrc/NRCModuleMetadataD2Repository.ts @@ -4,32 +4,52 @@ import { Id, NamedRef, Ref } from "../../../domain/entities/ReferenceObject"; import { DataElement, NRCModuleMetadata } from "../../../domain/entities/templates/NRCModuleMetadata"; import { NRCModuleMetadataRepository } from "../../../domain/repositories/templates/NRCModuleMetadataRepository"; import { User } from "../../../domain/entities/User"; +import { Maybe } from "../../../types/utils"; + +type DataSetCategories = { + projects: boolean; + phaseOfEmergency: boolean; + actualTargets: boolean; +}; export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepository { + attributeCodes = { + createdByApp: "GL_CREATED_BY_DATASET_CONFIGURATION", + }; + categoryComboCodes = { + all: "GL_CATBOMBO_ProjectCCTarAct", + }; + + categoryCodes = { + project: "GL_Project", phaseOfEmergency: "GL_CORECOMP_CATCOMBO", actualTargets: "GL_Actual_Targets", - all: "GL_CATBOMBO_ProjectCCTarAct", }; constructor(private api: D2Api) {} async get(options: { currentUser: User; dataSetId: Id }): Promise { const dataSet = await this.getDataSet(options); - const projectCategoryOption = await this.getProjectCategoryOption(dataSet); - const categoryOptions = await this.getCategoryOptions(projectCategoryOption); - const categoryOptionCombos = await this.getCategoryOptionCombos(projectCategoryOption); + const dataSetCategories = this.getDataSetCategories(dataSet); + const projectCategoryOptions = await this.getProjectCategoryOptions(dataSetCategories, dataSet); + const categoryOptions = await this.getCategoryOptions(dataSetCategories, projectCategoryOptions); + const categoryOptionCombos = await this.getCategoryOptionCombos(dataSet, projectCategoryOptions); return { dataSet: dataSet, dataElements: await this.getDataElementsWithDisaggregation(dataSet), - organisationUnits: this.getOrganisationUnits(options.currentUser, projectCategoryOption, dataSet), + organisationUnits: this.getOrganisationUnits(options.currentUser, projectCategoryOptions, dataSet), periods: this.getPeriods(dataSet), categoryCombo: { categories: { - project: { categoryOption: projectCategoryOption }, - phasesOfEmergency: { categoryOptions: categoryOptions.phasesOfEmergency }, - targetActual: { categoryOptions: categoryOptions.targetActual }, + projects: projectCategoryOptions ? { categoryOptions: projectCategoryOptions } : undefined, + phasesOfEmergency: categoryOptions.phasesOfEmergency + ? { categoryOptions: categoryOptions.phasesOfEmergency } + : undefined, + targetActual: categoryOptions.targetActual + ? { categoryOptions: categoryOptions.targetActual } + : undefined, }, categoryOptionCombos: categoryOptionCombos, }, @@ -38,21 +58,25 @@ export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepositor private getOrganisationUnits( currentUser: User, - projectCategoryOption: D2CategoryOption, + projects: Maybe, dataSet: D2DataSet ): NamedRef[] { - const projectOrgUnits = projectCategoryOption.organisationUnits; + const projectsOrgUnits = projects + ? _(projects) + .flatMap(project => project.organisationUnits) + .uniqBy(ou => ou.id) + .value() + : []; function isOrgUnitAvailableForCurrentUserAndProject(dataSetOrgUnit: { path: string }) { - const canUserAccessDataSetOrgUnit = currentUser.orgUnits.some(userOrgUnit => - dataSetOrgUnit.path.includes(userOrgUnit.id) - ); + const canUserAccessDataSetOrgUnit = () => + currentUser.orgUnits.some(userOrgUnit => dataSetOrgUnit.path.includes(userOrgUnit.id)); - const isProjectAssignedToDataSetOrgUnit = - projectOrgUnits.length === 0 || - projectOrgUnits.some(projectOrgUnit => dataSetOrgUnit.path.includes(projectOrgUnit.id)); + const isProjectAssignedToDataSetOrgUnit = () => + projectsOrgUnits.length === 0 || + projectsOrgUnits.some(projectOrgUnit => dataSetOrgUnit.path.includes(projectOrgUnit.id)); - return canUserAccessDataSetOrgUnit && isProjectAssignedToDataSetOrgUnit; + return canUserAccessDataSetOrgUnit() && isProjectAssignedToDataSetOrgUnit(); } return _(dataSet.organisationUnits) @@ -68,14 +92,19 @@ export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepositor .value(); } - private async getCategoryOptionCombos(projectCategoryOption: D2CategoryOption): Promise { + private async getCategoryOptionCombos( + dataSet: D2DataSet, + projectCategoryOptions: Maybe + ): Promise { const { categoryOptionCombos } = await this.api.metadata .get({ categoryOptionCombos: { fields: { id: true, name: true, categoryOptions: { id: true } }, filter: { - "categoryOptions.id": { eq: projectCategoryOption.id }, - "categoryCombo.code": { eq: this.categoryComboCodes.all }, + ...(projectCategoryOptions + ? { "categoryOptions.id": { in: projectCategoryOptions.map(co => co.id) } } + : {}), + "categoryCombo.id": { eq: dataSet.categoryCombo.id }, }, }, }) @@ -133,16 +162,27 @@ export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepositor .value(); } - private async getCategoryOptions(projectCategoryOption: D2CategoryOption) { - const { categoryComboCodes } = this; + private async getCategoryOptions( + dataSetCategories: DataSetCategories, + projectCategoryOptions: Maybe + ) { + const { categoryCodes } = this; const { categories } = await this.api.metadata .get({ categories: { - fields: { id: true, code: true, categoryOptions: { id: true, name: true } }, + fields: { + id: true, + code: true, + categoryOptions: { id: true, name: true }, + }, filter: { code: { - in: [categoryComboCodes.phaseOfEmergency, categoryComboCodes.actualTargets], + in: _.compact([ + dataSetCategories.phaseOfEmergency ? categoryCodes.phaseOfEmergency : null, + dataSetCategories.actualTargets ? categoryCodes.actualTargets : null, + "@@@", // placeholder to prevent passing an empty filter + ]), }, }, }, @@ -150,38 +190,89 @@ export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepositor .getData(); const categoriesByCode = _.keyBy(categories, category => category.code); - const categoryPhaseOfEmergency = categoriesByCode[categoryComboCodes.phaseOfEmergency]; + const categoryPhaseOfEmergency = categoriesByCode[categoryCodes.phaseOfEmergency]; + const categoryActualTargets = categoriesByCode[categoryCodes.actualTargets]; return { - project: [projectCategoryOption], - phasesOfEmergency: _(categoryPhaseOfEmergency?.categoryOptions || []) - .reject(categoryOption => categoryOption.name.includes("DEPRECATED")) - .value(), - targetActual: categoriesByCode[categoryComboCodes.actualTargets]?.categoryOptions || [], + projects: projectCategoryOptions, + phasesOfEmergency: categoryPhaseOfEmergency + ? _(categoryPhaseOfEmergency.categoryOptions || []) + .reject(categoryOption => categoryOption.name.includes("DEPRECATED")) + .value() + : undefined, + targetActual: categoryActualTargets ? categoryActualTargets.categoryOptions || [] : undefined, }; } - private async getProjectCategoryOption(dataSet: D2DataSet) { - const categoryOptionCode = dataSet.code.replace(/Data Set$/, "").trim(); + private getDataSetCategories(dataSet: D2DataSet): DataSetCategories { + const codes = this.categoryCodes; + const allCategories = [codes.project, codes.phaseOfEmergency, codes.actualTargets]; + const dataSetCategoryCodes = dataSet.categoryCombo.categories.map(category => category.code); - const res = await this.api.metadata - .get({ - categoryOptions: { - fields: { id: true, name: true, organisationUnits: { id: true } }, - filter: { code: { eq: categoryOptionCode } }, - }, - }) - .getData(); + const dataSetCategoriesAreASubSetOfSupportedCategories = + _.intersection(allCategories, dataSetCategoryCodes).length >= 1 && + _.difference(dataSetCategoryCodes, allCategories).length === 0; - const projectCategoryOption = res.categoryOptions[0]; + if (!dataSetCategoriesAreASubSetOfSupportedCategories) { + throw new Error(`Data set categories must be a subset of: ${allCategories.join(", ")}`); + } else { + return { + projects: dataSetCategoryCodes.includes(codes.project), + phaseOfEmergency: dataSetCategoryCodes.includes(codes.phaseOfEmergency), + actualTargets: dataSetCategoryCodes.includes(codes.actualTargets), + }; + } + } + + private async getProjectCategoryOptions( + dataSetCategories: DataSetCategories, + dataSet: D2DataSet + ): Promise> { + if (!dataSetCategories.projects) { + return undefined; + } else if (this.isCreatedByDataSetConfigurationApp(dataSet)) { + const categoryOptionCode = dataSet.code ? dataSet.code.replace(/Data Set$/, "").trim() : undefined; + + const res = await this.api.metadata + .get({ + categoryOptions: { + fields: { id: true, name: true, organisationUnits: { id: true } }, + filter: { code: { eq: categoryOptionCode } }, + }, + }) + .getData(); - if (!projectCategoryOption) { - throw new Error(`Project category option not found (code: ${categoryOptionCode})`); + const projectCategoryOption = res.categoryOptions[0]; + + if (!projectCategoryOption) { + throw new Error(`Project category option not found (code: ${categoryOptionCode})`); + } else { + return [projectCategoryOption]; + } } else { - return projectCategoryOption; + // It's not a project dataSet, get list of projects accessible for the user (limited) + const res = await this.api.models.categoryOptions + .get({ + fields: { id: true, name: true, organisationUnits: { id: true } }, + filter: { "categories.code": { eq: this.categoryCodes.project } }, + order: "name:asc", + paging: true, + pageSize: 300, + }) + .getData(); + + return res.objects; } } + private isCreatedByDataSetConfigurationApp(dataSet: D2DataSet): boolean { + const attributeValue = dataSet.attributeValues.find(attributeValue => { + return attributeValue.attribute.code === this.attributeCodes.createdByApp; + }); + + return attributeValue?.value === "true"; + } + private async getDataSet(options: { dataSetId: Id }): Promise { const { dataSets } = await this.api.metadata .get({ @@ -196,8 +287,6 @@ export class NRCModuleMetadataD2Repository implements NRCModuleMetadataRepositor if (!dataSet) { throw new Error(`Data set not found: ${options.dataSetId}`); - } else if (!dataSet.code) { - throw new Error(`Data set has no code, it's required to get the project category option`); } else { return dataSet; } @@ -209,6 +298,10 @@ const dataSetFields = { name: true, code: true, categoryCombo: { id: true, categories: { id: true, code: true } }, + attributeValues: { + attribute: { code: true }, + value: true, + }, dataSetElements: { dataElement: { id: true, name: true, categoryCombo: { id: true } }, categoryCombo: { id: true }, diff --git a/src/domain/entities/templates/NRCModuleMetadata.ts b/src/domain/entities/templates/NRCModuleMetadata.ts index ffa9087..a52b23f 100644 --- a/src/domain/entities/templates/NRCModuleMetadata.ts +++ b/src/domain/entities/templates/NRCModuleMetadata.ts @@ -7,14 +7,17 @@ export interface NRCModuleMetadata { periods: Period[]; categoryCombo: { categories: { - project: { categoryOption: NamedRef }; - phasesOfEmergency: { categoryOptions: NamedRef[] }; - targetActual: { categoryOptions: NamedRef[] }; + projects?: { categoryOptions: CategoryOption[] }; + phasesOfEmergency?: { categoryOptions: CategoryOption[] }; + targetActual?: { categoryOptions: CategoryOption[] }; }; + categoryOptionCombos: Array<{ id: Id; name: string; categoryOptions: Ref[] }>; }; } +export type CategoryOption = NamedRef; + export interface DataElement extends NamedRef { categoryCombo: CategoryCombo; } diff --git a/src/domain/repositories/InstanceRepository.ts b/src/domain/repositories/InstanceRepository.ts index a50a248..8647413 100644 --- a/src/domain/repositories/InstanceRepository.ts +++ b/src/domain/repositories/InstanceRepository.ts @@ -37,12 +37,14 @@ export interface InstanceRepository { getLocales(): Promise; getDefaultIds(filter?: string): Promise; deleteAggregatedData(dataPackage: DataPackage): Promise; - importDataPackage(dataPackage: DataPackage): Promise; + importDataPackage(dataPackage: DataPackage, options: ImportDataPackageOptions): Promise; getProgram(programId: Id): Promise; convertDataPackage(dataPackage: DataPackage): EventsPackage | AggregatedPackage; getBuilderMetadata(teis: TrackedEntityInstance[]): Promise; } +export type ImportDataPackageOptions = { createAndUpdate: boolean }; + export interface BuilderMetadata { orgUnits: Record; options: Record; diff --git a/src/domain/usecases/ImportTemplateUseCase.ts b/src/domain/usecases/ImportTemplateUseCase.ts index 5659426..b2d0c1a 100644 --- a/src/domain/usecases/ImportTemplateUseCase.ts +++ b/src/domain/usecases/ImportTemplateUseCase.ts @@ -33,7 +33,7 @@ export type ImportTemplateError = instanceDataValues: DataPackage; }; -export type DuplicateImportStrategy = "ERROR" | "IMPORT" | "IGNORE"; +export type DuplicateImportStrategy = "ERROR" | "IMPORT" | "IGNORE" | "IMPORT_WITHOUT_DELETE"; export type OrganisationUnitImportStrategy = "ERROR" | "IGNORE"; export interface ImportTemplateUseCaseParams { @@ -142,12 +142,16 @@ export class ImportTemplateUseCase implements UseCase { }); } - const deleteResult = - duplicateStrategy === "IGNORE" || dataForm.type !== "dataSets" - ? undefined - : await this.instanceRepository.deleteAggregatedData(instanceDataValues); + const shouldDeleteExistingData = + dataForm.type === "dataSets" ? this.shouldDeleteAggregatedData(duplicateStrategy) : false; - const importResult = await this.instanceRepository.importDataPackage(dataValues); + const deleteResult = shouldDeleteExistingData + ? await this.instanceRepository.deleteAggregatedData(instanceDataValues) + : undefined; + + const importResult = await this.instanceRepository.importDataPackage(dataValues, { + createAndUpdate: duplicateStrategy === "IMPORT_WITHOUT_DELETE" || duplicateStrategy === "ERROR", + }); const importResultHasErrors = importResult.flatMap(result => result.errors); if (importResultHasErrors.length > 0 || deleteResult) { @@ -166,6 +170,10 @@ export class ImportTemplateUseCase implements UseCase { } } + private shouldDeleteAggregatedData(strategy: DuplicateImportStrategy): boolean { + return strategy === "IMPORT"; + } + private validateOrgUnitAccess( dataPackage: DataPackage, orgUnits: OrgUnit[], @@ -256,7 +264,7 @@ export class ImportTemplateUseCase implements UseCase { ); const existingDataValues = - duplicateStrategy === "IMPORT" + duplicateStrategy === "IMPORT_WITHOUT_DELETE" || duplicateStrategy === "IMPORT" ? [] : _.remove(excelFile, base => { return instanceDataValues.find(dataPackage => diff --git a/src/webapp/components/modal-dialog/ModalDialog.tsx b/src/webapp/components/modal-dialog/ModalDialog.tsx new file mode 100644 index 0000000..038cd4f --- /dev/null +++ b/src/webapp/components/modal-dialog/ModalDialog.tsx @@ -0,0 +1,107 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogProps, + DialogTitle, + makeStyles, + Tooltip, +} from "@material-ui/core"; +import _ from "lodash"; +import React from "react"; + +import i18n from "../../../locales"; + +export interface ModalDialogProps extends Partial> { + isOpen?: boolean; + title?: string; + description?: string; + onSave?: (event: React.MouseEvent) => void; + onCancel?: (event: React.MouseEvent) => void; + onInfoAction?: (event: React.MouseEvent) => void; + onUpdate?: (event: React.MouseEvent) => void; + saveText?: string; + cancelText?: string; + infoActionText?: string; + disableSave?: boolean; + updateText?: string; + saveButtonPrimary?: boolean; + saveTooltipText?: string; + updateTooltipText?: string; + infoTooltipText?: string; +} + +export const ModalDialog: React.FC = ({ + title = "", + description, + onSave, + onUpdate, + onCancel, + onInfoAction, + isOpen = false, + children, + cancelText = i18n.t("Cancel"), + saveText = i18n.t("Save"), + updateText, + infoActionText = i18n.t("Info"), + disableSave = false, + saveButtonPrimary = true, + saveTooltipText = "", + updateTooltipText = "", + infoTooltipText = "", + ...other +}) => { + const classes = useStyles(); + + return ( + + {title} + + + {description} + {children} + + + + {onInfoAction && ( + + + + )} + {onCancel && ( + + )} + {onUpdate && ( + + + + )} + {onSave && ( + + + + )} + + + ); +}; + +const useStyles = makeStyles({ + infoButton: { marginRight: "auto" }, +}); + +export default ModalDialog; diff --git a/src/webapp/pages/import-template/ImportTemplatePage.tsx b/src/webapp/pages/import-template/ImportTemplatePage.tsx index 51a9377..a6af2a0 100644 --- a/src/webapp/pages/import-template/ImportTemplatePage.tsx +++ b/src/webapp/pages/import-template/ImportTemplatePage.tsx @@ -1,10 +1,4 @@ -import { - ConfirmationDialog, - ConfirmationDialogProps, - OrgUnitsSelector, - useLoading, - useSnackbar, -} from "@eyeseetea/d2-ui-components"; +import { OrgUnitsSelector, useLoading, useSnackbar } from "@eyeseetea/d2-ui-components"; import { Button, Checkbox, FormControlLabel, makeStyles } from "@material-ui/core"; import CloudDoneIcon from "@material-ui/icons/CloudDone"; import CloudUploadIcon from "@material-ui/icons/CloudUpload"; @@ -18,6 +12,7 @@ import { DataPackage } from "../../../domain/entities/DataPackage"; import { SynchronizationResult } from "../../../domain/entities/SynchronizationResult"; import { ImportTemplateUseCaseParams } from "../../../domain/usecases/ImportTemplateUseCase"; import i18n from "../../../locales"; +import ModalDialog, { ModalDialogProps } from "../../components/modal-dialog/ModalDialog"; import SyncSummary from "../../components/sync-summary/SyncSummary"; import { useAppContext } from "../../contexts/app-context"; import { orgUnitListParams } from "../../utils/template"; @@ -47,7 +42,7 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { const [orgUnitTreeFilter, setOrgUnitTreeFilter] = useState([]); const [importState, setImportState] = useState(); const [messages, setMessages] = useState([]); - const [dialogProps, updateDialog] = useState(null); + const [dialogProps, updateDialog] = useState(); useEffect(() => { compositionRoot.orgUnits.getUserRoots().then(setOrgUnitTreeRootIds); @@ -117,7 +112,6 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { const startImport = async (params: ImportTemplateUseCaseParams) => { loading.show(true, i18n.t("Importing data...")); - const result = await compositionRoot.templates.import(params); result.match({ @@ -141,12 +135,23 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { const dataSetConfig = { title: i18n.t("Existing data values"), message: i18n.t( - "There are {{totalExisting}} data values in the database for this organisation unit and periods. If you proceed, all those data values will be deleted and only the ones in the spreadsheet will be saved. Are you sure?", + "There are {{totalExisting}} data values in the database for this organisation unit and periods. Would you like to import only the new data values, import new values and update existing ones, or completely delete existing values before importing the data?", { totalExisting } ), - save: i18n.t("Proceed"), + save: i18n.t("Delete and Import"), cancel: i18n.t("Cancel"), info: i18n.t("Import only new data values"), + updateText: i18n.t("Import and Update"), + saveButtonPrimary: false, + saveTooltipText: i18n.t( + "All data values in the spreadsheet will be imported to the system, but any data that was existing for such organisation unit and periods in the system will be deleted first, so none will be kept before doing the import." + ), + infoTooltipText: i18n.t( + "Import only new data values, without updating nor deleting any existing one. Only values in the spreadsheet that do not currently exist in the system will be imported" + ), + updateTooltipText: i18n.t( + "Import new data values and also update existing ones. All data values in the spreadsheet will be imported to the system, but other data values present in the system that are not provided in the spreadsheet will be kept." + ), }; const programConfig = { @@ -160,32 +165,58 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { save: i18n.t("Import everything anyway"), cancel: i18n.t("Cancel import"), info: i18n.t("Import only new records"), + updateText: "", + saveButtonPrimary: true, + saveTooltipText: "", + infoTooltipText: "", + updateTooltipText: "", }; - const { title, message, save, cancel, info } = - dataValues.type === "dataSets" ? dataSetConfig : programConfig; + const { + title, + message, + save, + cancel, + info, + updateText, + saveButtonPrimary, + saveTooltipText, + infoTooltipText, + updateTooltipText, + } = dataValues.type === "dataSets" ? dataSetConfig : programConfig; updateDialog({ title, description: message, onSave: async () => { - updateDialog(null); + updateDialog(undefined); loading.show(true, i18n.t("Importing data...")); await startImport({ ...params, duplicateStrategy: "IMPORT" }); loading.reset(); }, + onUpdate: async () => { + updateDialog(undefined); + loading.show(true, i18n.t("Importing data...")); + await startImport({ ...params, duplicateStrategy: "IMPORT_WITHOUT_DELETE" }); + loading.reset(); + }, onInfoAction: async () => { - updateDialog(null); + updateDialog(undefined); loading.show(true, i18n.t("Importing data...")); await startImport({ ...params, duplicateStrategy: "IGNORE" }); loading.reset(); }, onCancel: () => { - updateDialog(null); + updateDialog(undefined); }, saveText: save, cancelText: cancel, infoActionText: info, + updateText, + saveButtonPrimary, + saveTooltipText, + updateTooltipText, + infoTooltipText, }); } break; @@ -206,10 +237,10 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { { totalInvalid } ), onCancel: () => { - updateDialog(null); + updateDialog(undefined); }, onSave: async () => { - updateDialog(null); + updateDialog(undefined); await startImport({ ...params, organisationUnitStrategy: "IGNORE", @@ -268,7 +299,7 @@ export default function ImportTemplatePage({ settings }: RouteComponentProps) { return ( - {dialogProps && } + {dialogProps && } {syncResults && }