From 5e0fc64726b344645fa86a2b91620e76a5f0c14f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 13 Nov 2020 16:05:37 +0100 Subject: [PATCH 01/32] Show package state filter and column for local instance --- i18n/en.pot | 4 +- .../package-list-table/PackageListTable.tsx | 55 +++++-------------- 2 files changed, 16 insertions(+), 43 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index dbb64632f..d768ff81f 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: 2020-11-13T09:44:46.370Z\n" -"PO-Revision-Date: 2020-11-13T09:44:46.370Z\n" +"POT-Creation-Date: 2020-11-13T15:04:06.311Z\n" +"PO-Revision-Date: 2020-11-13T15:04:06.311Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index fc9a6d87d..356dc1cd9 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -16,7 +16,6 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import semver from "semver"; import { Either } from "../../../../domain/common/entities/Either"; import { NamedRef } from "../../../../domain/common/entities/Ref"; -import { Instance } from "../../../../domain/instance/entities/Instance"; import { JSONDataSource } from "../../../../domain/instance/entities/JSONDataSource"; import { ImportedPackage } from "../../../../domain/package-import/entities/ImportedPackage"; import { @@ -26,7 +25,6 @@ import { } from "../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; import { BasePackage, ListPackage, Package } from "../../../../domain/packages/entities/Package"; -import { Store } from "../../../../domain/packages/entities/Store"; import i18n from "../../../../locales"; import SyncReport from "../../../../models/syncReport"; import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; @@ -36,7 +34,7 @@ import Dropdown from "../dropdown/Dropdown"; import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; import { PackagesDiffDialog, DiffPackages } from "../packages-diff-dialog/PackagesDiffDialog"; -type InstallState = "Installed" | "NotInstalled" | "Upgrade" | "Local"; +type InstallState = "Installed" | "NotInstalled" | "Upgrade"; type TableListPackage = Omit & { installState: InstallState }; interface PackagesListTableProps extends ModulePackageListPageProps { @@ -378,8 +376,6 @@ export const PackagesListTable: React.FC = ({ return i18n.t("Not Installed"); case "Upgrade": return i18n.t("Upgrade Available"); - case "Local": - return ""; } }; @@ -396,11 +392,10 @@ export const PackagesListTable: React.FC = ({ name: "installState", text: i18n.t("State"), sortable: true, - hidden: !remoteInstance && !remoteStore, getValue: (row: TableListPackage) => getInstallStateText(row.installState), }, ], - [remoteInstance, remoteStore] + [] ); const details: ObjectsTableDetailField[] = useMemo( @@ -592,16 +587,15 @@ export const PackagesListTable: React.FC = ({ /> ); - const installStateFilterComponent = - remoteInstance || remoteStore ? ( - - ) : null; + const installStateFilterComponent = ( + + ); return [ externalComponents, moduleFilterComponent, @@ -616,8 +610,6 @@ export const PackagesListTable: React.FC = ({ dhis2VersionFilter, installStateFilterItems, installStateFilter, - remoteInstance, - remoteStore, ]); const rowsFiltered = useMemo(() => { @@ -655,14 +647,7 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages .list(globalAdmin, remoteInstance) .then(packages => { - setInstancePackages( - mapPackagesToListPackages( - packages, - importedPackages, - remoteInstance, - remoteStore - ) - ); + setInstancePackages(mapPackagesToListPackages(packages, importedPackages)); }) .catch((error: Error) => { snackbar.error(error.message); @@ -684,14 +669,7 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages.listStore(remoteStore.id).then(validation => { validation.match({ success: packages => { - setStorePackages( - mapPackagesToListPackages( - packages, - importedPackages, - remoteInstance, - remoteStore - ) - ); + setStorePackages(mapPackagesToListPackages(packages, importedPackages)); }, error: () => { snackbar.error(i18n.t("Can't connect to store")); @@ -772,14 +750,9 @@ export const PackagesListTable: React.FC = ({ function mapPackagesToListPackages( packages: ListPackage[], - importedPackages: ImportedPackage[], - remoteInstance?: Instance, - remoteStore?: Store + importedPackages: ImportedPackage[] ): TableListPackage[] { const listPackages = packages.map(pkg => { - if (!remoteStore && !remoteInstance) - return { ...pkg, installState: "Local" as InstallState }; - const installed = importedPackages.some(imported => { return ( imported.module.id === pkg.module.id && From 380328f2bcdb392ecbe244dcdb3a862f42c8c4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 16 Nov 2020 11:52:46 +0100 Subject: [PATCH 02/32] Rename install state to status --- i18n/en.pot | 9 ++-- i18n/es.po | 7 +-- i18n/fr.po | 7 +-- i18n/pt.po | 7 +-- .../package-list-table/PackageListTable.tsx | 52 +++++++++---------- 5 files changed, 35 insertions(+), 47 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9d720283f..004172677 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: 2020-11-16T09:46:43.582Z\n" -"PO-Revision-Date: 2020-11-16T09:46:43.582Z\n" +"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" +"PO-Revision-Date: 2020-11-16T10:51:33.105Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -659,7 +659,7 @@ msgstr "" msgid "Module" msgstr "" -msgid "State" +msgid "Status" msgstr "" msgid "Download as JSON" @@ -892,9 +892,6 @@ msgstr "" msgid "Destination instance" msgstr "" -msgid "Status" -msgstr "" - msgid "Messages" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 7d7370e01..bd397a179 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T06:06:52.782Z\n" +"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -663,7 +663,7 @@ msgstr "" msgid "Module" msgstr "" -msgid "State" +msgid "Status" msgstr "" msgid "Download as JSON" @@ -897,9 +897,6 @@ msgstr "" msgid "Destination instance" msgstr "" -msgid "Status" -msgstr "" - msgid "Messages" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 7a7c1f6e7..ec4134f5c 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T06:06:52.782Z\n" +"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -660,7 +660,7 @@ msgstr "" msgid "Module" msgstr "" -msgid "State" +msgid "Status" msgstr "" msgid "Download as JSON" @@ -894,9 +894,6 @@ msgstr "" msgid "Destination instance" msgstr "" -msgid "Status" -msgstr "" - msgid "Messages" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 7a7c1f6e7..ec4134f5c 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T06:06:52.782Z\n" +"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -660,7 +660,7 @@ msgstr "" msgid "Module" msgstr "" -msgid "State" +msgid "Status" msgstr "" msgid "Download as JSON" @@ -894,9 +894,6 @@ msgstr "" msgid "Destination instance" msgstr "" -msgid "Status" -msgstr "" - msgid "Messages" msgstr "" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index 356dc1cd9..ea2d702e3 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -34,8 +34,8 @@ import Dropdown from "../dropdown/Dropdown"; import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; import { PackagesDiffDialog, DiffPackages } from "../packages-diff-dialog/PackagesDiffDialog"; -type InstallState = "Installed" | "NotInstalled" | "Upgrade"; -type TableListPackage = Omit & { installState: InstallState }; +type InstallStatus = "Installed" | "NotInstalled" | "Upgrade"; +type TableListPackage = Omit & { installStatus: InstallStatus }; interface PackagesListTableProps extends ModulePackageListPageProps { isImportDialog?: boolean; @@ -74,7 +74,7 @@ export const PackagesListTable: React.FC = ({ const [moduleFilter, setModuleFilter] = useState(""); const [dhis2VersionFilter, setDhis2VersionFilter] = useState(""); const [localDhis2Version, setLocalDhis2Version] = useState(""); - const [installStateFilter, setInstallStateFilter] = useState(""); + const [installStatusFilter, setInstallStatusFilter] = useState(""); const [globalAdmin, setGlobalAdmin] = useState(false); const [appConfigurator, setAppConfigurator] = useState(false); @@ -368,8 +368,8 @@ export const PackagesListTable: React.FC = ({ ] ); - const getInstallStateText = (installState: InstallState) => { - switch (installState) { + const getInstallStatusText = (installStatus: InstallStatus) => { + switch (installStatus) { case "Installed": return i18n.t("Installed"); case "NotInstalled": @@ -389,10 +389,10 @@ export const PackagesListTable: React.FC = ({ { name: "created", text: i18n.t("Created"), sortable: true, hidden: true }, { name: "user", text: i18n.t("Created by"), sortable: true, hidden: true }, { - name: "installState", - text: i18n.t("State"), + name: "installStatus", + text: i18n.t("Status"), sortable: true, - getValue: (row: TableListPackage) => getInstallStateText(row.installState), + getValue: (row: TableListPackage) => getInstallStatusText(row.installStatus), }, ], [] @@ -409,9 +409,9 @@ export const PackagesListTable: React.FC = ({ { name: "created", text: i18n.t("Created") }, { name: "user", text: i18n.t("Created by") }, { - name: "installState", - text: i18n.t("State"), - getValue: (row: TableListPackage) => getInstallStateText(row.installState), + name: "installStatus", + text: i18n.t("Status"), + getValue: (row: TableListPackage) => getInstallStatusText(row.installStatus), }, ], [] @@ -548,13 +548,13 @@ export const PackagesListTable: React.FC = ({ .value(); }, [instancePackages, storePackages, remoteStore, localDhis2Version]); - const installStateFilterItems = useMemo(() => { + const installStatusFilterItems = useMemo(() => { const packages = remoteStore ? storePackages : instancePackages; return _(packages) .map(pkg => ({ - id: pkg.installState, - name: getInstallStateText(pkg.installState), + id: pkg.installStatus, + name: getInstallStatusText(pkg.installStatus), })) .uniqBy(({ id }) => id) .sortBy(({ name }) => name) @@ -589,11 +589,11 @@ export const PackagesListTable: React.FC = ({ const installStateFilterComponent = ( ); return [ @@ -608,8 +608,8 @@ export const PackagesListTable: React.FC = ({ moduleFilterItems, dhis2VersionFilterItems, dhis2VersionFilter, - installStateFilterItems, - installStateFilter, + installStatusFilterItems, + installStatusFilter, ]); const rowsFiltered = useMemo(() => { @@ -618,9 +618,9 @@ export const PackagesListTable: React.FC = ({ row => (row.module.id === moduleFilter || !moduleFilter) && (row.dhisVersion === dhis2VersionFilter || !dhis2VersionFilter) && - (row.installState === installStateFilter || !installStateFilter) + (row.installStatus === installStatusFilter || !installStatusFilter) ); - }, [moduleFilter, rows, dhis2VersionFilter, installStateFilter]); + }, [moduleFilter, rows, dhis2VersionFilter, installStatusFilter]); const handleOpenSyncSummaryFromDialog = (syncReport: SyncReport) => { setOpenImportPackageDialog(false); @@ -697,7 +697,7 @@ export const PackagesListTable: React.FC = ({ useEffect(() => { setModuleFilter(""); setDhis2VersionFilter(""); - setInstallStateFilter(""); + setInstallStatusFilter(""); setResetKey(Math.random()); }, [remoteInstance, remoteStore]); @@ -774,13 +774,13 @@ function mapPackagesToListPackages( ); }); - const installState: InstallState = installed + const installStatus: InstallStatus = installed ? "Installed" : newUpdates ? "Upgrade" : "NotInstalled"; - return { ...pkg, installState }; + return { ...pkg, installStatus }; }); return listPackages; From 23f054c16ab58a6ae8010eedfbdefb19beef1701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 19 Nov 2020 12:02:09 +0100 Subject: [PATCH 03/32] Design changes in package diff dialog --- i18n/en.pot | 4 ++-- .../components/packages-diff-dialog/PackagesDiffDialog.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 004172677..6b9b4f1d3 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: 2020-11-16T10:51:33.105Z\n" -"PO-Revision-Date: 2020-11-16T10:51:33.105Z\n" +"POT-Creation-Date: 2020-11-19T11:00:05.157Z\n" +"PO-Revision-Date: 2020-11-19T11:00:05.157Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx b/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx index e7c1599f4..07bd04d30 100644 --- a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx +++ b/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx @@ -103,7 +103,9 @@ export const MetadataDiffTable: React.FC<{ {_.map(metadataDiff, (modelDiff, model) => (
  • {model}

    : {modelDiff.total}{" "} - {i18n.t("objects")} ({i18n.t("Unmodified")}: {modelDiff.unmodified.length}) + {i18n.t("objects")} ({i18n.t("Unmodified")}: {modelDiff.unmodified.length},{" "} + {i18n.t("New")}: {modelDiff.created.length}, {i18n.t("Updated")}:{" "} + {modelDiff.updates.length})
  • ))} @@ -123,7 +125,7 @@ export const ModelDiffList: React.FC<{ modelDiff: ModelDiff }> = props => { {i18n.t("New")}: {diff.created.length} - `[${obj.id}] ${obj.name}`)} /> + `${obj.name} (${obj.id})`)} /> )} From ec9352b7684c7654513f43eedd78c83816d356e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 20 Nov 2020 09:00:07 +0100 Subject: [PATCH 04/32] Show packages grouped by module, version --- i18n/en.pot | 8 +-- i18n/es.po | 6 +- i18n/fr.po | 6 +- i18n/pt.po | 6 +- .../package-list-table/PackageListTable.tsx | 56 ++++++++++++------- .../package-list-table/PackageModuleItem.ts | 42 ++++++++++++++ src/utils/flatten-union.ts | 14 +++++ 7 files changed, 104 insertions(+), 34 deletions(-) create mode 100644 src/presentation/react/components/package-list-table/PackageModuleItem.ts create mode 100644 src/utils/flatten-union.ts diff --git a/i18n/en.pot b/i18n/en.pot index 6b9b4f1d3..3fc62612c 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: 2020-11-19T11:00:05.157Z\n" -"PO-Revision-Date: 2020-11-19T11:00:05.157Z\n" +"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"PO-Revision-Date: 2020-11-20T07:58:40.203Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -656,10 +656,10 @@ msgstr "" msgid "DHIS2 Version" msgstr "" -msgid "Module" +msgid "Status" msgstr "" -msgid "Status" +msgid "Module" msgstr "" msgid "Download as JSON" diff --git a/i18n/es.po b/i18n/es.po index bd397a179..6ce42123a 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" +"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -660,10 +660,10 @@ msgstr "" msgid "DHIS2 Version" msgstr "" -msgid "Module" +msgid "Status" msgstr "" -msgid "Status" +msgid "Module" msgstr "" msgid "Download as JSON" diff --git a/i18n/fr.po b/i18n/fr.po index ec4134f5c..153603391 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" +"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -657,10 +657,10 @@ msgstr "" msgid "DHIS2 Version" msgstr "" -msgid "Module" +msgid "Status" msgstr "" -msgid "Status" +msgid "Module" msgstr "" msgid "Download as JSON" diff --git a/i18n/pt.po b/i18n/pt.po index ec4134f5c..153603391 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-16T10:51:33.105Z\n" +"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -657,10 +657,10 @@ msgstr "" msgid "DHIS2 Version" msgstr "" -msgid "Module" +msgid "Status" msgstr "" -msgid "Status" +msgid "Module" msgstr "" msgid "Download as JSON" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index ea2d702e3..6e595eff4 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -24,7 +24,7 @@ import { PackageSource, } from "../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; -import { BasePackage, ListPackage, Package } from "../../../../domain/packages/entities/Package"; +import { ListPackage, Package } from "../../../../domain/packages/entities/Package"; import i18n from "../../../../locales"; import SyncReport from "../../../../models/syncReport"; import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; @@ -33,9 +33,13 @@ import { useAppContext } from "../../contexts/AppContext"; import Dropdown from "../dropdown/Dropdown"; import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; import { PackagesDiffDialog, DiffPackages } from "../packages-diff-dialog/PackagesDiffDialog"; - -type InstallStatus = "Installed" | "NotInstalled" | "Upgrade"; -type TableListPackage = Omit & { installStatus: InstallStatus }; +import { + groupPackageByModuleAndVersion as groupPackagesByModuleAndVersion, + InstallStatus, + isPackageItem, + PackageItem, + PackageModuleItem, +} from "./PackageModuleItem"; interface PackagesListTableProps extends ModulePackageListPageProps { isImportDialog?: boolean; @@ -60,8 +64,8 @@ export const PackagesListTable: React.FC = ({ const snackbar = useSnackbar(); const loading = useLoading(); - const [instancePackages, setInstancePackages] = useState([]); - const [storePackages, setStorePackages] = useState([]); + const [instancePackages, setInstancePackages] = useState([]); + const [storePackages, setStorePackages] = useState([]); const [importedPackages, setImportedPackages] = useState([]); const rows = remoteStore ? storePackages : instancePackages; @@ -110,7 +114,7 @@ export const PackagesListTable: React.FC = ({ ); const updateTable = useCallback( - ({ selection }: TableState) => { + ({ selection }: TableState) => { updateSelection(selection); }, [updateSelection] @@ -203,7 +207,7 @@ export const PackagesListTable: React.FC = ({ async (ids: string[]) => { const packageId = _(ids).get(0, null); const remotePackage = packageId ? rows.find(row => row.id === packageId) : undefined; - if (packageId && remotePackage) { + if (packageId && remotePackage && isPackageItem(remotePackage)) { setPackagesToDiff({ merge: remotePackage }); } }, @@ -215,7 +219,12 @@ export const PackagesListTable: React.FC = ({ const [packageBase, packageMerge] = ids.map(packageId => { return rows.find(row => row.id === packageId); }); - if (packageBase && packageMerge) { + if ( + packageBase && + packageMerge && + isPackageItem(packageBase) && + isPackageItem(packageMerge) + ) { setPackagesToDiff({ base: packageBase, merge: packageMerge }); } }, @@ -379,26 +388,26 @@ export const PackagesListTable: React.FC = ({ } }; - const columns: TableColumn[] = useMemo( + const columns: TableColumn[] = useMemo( () => [ { name: "name", text: i18n.t("Name"), sortable: true }, { name: "description", text: i18n.t("Description"), sortable: true, hidden: true }, { name: "version", text: i18n.t("Version"), sortable: true }, { name: "dhisVersion", text: i18n.t("DHIS2 Version"), sortable: true }, - { name: "module", text: i18n.t("Module"), sortable: true }, { name: "created", text: i18n.t("Created"), sortable: true, hidden: true }, { name: "user", text: i18n.t("Created by"), sortable: true, hidden: true }, { name: "installStatus", text: i18n.t("Status"), sortable: true, - getValue: (row: TableListPackage) => getInstallStatusText(row.installStatus), + getValue: (row: PackageModuleItem) => + isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, }, ], [] ); - const details: ObjectsTableDetailField[] = useMemo( + const details: ObjectsTableDetailField[] = useMemo( () => [ { name: "id", text: i18n.t("ID") }, { name: "name", text: i18n.t("Name") }, @@ -411,13 +420,14 @@ export const PackagesListTable: React.FC = ({ { name: "installStatus", text: i18n.t("Status"), - getValue: (row: TableListPackage) => getInstallStatusText(row.installStatus), + getValue: (row: PackageModuleItem) => + isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, }, ], [] ); - const actions: TableAction[] = useMemo( + const actions: TableAction[] = useMemo( () => [ { name: "details", @@ -614,12 +624,15 @@ export const PackagesListTable: React.FC = ({ const rowsFiltered = useMemo(() => { setLoadingTable(false); - return rows.filter( + + const packageItems = rows.filter( row => (row.module.id === moduleFilter || !moduleFilter) && (row.dhisVersion === dhis2VersionFilter || !dhis2VersionFilter) && (row.installStatus === installStatusFilter || !installStatusFilter) ); + + return groupPackagesByModuleAndVersion(packageItems); }, [moduleFilter, rows, dhis2VersionFilter, installStatusFilter]); const handleOpenSyncSummaryFromDialog = (syncReport: SyncReport) => { @@ -647,7 +660,7 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages .list(globalAdmin, remoteInstance) .then(packages => { - setInstancePackages(mapPackagesToListPackages(packages, importedPackages)); + setInstancePackages(mapPackagesToPackageItems(packages, importedPackages)); }) .catch((error: Error) => { snackbar.error(error.message); @@ -669,7 +682,7 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages.listStore(remoteStore.id).then(validation => { validation.match({ success: packages => { - setStorePackages(mapPackagesToListPackages(packages, importedPackages)); + setStorePackages(mapPackagesToPackageItems(packages, importedPackages)); }, error: () => { snackbar.error(i18n.t("Can't connect to store")); @@ -708,7 +721,7 @@ export const PackagesListTable: React.FC = ({ return ( - + resetKey={`${resetKey}`} rows={rowsFiltered} columns={columns} @@ -722,6 +735,7 @@ export const PackagesListTable: React.FC = ({ paginationOptions={paginationOptions} actionButtonLabel={actionButtonLabel} loading={loadingTable} + childrenKeys={["packages"]} /> {dialogProps && } @@ -748,10 +762,10 @@ export const PackagesListTable: React.FC = ({ ); }; -function mapPackagesToListPackages( +function mapPackagesToPackageItems( packages: ListPackage[], importedPackages: ImportedPackage[] -): TableListPackage[] { +): PackageItem[] { const listPackages = packages.map(pkg => { const installed = importedPackages.some(imported => { return ( diff --git a/src/presentation/react/components/package-list-table/PackageModuleItem.ts b/src/presentation/react/components/package-list-table/PackageModuleItem.ts new file mode 100644 index 000000000..4a98754f9 --- /dev/null +++ b/src/presentation/react/components/package-list-table/PackageModuleItem.ts @@ -0,0 +1,42 @@ +import { BasePackage } from "../../../../domain/packages/entities/Package"; +import { FlattenUnion } from "../../../../utils/flatten-union"; + +export type PackageModuleItem = FlattenUnion; + +export interface ModuleItem { + id: string; + name: string; + version: string; + packages: PackageItem[]; +} + +export type InstallStatus = "Installed" | "NotInstalled" | "Upgrade"; +export type PackageItem = Omit & { installStatus: InstallStatus }; + +export const isPackageItem = (item: PackageModuleItem): item is PackageItem => { + return (item as PackageItem).version !== undefined; +}; + +export const groupPackageByModuleAndVersion = (packages: PackageItem[]) => { + return packages.reduce((acc, item) => { + const parent = acc.find( + parent => parent.id === item.module.id && parent.version === item.version + ); + + if (parent) { + return acc.map(parentItem => + parentItem.id === item.module.id && parentItem.version === item.version + ? { ...parentItem, packages: [...parentItem.packages, item] } + : parentItem + ); + } else { + const newParent = { + id: item.module.id, + name: item.module.name, + version: item.version, + packages: [item], + }; + return [...acc, newParent]; + } + }, [] as ModuleItem[]); +}; diff --git a/src/utils/flatten-union.ts b/src/utils/flatten-union.ts new file mode 100644 index 000000000..493de2c17 --- /dev/null +++ b/src/utils/flatten-union.ts @@ -0,0 +1,14 @@ +type AllKeysOfUnion = U extends any ? keyof U : never; + +type NonCommonKeysOfUnion = Exclude, keyof U>; + +type PartialBy = Omit & Partial>; + +type UnionWithKeys = U extends any + ? { [Key in K]: Key extends keyof U ? U[Key] : never } + : never; + +export type FlattenUnion = PartialBy< + UnionWithKeys>, + NonCommonKeysOfUnion +>; From 9fb3e53aed5e18f316c76e5e7994c384960f3368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 20 Nov 2020 10:30:27 +0100 Subject: [PATCH 05/32] Mark parent as non selectable and hide actions --- i18n/en.pot | 4 +-- .../package-list-table/PackageListTable.tsx | 29 +++++++++++++++---- .../package-list-table/PackageModuleItem.ts | 12 ++++---- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 3fc62612c..a93693fad 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: 2020-11-20T07:58:40.203Z\n" -"PO-Revision-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-20T09:27:57.833Z\n" +"PO-Revision-Date: 2020-11-20T09:27:57.833Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index 6e595eff4..926b8b416 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -4,6 +4,7 @@ import { ConfirmationDialogProps, ObjectsTable, ObjectsTableDetailField, + RowConfig, TableAction, TableColumn, TableSelection, @@ -434,6 +435,7 @@ export const PackagesListTable: React.FC = ({ text: i18n.t("Details"), multiple: false, primary: true, + isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), }, { name: "delete", @@ -441,7 +443,8 @@ export const PackagesListTable: React.FC = ({ multiple: true, onClick: deletePackages, icon: delete, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && !isImportDialog && presentation === "app" && !isRemoteInstance && @@ -454,6 +457,7 @@ export const PackagesListTable: React.FC = ({ multiple: false, onClick: downloadPackage, icon: cloud_download, + isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), }, { name: "publish", @@ -461,7 +465,8 @@ export const PackagesListTable: React.FC = ({ multiple: false, onClick: publishPackage, icon: publish, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && !isImportDialog && presentation === "app" && !isRemoteInstance && @@ -473,7 +478,8 @@ export const PackagesListTable: React.FC = ({ text: i18n.t("Compare with local instance"), multiple: false, icon: compare, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && presentation === "app" && (isRemoteInstance || remoteStore !== undefined) && appConfigurator, @@ -484,7 +490,8 @@ export const PackagesListTable: React.FC = ({ text: i18n.t("Compare selected packages"), multiple: true, icon: compare_arrows, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && presentation === "app" && appConfigurator && (selectedIds ? selectedIds.length === 2 : false), @@ -496,7 +503,8 @@ export const PackagesListTable: React.FC = ({ multiple: false, onClick: importPackage, icon: arrow_downward, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && !isImportDialog && presentation === "app" && (isRemoteInstance || remoteStore !== undefined) && @@ -508,7 +516,8 @@ export const PackagesListTable: React.FC = ({ multiple: true, onClick: importPackagesFromWizard, icon: arrow_downward, - isActive: () => + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && !isImportDialog && presentation === "app" && (isRemoteInstance || remoteStore !== undefined) && @@ -719,11 +728,19 @@ export const PackagesListTable: React.FC = ({ isGlobalAdmin(api).then(setGlobalAdmin); }, [api]); + const rowConfig = React.useCallback( + (item: PackageModuleItem): RowConfig => ({ + selectable: isPackageItem(item), + }), + [] + ); + return ( resetKey={`${resetKey}`} rows={rowsFiltered} + rowConfig={rowConfig} columns={columns} details={details} actions={actions} diff --git a/src/presentation/react/components/package-list-table/PackageModuleItem.ts b/src/presentation/react/components/package-list-table/PackageModuleItem.ts index 4a98754f9..fff291b89 100644 --- a/src/presentation/react/components/package-list-table/PackageModuleItem.ts +++ b/src/presentation/react/components/package-list-table/PackageModuleItem.ts @@ -14,24 +14,24 @@ export type InstallStatus = "Installed" | "NotInstalled" | "Upgrade"; export type PackageItem = Omit & { installStatus: InstallStatus }; export const isPackageItem = (item: PackageModuleItem): item is PackageItem => { - return (item as PackageItem).version !== undefined; + return (item as PackageItem).module !== undefined; }; export const groupPackageByModuleAndVersion = (packages: PackageItem[]) => { return packages.reduce((acc, item) => { - const parent = acc.find( - parent => parent.id === item.module.id && parent.version === item.version - ); + const parentKey = `${item.module.id}-${item.version}`; + + const parent = acc.find(parent => parent.id === parentKey); if (parent) { return acc.map(parentItem => - parentItem.id === item.module.id && parentItem.version === item.version + parentItem.id === parentKey ? { ...parentItem, packages: [...parentItem.packages, item] } : parentItem ); } else { const newParent = { - id: item.module.id, + id: parentKey, name: item.module.name, version: item.version, packages: [item], From 84e17a9af05c75ed80743230154fc88b56e1c9db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 23 Nov 2020 16:03:01 +0100 Subject: [PATCH 06/32] Create DocumentsModel and to add to metadata models --- i18n/en.pot | 7 +++++-- i18n/es.po | 5 ++++- i18n/fr.po | 5 ++++- i18n/pt.po | 5 ++++- src/models/dhis/factory.ts | 1 + src/models/dhis/metadata.ts | 15 +++++++++++++++ src/utils/d2.ts | 19 +++++++++++++++++++ 7 files changed, 52 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a93693fad..e747becf0 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: 2020-11-20T09:27:57.833Z\n" -"PO-Revision-Date: 2020-11-20T09:27:57.833Z\n" +"POT-Creation-Date: 2020-11-23T14:59:34.275Z\n" +"PO-Revision-Date: 2020-11-23T14:59:34.275Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1706,6 +1706,9 @@ msgstr "" msgid "Program Rule" msgstr "" +msgid "Url" +msgstr "" + msgid "This URL and username combination already exists" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6ce42123a..ee6681daf 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1712,6 +1712,9 @@ msgstr "" msgid "Program Rule" msgstr "" +msgid "Url" +msgstr "" + msgid "This URL and username combination already exists" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 153603391..c650ce775 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1709,6 +1709,9 @@ msgstr "" msgid "Program Rule" msgstr "" +msgid "Url" +msgstr "" + msgid "This URL and username combination already exists" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 153603391..c650ce775 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1709,6 +1709,9 @@ msgstr "" msgid "Program Rule" msgstr "" +msgid "Url" +msgstr "" + msgid "This URL and username combination already exists" msgstr "" diff --git a/src/models/dhis/factory.ts b/src/models/dhis/factory.ts index cffcdbf58..041ce9b1d 100644 --- a/src/models/dhis/factory.ts +++ b/src/models/dhis/factory.ts @@ -20,6 +20,7 @@ export const metadataModels = [ metadataClasses.DataElementGroupSetModel, metadataClasses.DataEntryFormModel, metadataClasses.DataSetModel, + metadataClasses.DocumentsModel, metadataClasses.EventChartModel, metadataClasses.EventReportModel, metadataClasses.IndicatorModel, diff --git a/src/models/dhis/metadata.ts b/src/models/dhis/metadata.ts index cb555a15b..fa21f88cc 100644 --- a/src/models/dhis/metadata.ts +++ b/src/models/dhis/metadata.ts @@ -2,6 +2,9 @@ import { dataElementGroupFields, dataElementGroupSetFields, dataSetFields, + documentColumns, + documentDetails, + documentFields, organisationUnitFields, organisationUnitsColumns, organisationUnitsDetails, @@ -257,6 +260,18 @@ export class DataSetModel extends D2Model { ]; } +export class DocumentsModel extends D2Model { + protected static metadataType = "document"; + protected static collectionName = "documents" as const; + + protected static columns = documentColumns; + protected static details = documentDetails; + protected static fields = documentFields; + + protected static excludeRules = []; + protected static includeRules = []; +} + export class EventChartModel extends D2Model { protected static metadataType = "eventChart"; protected static collectionName = "eventCharts" as const; diff --git a/src/utils/d2.ts b/src/utils/d2.ts index 02b1de36f..467d18c84 100644 --- a/src/utils/d2.ts +++ b/src/utils/d2.ts @@ -72,6 +72,19 @@ export const organisationUnitsDetails: typeof d2BaseModelDetails = _.map( column => _.pick(column, ["name", "text", "getValue"]) ); +export const documentColumns: typeof d2BaseModelColumns = [ + { name: "displayName", text: i18n.t("Name"), sortable: true }, + { name: "created", text: i18n.t("Created"), sortable: true, hidden: true }, + { name: "lastUpdated", text: i18n.t("Last updated"), sortable: true }, + { name: "id", text: i18n.t("ID"), sortable: true, hidden: true }, + { name: "url", text: i18n.t("Url"), sortable: true, hidden: true }, + { name: "href", text: i18n.t("API link"), sortable: false, hidden: true }, +]; + +export const documentDetails: typeof d2BaseModelDetails = _.map(documentColumns, column => + _.pick(column, ["name", "text", "getValue"]) +); + export const d2BaseModelFields = { id: include, name: include, @@ -160,3 +173,9 @@ export const optionFields = { displayName: include, }, }; + +export const documentFields = { + ...d2BaseModelFields, + url: include, + external: include, +}; From 5b93de075f52f4327fb079900274a662be57f8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 23 Nov 2020 16:04:10 +0100 Subject: [PATCH 07/32] Create FileRepository --- package.json | 2 + src/data/file/FileD2Repository.ts | 78 +++++++++++++++++++++++++++++++ src/domain/file/FileRepository.ts | 12 +++++ yarn.lock | 7 ++- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/data/file/FileD2Repository.ts create mode 100644 src/domain/file/FileRepository.ts diff --git a/package.json b/package.json index 7bec24c6d..5a35d4da0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "jest": "26.1.0", "lodash": "4.17.19", "material-ui": "0.20.2", + "mime-types": "^2.1.27", "moment": "2.27.0", "nano-memoize": "1.2.0", "node-schedule": "1.3.2", @@ -91,6 +92,7 @@ "@types/jest": "26.0.4", "@types/jest-expect-message": "^1.0.2", "@types/lodash": "4.14.157", + "@types/mime-types": "^2.1.0", "@types/node": "14.0.22", "@types/node-schedule": "1.3.0", "@types/react": "16.9.43", diff --git a/src/data/file/FileD2Repository.ts b/src/data/file/FileD2Repository.ts new file mode 100644 index 000000000..46687539c --- /dev/null +++ b/src/data/file/FileD2Repository.ts @@ -0,0 +1,78 @@ +import { D2Document } from "d2-api/2.30"; +import { FileId, FileRepository } from "../../domain/file/FileRepository"; +import { Instance } from "../../domain/instance/entities/Instance"; +import mime from "mime-types"; + +interface SaveApiResponse { + response: { + fileResource: { + id: string; + }; + }; +} + +export class FileD2Repository implements FileRepository { + constructor(private instance: Instance) {} + + public async getById(fileId: FileId): Promise { + const auth = this.instance.auth; + + const authHeaders: Record = this.getAuthHeaders(auth); + + const fetchOptions: RequestInit = { + method: "GET", + headers: { ...authHeaders }, + credentials: auth ? "omit" : ("include" as const), + }; + + const documentResponse = await fetch( + new URL(`/api/documents/${fileId}`, this.instance.url).href, + fetchOptions + ); + + const document: D2Document = JSON.parse(await documentResponse.text()); + + const response = await fetch( + new URL(`/api/documents/${fileId}/data`, this.instance.url).href, + fetchOptions + ); + const blob = await response.blob(); + + return this.blobToFile(blob, `${document.name}.${mime.extension(blob.type)}`); + } + + public async save(file: File): Promise { + const auth = this.instance.auth; + + const authHeaders: Record = this.getAuthHeaders(auth); + + const formdata = new FormData(); + formdata.append("file", file); + formdata.append("filename", file.name); + + const fetchOptions: RequestInit = { + method: "POST", + headers: { ...authHeaders }, + body: formdata, + credentials: auth ? "omit" : ("include" as const), + }; + + const response = await fetch( + new URL(`/api/fileResources`, this.instance.url).href, + fetchOptions + ); + const apiResponse: SaveApiResponse = JSON.parse(await response.text()); + + return apiResponse.response.fileResource.id; + } + + private getAuthHeaders( + auth: { username: string; password: string } | undefined + ): Record { + return auth ? { Authorization: "Basic " + btoa(auth.username + ":" + auth.password) } : {}; + } + + private blobToFile = (blob: Blob, fileName: string): File => { + return new File([blob], fileName, { type: blob.type }); + }; +} diff --git a/src/domain/file/FileRepository.ts b/src/domain/file/FileRepository.ts new file mode 100644 index 000000000..41b2b87ba --- /dev/null +++ b/src/domain/file/FileRepository.ts @@ -0,0 +1,12 @@ +import { Instance } from "../instance/entities/Instance"; + +export interface FileRepositoryConstructor { + new (instance: Instance): FileRepository; +} + +export type FileId = string; + +export interface FileRepository { + getById(fileId: FileId): Promise; + save(file: File): Promise; +} diff --git a/yarn.lock b/yarn.lock index 15acb69f5..ec97b2293 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2328,6 +2328,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== +"@types/mime-types@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73" + integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM= + "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" @@ -10595,7 +10600,7 @@ mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== From 275b10ebecaa3931130416656a308be9cd728ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 23 Nov 2020 16:12:18 +0100 Subject: [PATCH 08/32] Modify metadata sync to include internal documents --- i18n/en.pot | 4 +- src/domain/Repositories.ts | 1 + .../metadata/usecases/MetadataSyncUseCase.ts | 37 +++++++++++++++++-- .../usecases/GenericSyncUseCase.ts | 9 +++++ src/presentation/CompositionRoot.ts | 2 + 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index e747becf0..ed76a21f7 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: 2020-11-23T14:59:34.275Z\n" -"PO-Revision-Date: 2020-11-23T14:59:34.275Z\n" +"POT-Creation-Date: 2020-11-23T15:06:18.912Z\n" +"PO-Revision-Date: 2020-11-23T15:06:18.912Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/domain/Repositories.ts b/src/domain/Repositories.ts index a0b80cb18..bf94f57fe 100644 --- a/src/domain/Repositories.ts +++ b/src/domain/Repositories.ts @@ -7,4 +7,5 @@ export const Repositories = { EventsRepository: "eventsRepository", MetadataRepository: "metadataRepository", TransformationRepository: "transformationsRepository", + FileRepository: "fileRepository", }; diff --git a/src/domain/metadata/usecases/MetadataSyncUseCase.ts b/src/domain/metadata/usecases/MetadataSyncUseCase.ts index 0b687d09e..60d6b370b 100644 --- a/src/domain/metadata/usecases/MetadataSyncUseCase.ts +++ b/src/domain/metadata/usecases/MetadataSyncUseCase.ts @@ -9,7 +9,7 @@ import { Instance } from "../../instance/entities/Instance"; import { MappingMapper } from "../../mapping/helpers/MappingMapper"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; import { GenericSyncUseCase } from "../../synchronization/usecases/GenericSyncUseCase"; -import { MetadataEntities, MetadataPackage } from "../entities/MetadataEntities"; +import { MetadataEntities, MetadataPackage, Document } from "../entities/MetadataEntities"; import { buildNestedRules, cleanObject, cleanReferences, getAllReferences } from "../utils"; export class MetadataSyncUseCase extends GenericSyncUseCase { @@ -129,9 +129,15 @@ export class MetadataSyncUseCase extends GenericSyncUseCase { const { syncParams } = this.builder; const payloadPackage = await this.buildPayload(); + + const payloadWithDocumentFiles = await this.createDocumentFilesInRemote( + instance, + payloadPackage + ); + const mappedPayloadPackage = syncParams?.enableMapping - ? await this.mapPayload(instance, payloadPackage) - : payloadPackage; + ? await this.mapPayload(instance, payloadWithDocumentFiles) + : payloadWithDocumentFiles; debug("Metadata package", { payloadPackage, mappedPayloadPackage }); @@ -165,4 +171,29 @@ export class MetadataSyncUseCase extends GenericSyncUseCase { return mapper.applyMapping(payload); } + + public async createDocumentFilesInRemote( + instance: Instance, + payload: MetadataPackage + ): Promise { + const documents = payload.documents as Document[] | undefined; + + if (documents) { + const newDocuments = await promiseMap(documents, async (document: Document) => { + if (!document.external) { + const fileRepository = await this.getFileRepository(); + const file = await fileRepository.getById(document.id); + const fileRemoteRepository = await this.getFileRepository(instance); + const fileId = await fileRemoteRepository.save(file); + return { ...document, url: fileId }; + } else { + return document; + } + }); + + return { ...payload, documents: newDocuments }; + } else { + return payload; + } + } } diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 5105aeb9f..748e243f6 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -15,6 +15,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { EventsPackage } from "../../events/entities/EventsPackage"; import { EventsRepositoryConstructor } from "../../events/repositories/EventsRepository"; import { EventsSyncUseCase } from "../../events/usecases/EventsSyncUseCase"; +import { FileRepositoryConstructor } from "../../file/FileRepository"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataMapping, MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; @@ -102,6 +103,14 @@ export abstract class GenericSyncUseCase { ); } + @cache() + protected async getFileRepository(remoteInstance?: Instance) { + const defaultInstance = await this.getOriginInstance(); + return this.repositoryFactory.get(Repositories.FileRepository, [ + remoteInstance ?? defaultInstance, + ]); + } + @cache() protected async getAggregatedRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index ca4905718..609c51e5f 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -1,5 +1,6 @@ import { AggregatedD2ApiRepository } from "../data/aggregated/AggregatedD2ApiRepository"; import { EventsD2ApiRepository } from "../data/events/EventsD2ApiRepository"; +import { FileD2Repository } from "../data/file/FileD2Repository"; import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepository"; import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; @@ -83,6 +84,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); this.repositoryFactory.bind(Repositories.EventsRepository, EventsD2ApiRepository); this.repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); + this.repositoryFactory.bind(Repositories.FileRepository, FileD2Repository); this.repositoryFactory.bind( Repositories.MetadataRepository, MetadataJSONRepository, From dd10f8586df2224092659c04a702c6d4f740f475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 24 Nov 2020 07:23:48 +0100 Subject: [PATCH 09/32] Show documents errors in sync summary --- i18n/en.pot | 4 ++-- src/data/file/FileD2Repository.ts | 23 ++++++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ed76a21f7..a051be35f 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: 2020-11-23T15:06:18.912Z\n" -"PO-Revision-Date: 2020-11-23T15:06:18.912Z\n" +"POT-Creation-Date: 2020-11-24T06:20:39.492Z\n" +"PO-Revision-Date: 2020-11-24T06:20:39.492Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/data/file/FileD2Repository.ts b/src/data/file/FileD2Repository.ts index 46687539c..c1017139b 100644 --- a/src/data/file/FileD2Repository.ts +++ b/src/data/file/FileD2Repository.ts @@ -36,9 +36,16 @@ export class FileD2Repository implements FileRepository { new URL(`/api/documents/${fileId}/data`, this.instance.url).href, fetchOptions ); - const blob = await response.blob(); - return this.blobToFile(blob, `${document.name}.${mime.extension(blob.type)}`); + if (!response.ok) { + throw Error( + `An error has ocurred retrieving the file resource of document '${document.name}' from ${this.instance.name}` + ); + } else { + const blob = await response.blob(); + + return this.blobToFile(blob, `${document.name}.${mime.extension(blob.type)}`); + } } public async save(file: File): Promise { @@ -61,9 +68,15 @@ export class FileD2Repository implements FileRepository { new URL(`/api/fileResources`, this.instance.url).href, fetchOptions ); - const apiResponse: SaveApiResponse = JSON.parse(await response.text()); - - return apiResponse.response.fileResource.id; + if (!response.ok) { + throw Error( + `An error has ocurred saving the resource file of the document '${file.name}' in ${this.instance.name}` + ); + } else { + const apiResponse: SaveApiResponse = JSON.parse(await response.text()); + + return apiResponse.response.fileResource.id; + } } private getAuthHeaders( From d2f7a5f1e5fa32ac9a7d60e02871097db99d45b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 24 Nov 2020 11:51:22 +0100 Subject: [PATCH 10/32] Allow include autogenerated modules in list use case --- src/domain/modules/usecases/ListModulesUseCase.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index 6c74b1c8b..1ed60145c 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -14,7 +14,8 @@ export class ListModulesUseCase implements UseCase { public async execute( bypassSharingSettings = false, - instance = this.localInstance + instance = this.localInstance, + includeAutogenerated = false ): Promise { const userGroups = await this.instanceRepository(this.localInstance).getUserGroups(); const { id: userId } = await this.instanceRepository(this.localInstance).getUser(); @@ -23,7 +24,7 @@ export class ListModulesUseCase implements UseCase { await this.storageRepository(instance).listObjectsInCollection( Namespace.MODULES ) - ).filter(module => !module.autogenerated); + ).filter(module => includeAutogenerated || !module.autogenerated); return data .map(module => { From 9190a6c9d6721addbae5e2635e8447bb06d078f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 24 Nov 2020 11:51:44 +0100 Subject: [PATCH 11/32] Add generate module action in package list --- i18n/en.pot | 16 ++++- i18n/es.po | 14 ++++- i18n/fr.po | 14 ++++- i18n/pt.po | 14 ++++- .../package-list-table/PackageListTable.tsx | 62 +++++++++++++++++++ 5 files changed, 115 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a051be35f..f8e9308f7 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: 2020-11-24T06:20:39.492Z\n" -"PO-Revision-Date: 2020-11-24T06:20:39.492Z\n" +"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" +"PO-Revision-Date: 2020-11-24T10:33:51.362Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -641,6 +641,15 @@ msgstr "" msgid "Couldn't create new branch on store" msgstr "" +msgid "Generating module" +msgstr "" + +msgid "Module generated successfully" +msgstr "" + +msgid "An error has ocurred generating the module" +msgstr "" + msgid "Installed" msgstr "" @@ -680,6 +689,9 @@ msgstr "" msgid "Import package (wizard)" msgstr "" +msgid "Generate Module" +msgstr "" + msgid "Not recommended" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index ee6681daf..6e6b729f0 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" +"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -645,6 +645,15 @@ msgstr "" msgid "Couldn't create new branch on store" msgstr "" +msgid "Generating module" +msgstr "" + +msgid "Module generated successfully" +msgstr "" + +msgid "An error has ocurred generating the module" +msgstr "" + msgid "Installed" msgstr "" @@ -684,6 +693,9 @@ msgstr "" msgid "Import package (wizard)" msgstr "" +msgid "Generate Module" +msgstr "" + msgid "Not recommended" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index c650ce775..a416ad4c8 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" +"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -642,6 +642,15 @@ msgstr "" msgid "Couldn't create new branch on store" msgstr "" +msgid "Generating module" +msgstr "" + +msgid "Module generated successfully" +msgstr "" + +msgid "An error has ocurred generating the module" +msgstr "" + msgid "Installed" msgstr "" @@ -681,6 +690,9 @@ msgstr "" msgid "Import package (wizard)" msgstr "" +msgid "Generate Module" +msgstr "" + msgid "Not recommended" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index c650ce775..a416ad4c8 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-23T06:00:25.783Z\n" +"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -642,6 +642,15 @@ msgstr "" msgid "Couldn't create new branch on store" msgstr "" +msgid "Generating module" +msgstr "" + +msgid "Module generated successfully" +msgstr "" + +msgid "An error has ocurred generating the module" +msgstr "" + msgid "Installed" msgstr "" @@ -681,6 +690,9 @@ msgstr "" msgid "Import package (wizard)" msgstr "" +msgid "Generate Module" +msgstr "" + msgid "Not recommended" msgstr "" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index 926b8b416..aa2ffbf74 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -18,6 +18,7 @@ import semver from "semver"; import { Either } from "../../../../domain/common/entities/Either"; import { NamedRef } from "../../../../domain/common/entities/Ref"; import { JSONDataSource } from "../../../../domain/instance/entities/JSONDataSource"; +import { Module } from "../../../../domain/modules/entities/Module"; import { ImportedPackage } from "../../../../domain/package-import/entities/ImportedPackage"; import { isInstance, @@ -68,6 +69,7 @@ export const PackagesListTable: React.FC = ({ const [instancePackages, setInstancePackages] = useState([]); const [storePackages, setStorePackages] = useState([]); const [importedPackages, setImportedPackages] = useState([]); + const [modules, setModules] = useState([]); const rows = remoteStore ? storePackages : instancePackages; const [resetKey, setResetKey] = useState(Math.random()); @@ -289,6 +291,37 @@ export const PackagesListTable: React.FC = ({ setOpenImportPackageDialog(true); }, []); + const generateModule = useCallback( + async (ids: string[]) => { + loading.show(true, i18n.t("Generating module")); + + const selectedPackage = rows.find(row => row.id === ids[0]); + const module = selectedPackage + ? modules.find( + module => selectedPackage.module && module.id === selectedPackage.module.id + ) + : undefined; + + if (module) { + const editedModule = module?.update({ autogenerated: false }); + + const moduleErrors = await compositionRoot.modules.save(editedModule); + + if (moduleErrors.length === 0) { + loading.reset(); + snackbar.success(i18n.t("Module generated successfully")); + } else { + loading.reset(); + snackbar.error(moduleErrors.map(error => error.description).join("\n")); + } + } else { + loading.reset(); + snackbar.error(i18n.t("An error has ocurred generating the module")); + } + }, + [compositionRoot, rows, modules, snackbar, loading] + ); + const importPackage = useCallback( async (ids: string[]) => { const packageSource: PackageSource = getPackageSourceToImport(); @@ -329,8 +362,10 @@ export const PackagesListTable: React.FC = ({ ? "FAILURE" : "DONE" ); + report.addSyncResult({ ...result, + originPackage: originPackage.toRef(), origin: remoteInstance?.toPublicObject(), }); await report.save(api); @@ -523,6 +558,27 @@ export const PackagesListTable: React.FC = ({ (isRemoteInstance || remoteStore !== undefined) && appConfigurator, }, + { + name: "generateModule", + text: i18n.t("Generate Module"), + onClick: generateModule, + icon: note_add, + isActive: (rows: PackageModuleItem[]) => { + const module = modules.find( + module => rows[0].module && module.id === rows[0].module.id + ); + + return ( + _.every(rows, row => isPackageItem(row)) && + !isImportDialog && + presentation === "app" && + rows[0].installStatus === "Installed" && + module !== undefined && + module.autogenerated === true && + appConfigurator + ); + }, + }, ], [ appConfigurator, @@ -538,6 +594,8 @@ export const PackagesListTable: React.FC = ({ remoteStore, isImportDialog, selectedIds, + generateModule, + modules, ] ); @@ -716,6 +774,10 @@ export const PackagesListTable: React.FC = ({ ); }, [compositionRoot, snackbar, resetKey]); + useEffect(() => { + compositionRoot.modules.list(globalAdmin, remoteInstance, true).then(setModules); + }, [compositionRoot, globalAdmin, remoteInstance]); + useEffect(() => { setModuleFilter(""); setDhis2VersionFilter(""); From 7321735730ef75afd712b98e7fe69286637c2e3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 24 Nov 2020 12:12:16 +0100 Subject: [PATCH 12/32] Reorder use effect --- i18n/en.pot | 4 ++-- .../components/package-list-table/PackageListTable.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index f8e9308f7..d426815dc 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: 2020-11-24T10:33:51.362Z\n" -"PO-Revision-Date: 2020-11-24T10:33:51.362Z\n" +"POT-Creation-Date: 2020-11-24T11:00:41.553Z\n" +"PO-Revision-Date: 2020-11-24T11:00:41.553Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index aa2ffbf74..f1c72faf9 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -92,6 +92,10 @@ export const PackagesListTable: React.FC = ({ const isRemoteInstance = !!remoteInstance; + useEffect(() => { + compositionRoot.modules.list(globalAdmin, remoteInstance, true).then(setModules); + }, [compositionRoot, globalAdmin, remoteInstance]); + const updateSelection = useCallback( (selection: TableSelection[]) => { updateStateSelection(selection); @@ -365,7 +369,6 @@ export const PackagesListTable: React.FC = ({ report.addSyncResult({ ...result, - originPackage: originPackage.toRef(), origin: remoteInstance?.toPublicObject(), }); await report.save(api); @@ -774,10 +777,6 @@ export const PackagesListTable: React.FC = ({ ); }, [compositionRoot, snackbar, resetKey]); - useEffect(() => { - compositionRoot.modules.list(globalAdmin, remoteInstance, true).then(setModules); - }, [compositionRoot, globalAdmin, remoteInstance]); - useEffect(() => { setModuleFilter(""); setDhis2VersionFilter(""); From 925729f02cdf62bd97a11a92abc542122e1d9b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 24 Nov 2020 13:11:21 +0100 Subject: [PATCH 13/32] Fix bug origin package not assigned from normal import --- .../react/components/package-list-table/PackageListTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index f1c72faf9..88b370e4b 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -369,6 +369,7 @@ export const PackagesListTable: React.FC = ({ report.addSyncResult({ ...result, + originPackage: originPackage.toRef(), origin: remoteInstance?.toPublicObject(), }); await report.save(api); From 2d1b68b375fbcc46b8dfff6331e6d165033ba2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 08:32:59 +0100 Subject: [PATCH 14/32] Add Save & import button --- i18n/en.pot | 19 +- i18n/es.po | 17 +- i18n/fr.po | 17 +- i18n/pt.po | 17 +- .../CreatePackageFromFileDialog.tsx | 165 ++++++++++-------- 5 files changed, 137 insertions(+), 98 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d426815dc..aac9c47cd 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: 2020-11-24T11:00:41.553Z\n" -"PO-Revision-Date: 2020-11-24T11:00:41.553Z\n" +"POT-Creation-Date: 2020-11-25T07:24:45.796Z\n" +"PO-Revision-Date: 2020-11-25T07:24:45.796Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -143,6 +143,15 @@ msgstr "" msgid "Description" msgstr "" +msgid "Cancel" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save & import" +msgstr "" + msgid "Identifier" msgstr "" @@ -176,9 +185,6 @@ msgstr "" msgid "Update" msgstr "" -msgid "Cancel" -msgstr "" - msgid "Match string (name, code, description)" msgstr "" @@ -526,9 +532,6 @@ msgstr "" msgid "Removed {{difference}} elements" msgstr "" -msgid "Save" -msgstr "" - msgid "Metadata exclusions" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6e6b729f0..ad7c43b2c 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" +"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -143,6 +143,15 @@ msgstr "" msgid "Description" msgstr "" +msgid "Cancel" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save & import" +msgstr "" + msgid "Identifier" msgstr "" @@ -176,9 +185,6 @@ msgstr "" msgid "Update" msgstr "" -msgid "Cancel" -msgstr "" - msgid "Match string (name, code, description)" msgstr "" @@ -527,9 +533,6 @@ msgstr "" msgid "Removed {{difference}} elements" msgstr "" -msgid "Save" -msgstr "" - msgid "Metadata exclusions" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index a416ad4c8..e32d7cd52 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" +"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -143,6 +143,15 @@ msgstr "" msgid "Description" msgstr "" +msgid "Cancel" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save & import" +msgstr "" + msgid "Identifier" msgstr "" @@ -176,9 +185,6 @@ msgstr "" msgid "Update" msgstr "" -msgid "Cancel" -msgstr "" - msgid "Match string (name, code, description)" msgstr "" @@ -527,9 +533,6 @@ msgstr "" msgid "Removed {{difference}} elements" msgstr "" -msgid "Save" -msgstr "" - msgid "Metadata exclusions" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index a416ad4c8..e32d7cd52 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-24T10:33:51.362Z\n" +"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -143,6 +143,15 @@ msgstr "" msgid "Description" msgstr "" +msgid "Cancel" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Save & import" +msgstr "" + msgid "Identifier" msgstr "" @@ -176,9 +185,6 @@ msgstr "" msgid "Update" msgstr "" -msgid "Cancel" -msgstr "" - msgid "Match string (name, code, description)" msgstr "" @@ -527,9 +533,6 @@ msgstr "" msgid "Removed {{difference}} elements" msgstr "" -msgid "Save" -msgstr "" - msgid "Metadata exclusions" msgstr "" diff --git a/src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx b/src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx index 1ecd33522..9e8bf1475 100644 --- a/src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx +++ b/src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx @@ -1,9 +1,17 @@ import i18n from "../../../../locales"; import MetadataDropZone from "../metadata-drop-zone/MetadataDropZone"; import { MetadataPackage } from "../../../../domain/metadata/entities/MetadataEntities"; -import { makeStyles, TextField } from "@material-ui/core"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + makeStyles, + TextField, +} from "@material-ui/core"; import Autocomplete from "@material-ui/lab/Autocomplete"; -import { ConfirmationDialog, useLoading, useSnackbar } from "d2-ui-components"; +import { useLoading, useSnackbar } from "d2-ui-components"; import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import semver from "semver"; @@ -21,11 +29,13 @@ import { Module } from "../../../../domain/modules/entities/Module"; interface CreatePackageFromFileDialogProps { onClose: () => void; onSaved?: () => void; + onImport?: (packakeId: string) => void; } export const CreatePackageFromFileDialog: React.FC = ({ onClose, onSaved, + onImport, }) => { const { compositionRoot } = useAppContext(); const loading = useLoading(); @@ -149,7 +159,7 @@ export const CreatePackageFromFileDialog: React.FC { + const onSave = async (importAfter: boolean) => { i18n.t("Creating autogenerated module"); const moduleErrors = module .validate() @@ -165,7 +175,9 @@ export const CreatePackageFromFileDialog: React.FC error.description).join("\n")); setErrors(messages); @@ -195,78 +207,93 @@ export const CreatePackageFromFileDialog: React.FC - - -
    - -
    + + {i18n.t("Generate package from File")} -
    + + +
    + +
    + +
    + + +
    + + updateVersions(value)} + renderTags={(values: string[]) => values.sort().join(", ")} + renderInput={params => ( + + )} + /> + -
    - - updateVersions(value)} - renderTags={(values: string[]) => values.sort().join(", ")} - renderInput={params => ( - - )} - /> - - - - -
    + + + + + + + + + + + + ); }; From 1e38187b62e7e135b389cf710933e1f0771cbb43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 11:53:42 +0100 Subject: [PATCH 15/32] Create GetLocalInstanceUseCase --- .../instance/usecases/GetLocalInstanceUseCase.ts | 10 ++++++++++ src/presentation/CompositionRoot.ts | 2 ++ 2 files changed, 12 insertions(+) create mode 100644 src/domain/instance/usecases/GetLocalInstanceUseCase.ts diff --git a/src/domain/instance/usecases/GetLocalInstanceUseCase.ts b/src/domain/instance/usecases/GetLocalInstanceUseCase.ts new file mode 100644 index 000000000..e9efcc81b --- /dev/null +++ b/src/domain/instance/usecases/GetLocalInstanceUseCase.ts @@ -0,0 +1,10 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { Instance } from "../entities/Instance"; + +export class GetLocalInstanceUseCase implements UseCase { + constructor(private localInstance: Instance) {} + + public async execute(): Promise { + return this.localInstance; + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 609c51e5f..69ec71938 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -18,6 +18,7 @@ import { DeleteInstanceUseCase } from "../domain/instance/usecases/DeleteInstanc import { GetInstanceApiUseCase } from "../domain/instance/usecases/GetInstanceApiUseCase"; import { GetInstanceByIdUseCase } from "../domain/instance/usecases/GetInstanceByIdUseCase"; import { GetInstanceVersionUseCase } from "../domain/instance/usecases/GetInstanceVersionUseCase"; +import { GetLocalInstanceUseCase } from "../domain/instance/usecases/GetLocalInstanceUseCase"; import { GetRootOrgUnitUseCase } from "../domain/instance/usecases/GetRootOrgUnitUseCase"; import { GetUserGroupsUseCase } from "../domain/instance/usecases/GetUserGroupsUseCase"; import { ListInstancesUseCase } from "../domain/instance/usecases/ListInstancesUseCase"; @@ -252,6 +253,7 @@ export class CompositionRoot { public get instances() { return getExecute({ getApi: new GetInstanceApiUseCase(this.repositoryFactory, this.localInstance), + getLocal: new GetLocalInstanceUseCase(this.localInstance), list: new ListInstancesUseCase( this.repositoryFactory, this.localInstance, From a291b2f1306d05eea59c6c33743a296d6c076eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 11:55:30 +0100 Subject: [PATCH 16/32] Create MappingOwnerPackage --- src/domain/mapping/entities/MappingOwner.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/domain/mapping/entities/MappingOwner.ts b/src/domain/mapping/entities/MappingOwner.ts index ae3f7d848..69764c696 100644 --- a/src/domain/mapping/entities/MappingOwner.ts +++ b/src/domain/mapping/entities/MappingOwner.ts @@ -9,7 +9,12 @@ export interface MappingOwnerInstance { id: string; } -export type MappingOwner = MappingOwnerStore | MappingOwnerInstance; +export interface MappingOwnerPackage { + type: "package"; + id: string; +} + +export type MappingOwner = MappingOwnerStore | MappingOwnerInstance | MappingOwnerPackage; export const isMappingOwnerStore = (source: MappingOwner): source is MappingOwnerStore => { return source.type === "store"; @@ -19,4 +24,4 @@ export const isMappingOwnerInstance = (source: MappingOwner): source is MappingO return source.type === "instance"; }; -export type MappingOwnerType = "instance" | "store"; +export type MappingOwnerType = "instance" | "store" | "package"; From f26cf7b5ab2a3f18c41a9d3dd9215fdb4702b7cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 11:56:07 +0100 Subject: [PATCH 17/32] Fix bug overwritting package id to save --- src/domain/packages/usecases/CreatePackageUseCase.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index ac6f702d8..53dec9841 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -1,4 +1,3 @@ -import { generateUid } from "d2/uid"; import { metadataTransformations } from "../../../data/transformations/PackageTransformations"; import { CompositionRoot } from "../../../presentation/CompositionRoot"; import { cache } from "../../../utils/cache"; @@ -64,7 +63,6 @@ export class CreatePackageUseCase implements UseCase { if (validations.length === 0) { const user = await instanceRepository.getUser(); const newPackage = payload.update({ - id: generateUid(), lastUpdated: new Date(), lastUpdatedBy: user, user: payload.user.id ? payload.user : user, From 8c228afad1d0e50cfd05f6bf1304c7fdd2335005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 11:59:16 +0100 Subject: [PATCH 18/32] show package import dialog after file dialog --- i18n/en.pot | 10 +++- i18n/es.po | 8 +++- i18n/fr.po | 8 +++- i18n/pt.po | 8 +++- .../PackageImportDialog.tsx | 3 ++ .../PackageImportWizard.tsx | 12 ++++- .../steps/PackageMappingStep.tsx | 48 +++++++++++++------ .../ModulePackageListPage.tsx | 30 ++++++++++-- 8 files changed, 104 insertions(+), 23 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index aac9c47cd..2a97fc95b 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: 2020-11-25T07:24:45.796Z\n" -"PO-Revision-Date: 2020-11-25T07:24:45.796Z\n" +"POT-Creation-Date: 2020-11-25T10:53:58.258Z\n" +"PO-Revision-Date: 2020-11-25T10:53:58.258Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -580,9 +580,15 @@ msgstr "" msgid "Unknown error happened loading package" msgstr "" +msgid "Unknown error happened loading module" +msgstr "" + msgid "Unknown error happened loading store" msgstr "" +msgid "There are not elements to map in the package" +msgstr "" + msgid "Existing mapping will be used" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index ad7c43b2c..183a11761 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" +"POT-Creation-Date: 2020-11-25T10:47:53.003Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -584,9 +584,15 @@ msgstr "Paquetes" msgid "Unknown error happened loading package" msgstr "" +msgid "Unknown error happened loading module" +msgstr "" + msgid "Unknown error happened loading store" msgstr "" +msgid "There are not elements to map in the package" +msgstr "" + msgid "Existing mapping will be used" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index e32d7cd52..cac5ca4a8 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" +"POT-Creation-Date: 2020-11-25T10:47:53.003Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -581,9 +581,15 @@ msgstr "" msgid "Unknown error happened loading package" msgstr "" +msgid "Unknown error happened loading module" +msgstr "" + msgid "Unknown error happened loading store" msgstr "" +msgid "There are not elements to map in the package" +msgstr "" + msgid "Existing mapping will be used" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index e32d7cd52..cac5ca4a8 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-25T06:26:27.611Z\n" +"POT-Creation-Date: 2020-11-25T10:47:53.003Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -581,9 +581,15 @@ msgstr "" msgid "Unknown error happened loading package" msgstr "" +msgid "Unknown error happened loading module" +msgstr "" + msgid "Unknown error happened loading store" msgstr "" +msgid "There are not elements to map in the package" +msgstr "" + msgid "Existing mapping will be used" msgstr "" diff --git a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx index c52a6e052..3926e02ea 100644 --- a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx +++ b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx @@ -23,6 +23,7 @@ interface PackageImportDialogProps { selectedPackagesId?: string[]; onClose: () => void; openSyncSummary?: (result: SyncReport) => void; + disablePackageSelection?: boolean; } const PackageImportDialog: React.FC = ({ @@ -31,6 +32,7 @@ const PackageImportDialog: React.FC = ({ selectedPackagesId, onClose, openSyncSummary, + disablePackageSelection, }) => { const [enableImport, setEnableImport] = useState(false); const snackbar = useSnackbar(); @@ -206,6 +208,7 @@ const PackageImportDialog: React.FC = ({ onChange={handlePackageImportRuleChange} onCancel={onClose} onClose={onClose} + disablePackageSelection={disablePackageSelection} /> diff --git a/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx b/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx index 56b2bed08..1bd41e4ab 100644 --- a/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx +++ b/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx @@ -47,17 +47,27 @@ export const stepsBaseInfo = [ }, ]; +const stepsRelatedToPackageSelection = ["instance-playstore", "packages"]; + export interface PackageImportWizardProps { packageImportRule: PackageImportRule; onChange: (packageImportRule: PackageImportRule) => void; onCancel: () => void; onClose: () => void; + disablePackageSelection?: boolean; } export const PackageImportWizard: React.FC = props => { const location = useLocation(); - const steps = stepsBaseInfo.map(step => ({ ...step, props })); + const steps = stepsBaseInfo + .filter( + step => + !props.disablePackageSelection || + (props.disablePackageSelection && + !stepsRelatedToPackageSelection.includes(step.key)) + ) + .map(step => ({ ...step, props })); const onStepChangeRequest = async (_currentStep: WizardStep, newStep: WizardStep) => { const index = _(steps).findIndex(step => step.key === newStep.key); diff --git a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx b/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx index adddc62fe..410cb1eb4 100644 --- a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx +++ b/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx @@ -72,15 +72,17 @@ export const PackageMappingStep: React.FC = ({ package const newMapping = dataSourceMapping.updateMappingDictionary(metadataMapping); - const result = await compositionRoot.mapping.save(newMapping); - result.match({ - error: () => { - snackbar.error(i18n.t("Could not save mapping")); - }, - success: () => { - setDataSourceMapping(newMapping); - }, - }); + if (newMapping.owner.type !== "package") { + const result = await compositionRoot.mapping.save(newMapping); + result.match({ + error: () => { + snackbar.error(i18n.t("Could not save mapping")); + }, + success: () => { + setDataSourceMapping(newMapping); + }, + }); + } }, [compositionRoot, dataSourceMapping, snackbar] ); @@ -120,13 +122,29 @@ export const PackageMappingStep: React.FC = ({ package error: async () => { snackbar.error(i18n.t("Unknown error happened loading package")); }, - success: async ({ contents }) => { + success: async ({ dhisVersion, module, contents, id }) => { setPackageContents(contents); + + const fullModule = await compositionRoot.modules.get(module.id, source); + + if (fullModule) { + if (fullModule.autogenerated) { + const temporalMapping = DataSourceMapping.build({ + owner: { type: "package" as const, id }, + mappingDictionary: {}, + }); + + setDataSourceMapping(temporalMapping); + setInstance(JSONDataSource.build(dhisVersion, contents)); + } else { + setDataSourceMapping(mapping); + setInstance(source); + } + } else { + snackbar.error(i18n.t("Unknown error happened loading module")); + } }, }); - - setDataSourceMapping(mapping); - setInstance(source); } else { const result = await compositionRoot.packages.getStore(source.id, packageId); @@ -186,7 +204,9 @@ export const PackageMappingStep: React.FC = ({ package const noMappedIds = _.difference(contentsIds, mappingIds); const message = - noMappedIds.length === 0 + contentsIds.length === 0 + ? i18n.t("There are not elements to map in the package") + : noMappedIds.length === 0 ? i18n.t("Existing mapping will be used") : noMappedIds.length < contentsIds.length ? i18n.t( diff --git a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx index c6161c7d8..0593fd724 100644 --- a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx @@ -1,6 +1,6 @@ import { Icon } from "@material-ui/core"; import { PaginationOptions } from "d2-ui-components"; -import React, { ReactNode, useCallback, useMemo, useState } from "react"; +import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { Store } from "../../../../domain/packages/entities/Store"; @@ -15,6 +15,7 @@ import { import PackageImportDialog from "../../../react/components/package-import-dialog/PackageImportDialog"; import PageHeader from "../../../react/components/page-header/PageHeader"; import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; +import { useAppContext } from "../../../react/contexts/AppContext"; export interface ModulePackageListPageProps { remoteInstance?: Instance; @@ -34,11 +35,21 @@ export const ModulePackageListPage: React.FC = () => { const [openImportPackageDialog, setOpenImportPackageDialog] = useState(false); const [addPackageDialogOpen, setAddPackageDialogOpen] = useState(false); const [selectedInstance, setSelectedInstance] = useState(); + const [localInstance, setLocalInstance] = useState(); const [resetKey, setResetKey] = useState(Math.random); + const [selectedPackagesId, setSelectedPackagesId] = useState([]); + const [disablePackageSelection, setDisablePackageSelection] = useState(false); + const { compositionRoot } = useAppContext(); const { list: tableOption = "modules" } = useParams<{ list: ViewOption }>(); const title = buildTitle(tableOption); + useEffect(() => { + //TODO: when we have local instance in data store this will not be necessary + // because local instance will be selected by dropdown + compositionRoot.instances.getLocal().then(setLocalInstance); + }, [compositionRoot]); + const backHome = useCallback(() => { history.push("/"); }, [history]); @@ -50,6 +61,7 @@ export const ModulePackageListPage: React.FC = () => { if (!selectedInstance) { setAddPackageDialogOpen(true); } else { + setDisablePackageSelection(false); setOpenImportPackageDialog(true); } } @@ -86,6 +98,15 @@ export const ModulePackageListPage: React.FC = () => { } }; + const handleOnImportFromFilePackage = (packageId: string) => { + setResetKey(Math.random()); + setSelectedPackagesId([packageId]); + setOpenImportPackageDialog(true); + setDisablePackageSelection(true); + }; + + const instanceInImportDialog = selectedInstance ?? localInstance; + return ( @@ -113,12 +134,14 @@ export const ModulePackageListPage: React.FC = () => { setSyncReport(undefined)} /> )} - {selectedInstance && ( + {instanceInImportDialog && ( setOpenImportPackageDialog(false)} - instance={selectedInstance} + instance={instanceInImportDialog} + selectedPackagesId={selectedPackagesId} openSyncSummary={handleOpenSyncSummaryFromDialog} + disablePackageSelection={disablePackageSelection} /> )} @@ -126,6 +149,7 @@ export const ModulePackageListPage: React.FC = () => { setAddPackageDialogOpen(false)} onSaved={handleCreatedNewPackageFromFile} + onImport={handleOnImportFromFilePackage} /> )} From e3b975b7680e8b929e45cc005a713281c65473f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 25 Nov 2020 13:29:40 +0100 Subject: [PATCH 19/32] Use temporal package mapping to import --- i18n/en.pot | 4 +-- .../entities/PackageImportRule.ts | 30 +++++++++++++++++++ .../PackageImportDialog.tsx | 16 ++++++---- .../steps/PackageMappingStep.tsx | 27 ++++++++++++----- 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 2a97fc95b..54c607dd2 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: 2020-11-25T10:53:58.258Z\n" -"PO-Revision-Date: 2020-11-25T10:53:58.258Z\n" +"POT-Creation-Date: 2020-11-25T12:25:27.475Z\n" +"PO-Revision-Date: 2020-11-25T12:25:27.475Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/domain/package-import/entities/PackageImportRule.ts b/src/domain/package-import/entities/PackageImportRule.ts index 3be4f4911..0785b7232 100644 --- a/src/domain/package-import/entities/PackageImportRule.ts +++ b/src/domain/package-import/entities/PackageImportRule.ts @@ -1,24 +1,29 @@ import { ModelValidation, validateModel, ValidationError } from "../../common/entities/Validations"; +import { DataSourceMapping } from "../../mapping/entities/DataSourceMapping"; import { PackageSource } from "./PackageSource"; interface PackageImportRuleData { source: PackageSource; packageIds: string[]; + temporalPackageMappings: DataSourceMapping[]; } export class PackageImportRule { public readonly source: PackageSource; public readonly packageIds: string[]; + public readonly temporalPackageMappings: DataSourceMapping[]; constructor(private data: PackageImportRuleData) { this.source = data.source; this.packageIds = data.packageIds; + this.temporalPackageMappings = data.temporalPackageMappings; } static create(source: PackageSource, selectedPackagesId?: string[]): PackageImportRule { return new PackageImportRule({ source, packageIds: selectedPackagesId || [], + temporalPackageMappings: [], }); } @@ -30,6 +35,31 @@ export class PackageImportRule { return new PackageImportRule({ ...this.data, packageIds }); } + public addOrUpdateTemporalPackageMapping( + temporalPackageMapping: DataSourceMapping + ): PackageImportRule { + if ( + this.temporalPackageMappings.find( + mappingTemp => mappingTemp.id === temporalPackageMapping.id + ) + ) { + return new PackageImportRule({ + ...this.data, + temporalPackageMappings: this.data.temporalPackageMappings.map(existed => + existed.id === temporalPackageMapping.id ? temporalPackageMapping : existed + ), + }); + } else { + return new PackageImportRule({ + ...this.data, + temporalPackageMappings: [ + ...this.data.temporalPackageMappings, + temporalPackageMapping, + ], + }); + } + } + public validate(filter?: string[]): ValidationError[] { return validateModel(this, this.validations()).filter( ({ property }) => filter?.includes(property) ?? true diff --git a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx index 3926e02ea..397d1a3bb 100644 --- a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx +++ b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx @@ -118,11 +118,17 @@ const PackageImportDialog: React.FC = ({ storePackageUrls[originPackage.id] = packageId; } - const mapping = await compositionRoot.mapping.get({ - type: isInstance(packageImportRule.source) ? "instance" : "store", - id: packageImportRule.source.id, - moduleId: originPackage.module.id, - }); + const temporalPackageMapping = packageImportRule.temporalPackageMappings.find( + mappingTemp => mappingTemp.owner.id === packageId + ); + + const mapping = temporalPackageMapping + ? temporalPackageMapping + : await compositionRoot.mapping.get({ + type: isInstance(packageImportRule.source) ? "instance" : "store", + id: packageImportRule.source.id, + moduleId: originPackage.module.id, + }); const originInstance = isInstance(packageImportRule.source) ? await compositionRoot.instances.getById(packageImportRule.source.id) diff --git a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx b/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx index 410cb1eb4..2c0e35e05 100644 --- a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx +++ b/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx @@ -49,7 +49,10 @@ const models = [ OrganisationUnitMappedModel, ]; -export const PackageMappingStep: React.FC = ({ packageImportRule }) => { +export const PackageMappingStep: React.FC = ({ + packageImportRule, + onChange, +}) => { const classes = useStyles(); const { compositionRoot, api } = useAppContext(); const snackbar = useSnackbar(); @@ -82,9 +85,11 @@ export const PackageMappingStep: React.FC = ({ package setDataSourceMapping(newMapping); }, }); + } else { + onChange(packageImportRule.addOrUpdateTemporalPackageMapping(newMapping)); } }, - [compositionRoot, dataSourceMapping, snackbar] + [compositionRoot, dataSourceMapping, snackbar, packageImportRule, onChange] ); const onApplyGlobalMapping = useCallback( @@ -122,17 +127,23 @@ export const PackageMappingStep: React.FC = ({ package error: async () => { snackbar.error(i18n.t("Unknown error happened loading package")); }, - success: async ({ dhisVersion, module, contents, id }) => { + success: async ({ dhisVersion, module, contents }) => { setPackageContents(contents); const fullModule = await compositionRoot.modules.get(module.id, source); if (fullModule) { if (fullModule.autogenerated) { - const temporalMapping = DataSourceMapping.build({ - owner: { type: "package" as const, id }, - mappingDictionary: {}, - }); + const savedTemporalMapping = packageImportRule.temporalPackageMappings.find( + mappingTemp => mappingTemp.owner.id === packageId + ); + + const temporalMapping = savedTemporalMapping + ? savedTemporalMapping + : DataSourceMapping.build({ + owner: { type: "package" as const, id: packageId }, + mappingDictionary: {}, + }); setDataSourceMapping(temporalMapping); setInstance(JSONDataSource.build(dhisVersion, contents)); @@ -172,7 +183,7 @@ export const PackageMappingStep: React.FC = ({ package }); } }, - [compositionRoot, snackbar] + [compositionRoot, snackbar, packageImportRule] ); useEffect(() => { From 76c0d6b6f847b09315d9cb904a3aa700bbdb358a Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 20:40:07 +0100 Subject: [PATCH 20/32] Allow building multiple front-ends --- README.md | 14 +- package.json | 12 +- scripts/run.ts | 150 ++++++++++++++++++ scripts/widget.ts | 122 -------------- src/index.js | 15 +- src/presentation/PresentationLoader.tsx | 20 +++ .../webapp/{WebApp.jsx => WebApp.tsx} | 58 +++++-- src/presentation/webapp/pages/Root.jsx | 3 + src/presentation/widget/pages/Root.tsx | 2 +- src/types/utils.ts | 6 + 10 files changed, 242 insertions(+), 160 deletions(-) create mode 100644 scripts/run.ts delete mode 100644 scripts/widget.ts create mode 100644 src/presentation/PresentationLoader.tsx rename src/presentation/webapp/{WebApp.jsx => WebApp.tsx} (75%) diff --git a/README.md b/README.md index 13165ba9a..dc3ec2bf9 100644 --- a/README.md +++ b/README.md @@ -47,13 +47,13 @@ $ yarn start This will open the development server for the main application at port 8081 and will connect to DHIS 2 instance http://localhost:8080. -### Start the development server of a given widget: +### Customization of the development server: ``` -$ yarn start-widget -p 8082 modules-list|package-exporter +$ yarn start -p 8082 modules-list|package-exporter ``` -This will open the development server for the given widget at port 8082 and will connect to DHIS 2 instance http://localhost:8080. +This will open the development server for the given front-end at port 8082 and will connect to DHIS 2 instance http://localhost:8080. ### Customize DHIS2 instance url @@ -91,16 +91,16 @@ Note tests only pass on the testing docker instance eyeseetea/dhis2-data:2.30-da ## Build -To build the web app: +To build all the front-ends: ``` -$ yarn build-webapp +$ yarn build ``` -To build the widget: +To build a given front-end: ``` -$ yarn build-widget modules-list|package-exporter +$ yarn build [all|core-app|modules-list|package-exporter] ``` To build the scheduler: diff --git a/package.json b/package.json index 57abb70eb..fe79d5b68 100644 --- a/package.json +++ b/package.json @@ -53,13 +53,10 @@ "scripts": { "clean": "npx rimraf build/ dist/", "prestart": "yarn localize && d2-manifest package.json manifest.webapp", - "start": "react-scripts start", - "start-widget": "yarn run-ts scripts/widget.ts start-server -p ${PORT:-8082}", + "start": "yarn run-ts scripts/run.ts start-server -p ${PORT:-8081}", "start-scheduler": "yarn run-ts --files src/scheduler/cli.ts", - "prebuild": "rm -rf build/ && yarn localize && yarn test", - "build": "react-scripts build && cp -r i18n icon.png build", - "build-webapp": "yarn build && yarn manifest && rm -f $npm_package_name.zip && cd build && zip -r ../$npm_package_name.zip *", - "build-widget": "yarn run-ts scripts/widget.ts build $npm_package_name", + "prebuild": "yarn clean && yarn localize && yarn test", + "build": "yarn run-ts scripts/run.ts build", "build-scheduler": "ncc build src/scheduler/cli.ts -m && cp dist/index.js $npm_package_name-server.js", "run-ts": "ts-node -O '{\"module\":\"commonjs\"}'", "test": "jest --env=jsdom-fourteen --passWithNoTests", @@ -70,7 +67,6 @@ "localize": "yarn update-po && d2-i18n-generate -n metadata-synchronization -p ./i18n/ -o ./src/locales/", "update-po": "yarn extract-pot && for pofile in i18n/*.po; do msgmerge --backup=off -U $pofile i18n/en.pot; done", "migrate": "yarn run-ts src/migrations/cli.ts", - "manifest": "d2-manifest package.json build/manifest.webapp", "pre-push": "yarn prettify && yarn lint && yarn localize && yarn jest", "cy:verify": "cypress verify", "cy:e2e:open": "CYPRESS_E2E=true cypress open", @@ -136,7 +132,7 @@ }, "manifest.webapp": { "name": "MetaData Synchronization", - "description": "Advanced metadata synchronization utility", + "description": "Advanced metadata & data synchronization utility", "icons": { "48": "icon.png" }, diff --git a/scripts/run.ts b/scripts/run.ts new file mode 100644 index 000000000..1c943bf6d --- /dev/null +++ b/scripts/run.ts @@ -0,0 +1,150 @@ +import { execSync } from "child_process"; +import yargs, { Argv } from "yargs"; +import { ArrayElementType } from "../src/types/utils"; + +const defaultVariant = "core-app"; +const variants = [ + { + type: "app", + name: "core-app", + title: "MetaData Synchronization", + file: "metadata-synchronization", + }, + { + type: "widget", + name: "modules-list", + title: "MetaData Synchronization Modules List Widget", + file: "metadata-synchronization-widget-modules-list", + }, + { + type: "widget", + name: "package-exporter", + title: "MetaData Synchronization Package Exporter Widget", + file: "metadata-synchronization-widget-package-exporter", + }, +] as const; + +function getYargs(): Argv { + yargs + .usage("Usage: $0 [options]") + .parserConfiguration({ "duplicate-arguments-array": false }) + .help("h") + .alias("h", "help") + .demandCommand() + .strict(); + + yargs.option("verbose", { + alias: "v", + type: "boolean", + description: "Run with verbose logging", + }); + + yargs.command( + "build [variant]", + "Build a variant", + yargs => { + yargs.positional("variant", { + choices: ["all", ...variants.map(w => w.name)], + default: "all", + }); + }, + (argv: BuildArgs) => { + build(argv); + } + ); + + yargs.command( + "start-server [variant]", + "start the development server", + yargs => { + yargs + .positional("variant", { + choices: variants.map(w => w.name), + default: defaultVariant, + }) + .option("port", { + alias: "p", + describe: "port to bind on", + default: process.env.PORT || "8082", + }); + }, + (argv: StartServerArgs) => { + startServer(argv); + } + ); + + return yargs; +} + +function main() { + getYargs().argv; +} + +function run(cmd: string): void { + console.debug(`Run: ${cmd}`); + execSync(cmd, { stdio: [0, 1, 2] }); +} + +/* Build */ + +type VariantKeys = ArrayElementType["name"]; +type BuildArgs = { variant: "all" | VariantKeys; verbose: boolean }; + +function build(args: BuildArgs): void { + const buildVariants = variants.filter( + variant => args.variant === "all" || variant.name === args.variant + ); + + if (buildVariants.length === 0) { + throw new Error(`Unknown variant: ${args.variant}`); + } + + for (const variant of buildVariants) { + Object.assign(process.env, { + REACT_APP_PRESENTATION_TYPE: variant.type, + REACT_APP_PRESENTATION_VARIANT: variant.name, + }); + + if (args.verbose) { + console.info(`Package name: ${variant.name}`); + } + + const fileName = `${variant.file}.zip`; + const manifestType = variant.type === "widget" ? "DASHBOARD_WIDGET" : "APP"; + + run(`react-scripts build && cp -r i18n icon.png build`); + run( + `d2-manifest package.json build/manifest.webapp -t ${manifestType} -n '${variant.title}'` + ); + run(`rm -f ${fileName}`); + run(`cd build && zip -r ../${fileName} *`); + console.info(`Written: ${fileName}`); + } +} + +/* Start server */ + +type StartServerArgs = { variant: string; port: number; verbose: boolean }; + +function startServer(args: StartServerArgs): void { + const variant = variants.find(variant => variant.name === args.variant); + + if (!variant) { + throw new Error(`Unknown variant: ${args.variant}`); + } + + if (args.verbose) { + console.info(`Variant: ${args.variant}`); + console.info(`Start server on: ${args.port}`); + } + + Object.assign(process.env, { + REACT_APP_PRESENTATION_TYPE: variant.type, + REACT_APP_PRESENTATION_VARIANT: variant.name, + PORT: args.port, + }); + + run("react-scripts start"); +} + +main(); diff --git a/scripts/widget.ts b/scripts/widget.ts deleted file mode 100644 index 19b029a71..000000000 --- a/scripts/widget.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { execSync } from "child_process"; -import yargs, { Argv } from "yargs"; - -type Widget = { name: string; description: string }; - -const widgets: Widget[] = [ - { - name: "modules-list", - description: "MetaData Synchronization Modules List Widget", - }, - { - name: "package-exporter", - description: "MetaData Synchronization Package Exporter Widget", - }, -]; - -function getYargs(): Argv { - yargs - .usage("Usage: $0 [options]") - .parserConfiguration({ "duplicate-arguments-array": false }) - .help("h") - .alias("h", "help") - .demandCommand() - .strict(); - - yargs.option("verbose", { - alias: "v", - type: "boolean", - description: "Run with verbose logging", - }); - - yargs.command( - "build ", - "Build a widget", - yargs => { - yargs - .positional("appName", { - describe: "Application name", - }) - .positional("widget", { - choices: widgets.map(w => w.name), - }); - }, - (argv: BuildArgs) => { - build(argv); - } - ); - - yargs.command( - "start-server ", - "start the development server for a widget", - yargs => { - yargs - .positional("widget", { - choices: widgets.map(w => w.name), - }) - .option("port", { - alias: "p", - describe: "port to bind on", - default: process.env.PORT || "8082", - }); - }, - (argv: StartServerArgs) => { - startServer(argv); - } - ); - - return yargs; -} - -function main() { - getYargs().argv; -} - -function run(cmd: string): void { - console.debug(`Run: ${cmd}`); - execSync(cmd, { stdio: [0, 1, 2] }); -} - -/* Build */ - -type BuildArgs = { appName: string; widget: string; verbose: boolean }; - -function build(args: BuildArgs): void { - const widget = widgets.find(w => w.name === args.widget); - if (!widget) throw new Error(`Unknown widget: ${args.widget}`); - - const packageName = [args.appName, "widget", widget.name].join("-"); - Object.assign(process.env, { - REACT_APP_DASHBOARD_WIDGET: args.widget, - }); - - if (args.verbose) { - console.info(`Widget: ${widget.name}`); - console.info(`Package name: ${packageName}`); - } - - run(`yarn build`); - run(`yarn manifest -t DASHBOARD_WIDGET -n '${widget.description}'`); - run(`rm -f ${packageName}`); - run(`cd build && zip -r ../${packageName} *`); - console.info(`Written: ${packageName}.zip`); -} - -/* Start server */ - -type StartServerArgs = { widget: string; port: number; verbose: boolean }; - -function startServer(args: StartServerArgs): void { - const widgetName = args.widget; - if (args.verbose) { - console.info(`Widget: ${widgetName}`); - console.info(`Start server on: ${args.port}`); - } - Object.assign(process.env, { - REACT_APP_DASHBOARD_WIDGET: widgetName, - PORT: args.port, - }); - run("yarn start"); -} - -main(); diff --git a/src/index.js b/src/index.js index 082be1ba8..df7967079 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import { D2Api } from "d2-api/2.30"; import _ from "lodash"; import React from "react"; import ReactDOM from "react-dom"; +import { PresentationLoader } from "./presentation/PresentationLoader"; async function getBaseUrl() { if (process.env.NODE_ENV === "development") { @@ -17,17 +18,6 @@ async function getBaseUrl() { } } -// Presentation layer is loaded with code-splitting for performance -async function getPresentation() { - if (process.env.REACT_APP_DASHBOARD_WIDGET) { - const { default: App } = await import("./presentation/widget/WidgetApp"); - return App; - } else { - const { default: App } = await import("./presentation/webapp/WebApp"); - return App; - } -} - const isLangRTL = code => { const langs = ["ar", "fa", "ur"]; const prefixed = langs.map(c => `${c}-`); @@ -63,10 +53,9 @@ async function main() { } try { - const App = await getPresentation(); ReactDOM.render( - + , document.getElementById("root") ); diff --git a/src/presentation/PresentationLoader.tsx b/src/presentation/PresentationLoader.tsx new file mode 100644 index 000000000..8b926df0c --- /dev/null +++ b/src/presentation/PresentationLoader.tsx @@ -0,0 +1,20 @@ +import React, { Suspense } from "react"; + +const App = React.lazy(() => { + switch (process.env.REACT_APP_PRESENTATION_TYPE) { + case "widget": { + return import("./widget/WidgetApp"); + } + default: { + return import("./webapp/WebApp"); + } + } +}); + +export const PresentationLoader: React.FC = () => { + return ( + + + + ); +}; diff --git a/src/presentation/webapp/WebApp.jsx b/src/presentation/webapp/WebApp.tsx similarity index 75% rename from src/presentation/webapp/WebApp.jsx rename to src/presentation/webapp/WebApp.tsx index be5278001..564f73822 100644 --- a/src/presentation/webapp/WebApp.jsx +++ b/src/presentation/webapp/WebApp.tsx @@ -1,10 +1,12 @@ import { useConfig } from "@dhis2/app-runtime"; +//@ts-ignore import { HeaderBar } from "@dhis2/ui-widgets"; import { MuiThemeProvider } from "@material-ui/core/styles"; import { createGenerateClassName, StylesProvider } from "@material-ui/styles"; import { init } from "d2"; import { LoadingProvider, SnackbarProvider } from "d2-ui-components"; import _ from "lodash"; +//@ts-ignore import OldMuiThemeProvider from "material-ui/styles/MuiThemeProvider"; import React, { useEffect, useState } from "react"; import { Instance } from "../../domain/instance/entities/Instance"; @@ -26,23 +28,62 @@ const generateClassName = createGenerateClassName({ productionPrefix: "c", }); -function initFeedbackTool(d2, appConfig) { - const appKey = _(appConfig).get("appKey"); +interface AppConfig { + appKey: string; + appearance: { + showShareButton: boolean; + }; + feedback: { + token: string[]; + createIssue: boolean; + sendToDhis2UserGroups: string[]; + issues: { + repository: string; + title: string; + body: string; + }; + snapshots: { + repository: string; + branch: string; + }; + feedbackOptions: {}; + }; +} +interface AppWindow extends Window { + $: { + feedbackDhis2: ( + d2: unknown, + appKey: string, + appConfig: AppConfig["feedback"]["feedbackOptions"] + ) => void; + }; +} + +function initFeedbackTool(d2: unknown, appConfig: AppConfig): void { + const appKey = _(appConfig).get("appKey"); if (appConfig && appConfig.feedback) { const feedbackOptions = { ...appConfig.feedback, i18nPath: "feedback-tool/i18n", }; - if (window.$) window.$.feedbackDhis2(d2, appKey, feedbackOptions); - else console.error("Could not initialize feedback tool"); + ((window as unknown) as AppWindow).$.feedbackDhis2(d2, appKey, feedbackOptions); } } +type MigrationState = + | { + type: "checking" | "checked"; + } + | { + type: "pending"; + runner: MigrationsRunner; + }; + const App = () => { const { baseUrl } = useConfig(); - const [appContext, setAppContext] = useState(null); - const [migrationsState, setMigrationsState] = useState({ type: "checking" }); + const [appContext, setAppContext] = useState(null); + const [migrationsState, setMigrationsState] = useState({ type: "checking" }); const [showShareButton, setShowShareButton] = useState(false); useEffect(() => { @@ -61,8 +102,7 @@ const App = () => { const compositionRoot = new CompositionRoot(instance, encryptionKey); - const appContext = { d2, api, compositionRoot }; - setAppContext(appContext); + setAppContext({ d2: d2 as object, api, compositionRoot }); Object.assign(window, { d2, api }); setShowShareButton(_(appConfig).get("appearance.showShareButton") || false); @@ -107,7 +147,7 @@ const App = () => { } else return null; }; -async function runMigrations(api) { +async function runMigrations(api: D2Api): Promise { const runner = await MigrationsRunner.init({ api, debug: debug }); if (runner.hasPendingMigrations()) { diff --git a/src/presentation/webapp/pages/Root.jsx b/src/presentation/webapp/pages/Root.jsx index 1b098b124..3e1608d03 100644 --- a/src/presentation/webapp/pages/Root.jsx +++ b/src/presentation/webapp/pages/Root.jsx @@ -23,6 +23,9 @@ import SyncRulesPage from "./sync-rules-list/SyncRulesListPage"; function Root() { const { api } = useAppContext(); + // TODO: Jorge from here on we can customize the front-end + console.log("Variant", process.env.REACT_APP_PRESENTATION_VARIANT); + return ( diff --git a/src/presentation/widget/pages/Root.tsx b/src/presentation/widget/pages/Root.tsx index 4c5d56dae..42a2b1361 100644 --- a/src/presentation/widget/pages/Root.tsx +++ b/src/presentation/widget/pages/Root.tsx @@ -4,7 +4,7 @@ import { HashRouter } from "react-router-dom"; import { Dictionary } from "../../../types/utils"; function useWidget(): { dashboardItemId: string; userOrgUnits: string[]; widget: string } { - const widget = process.env.REACT_APP_DASHBOARD_WIDGET; + const widget = process.env.REACT_APP_PRESENTATION_VARIANT; if (!widget) { throw new Error("Attempting to use useWidget on application"); } diff --git a/src/types/utils.ts b/src/types/utils.ts index e2c177e43..6fe55c02d 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -13,6 +13,12 @@ export type RequireAtLeastOne = Pick> & Partial>>; }[Keys]; +export type ArrayElementType> = T extends ReadonlyArray< + infer ElementType +> + ? ElementType + : never; + export function isValueInUnionType(value: S, values: readonly T[]): value is T { return (values as readonly S[]).indexOf(value) >= 0; } From 629c728eedcc743971df3d3200d09ac311f52e56 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 20:49:57 +0100 Subject: [PATCH 21/32] Move pre-actions to script --- package.json | 2 -- scripts/run.ts | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fe79d5b68..2907c94d9 100644 --- a/package.json +++ b/package.json @@ -52,10 +52,8 @@ }, "scripts": { "clean": "npx rimraf build/ dist/", - "prestart": "yarn localize && d2-manifest package.json manifest.webapp", "start": "yarn run-ts scripts/run.ts start-server -p ${PORT:-8081}", "start-scheduler": "yarn run-ts --files src/scheduler/cli.ts", - "prebuild": "yarn clean && yarn localize && yarn test", "build": "yarn run-ts scripts/run.ts build", "build-scheduler": "ncc build src/scheduler/cli.ts -m && cp dist/index.js $npm_package_name-server.js", "run-ts": "ts-node -O '{\"module\":\"commonjs\"}'", diff --git a/scripts/run.ts b/scripts/run.ts index 1c943bf6d..709e803bc 100644 --- a/scripts/run.ts +++ b/scripts/run.ts @@ -112,6 +112,7 @@ function build(args: BuildArgs): void { const fileName = `${variant.file}.zip`; const manifestType = variant.type === "widget" ? "DASHBOARD_WIDGET" : "APP"; + run(`yarn clean && yarn localize && yarn test`); run(`react-scripts build && cp -r i18n icon.png build`); run( `d2-manifest package.json build/manifest.webapp -t ${manifestType} -n '${variant.title}'` @@ -144,6 +145,7 @@ function startServer(args: StartServerArgs): void { PORT: args.port, }); + run("yarn localize && d2-manifest package.json manifest.webapp"); run("react-scripts start"); } From dba3b0e76d26a951c2c45c66727965750b9055b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 26 Nov 2020 12:02:17 +0100 Subject: [PATCH 22/32] Create new variants --- scripts/run.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/run.ts b/scripts/run.ts index 709e803bc..9d4df673f 100644 --- a/scripts/run.ts +++ b/scripts/run.ts @@ -10,6 +10,18 @@ const variants = [ title: "MetaData Synchronization", file: "metadata-synchronization", }, + { + type: "app", + name: "data-metadata-app", + title: "Data/Metadata Exchange", + file: "data-metadata-exchange", + }, + { + type: "app", + name: "module-package-app", + title: "Module/Package Generation", + file: "module-package-generation", + }, { type: "widget", name: "modules-list", From 565749deaca7234086c1c3013645061721edcee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 26 Nov 2020 12:31:18 +0100 Subject: [PATCH 23/32] Customize the home page by variant --- src/presentation/webapp/pages/Root.jsx | 3 --- .../webapp/pages/home/HomePage.tsx | 25 +++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/presentation/webapp/pages/Root.jsx b/src/presentation/webapp/pages/Root.jsx index 3e1608d03..1b098b124 100644 --- a/src/presentation/webapp/pages/Root.jsx +++ b/src/presentation/webapp/pages/Root.jsx @@ -23,9 +23,6 @@ import SyncRulesPage from "./sync-rules-list/SyncRulesListPage"; function Root() { const { api } = useAppContext(); - // TODO: Jorge from here on we can customize the front-end - console.log("Variant", process.env.REACT_APP_PRESENTATION_VARIANT); - return ( diff --git a/src/presentation/webapp/pages/home/HomePage.tsx b/src/presentation/webapp/pages/home/HomePage.tsx index 9effac205..84f896d30 100644 --- a/src/presentation/webapp/pages/home/HomePage.tsx +++ b/src/presentation/webapp/pages/home/HomePage.tsx @@ -12,7 +12,24 @@ import { useAppContext } from "../../../react/contexts/AppContext"; import { Card, Landing } from "../../../react/components/landing/Landing"; import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +export type AppVariant = "core-app" | "data-metadata-app" | "module-package-app"; + +const appVariantConfiguration: Record = { + "core-app": [ + "aggregated", + "events", + "metadata", + "other", + "metadata-distribution", + "configuration", + ], + "data-metadata-app": ["aggregated", "events", "metadata", "other", "configuration"], + "module-package-app": ["metadata-distribution", "configuration"], +}; + const LandingPage: React.FC = () => { + const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; + const { api, compositionRoot } = useAppContext(); const history = useHistory(); @@ -31,7 +48,7 @@ const LandingPage: React.FC = () => { }); }, [api, compositionRoot]); - const cards: Card[] = useMemo( + const allCards: Card[] = useMemo( () => [ { title: i18n.t("Aggregated Data Sync"), @@ -212,7 +229,11 @@ const LandingPage: React.FC = () => { return ( - + + appVariantConfiguration[appVariant].includes(card.key) + )} + /> ); }; From abcfc489c3d250e3fdb20afc59bd47cf1c184ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 26 Nov 2020 12:31:35 +0100 Subject: [PATCH 24/32] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dc3ec2bf9..9a0443daf 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ This will open the development server for the main application at port 8081 and ### Customization of the development server: ``` -$ yarn start -p 8082 modules-list|package-exporter +$ yarn start -p 8082 core-app|data-metadata-app|module-package-app|modules-list|package-exporter ``` This will open the development server for the given front-end at port 8082 and will connect to DHIS 2 instance http://localhost:8080. @@ -100,7 +100,7 @@ $ yarn build To build a given front-end: ``` -$ yarn build [all|core-app|modules-list|package-exporter] +$ yarn build [all|core-app|data-metadata-app|module-package-app|modules-list|package-exporter] ``` To build the scheduler: From 517e31682d4ba81cf94e9e2faac0290767302f0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 26 Nov 2020 13:06:55 +0100 Subject: [PATCH 25/32] Update app name in AppBar by variant --- i18n/en.pot | 7 ++----- i18n/es.po | 5 +---- i18n/fr.po | 5 +---- i18n/pt.po | 5 +---- scripts/run.ts | 6 ++++-- src/presentation/webapp/WebApp.tsx | 5 +++-- 6 files changed, 12 insertions(+), 21 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a93693fad..600c157bc 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: 2020-11-20T09:27:57.833Z\n" -"PO-Revision-Date: 2020-11-20T09:27:57.833Z\n" +"POT-Creation-Date: 2020-11-26T11:58:52.696Z\n" +"PO-Revision-Date: 2020-11-26T11:58:52.696Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1150,9 +1150,6 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" -msgid "MetaData Synchronization" -msgstr "" - msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6ce42123a..e6046acd8 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1156,9 +1156,6 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" -msgid "MetaData Synchronization" -msgstr "" - msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 153603391..11a05f3ca 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1153,9 +1153,6 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" -msgid "MetaData Synchronization" -msgstr "" - msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 153603391..11a05f3ca 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-20T07:58:40.203Z\n" +"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1153,9 +1153,6 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" -msgid "MetaData Synchronization" -msgstr "" - msgid "Metadata Synchronization History" msgstr "" diff --git a/scripts/run.ts b/scripts/run.ts index 9d4df673f..c11069114 100644 --- a/scripts/run.ts +++ b/scripts/run.ts @@ -14,13 +14,13 @@ const variants = [ type: "app", name: "data-metadata-app", title: "Data/Metadata Exchange", - file: "data-metadata-exchange", + file: "metadata-synchronization-data-metadata-exchange", }, { type: "app", name: "module-package-app", title: "Module/Package Generation", - file: "module-package-generation", + file: "metadata-synchronization-module-package-generation", }, { type: "widget", @@ -115,6 +115,7 @@ function build(args: BuildArgs): void { Object.assign(process.env, { REACT_APP_PRESENTATION_TYPE: variant.type, REACT_APP_PRESENTATION_VARIANT: variant.name, + REACT_APP_PRESENTATION_TITLE: variant.title, }); if (args.verbose) { @@ -154,6 +155,7 @@ function startServer(args: StartServerArgs): void { Object.assign(process.env, { REACT_APP_PRESENTATION_TYPE: variant.type, REACT_APP_PRESENTATION_VARIANT: variant.name, + REACT_APP_PRESENTATION_TITLE: variant.title, PORT: args.port, }); diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index 564f73822..ef4de524b 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -10,7 +10,6 @@ import _ from "lodash"; import OldMuiThemeProvider from "material-ui/styles/MuiThemeProvider"; import React, { useEffect, useState } from "react"; import { Instance } from "../../domain/instance/entities/Instance"; -import i18n from "../../locales"; import { MigrationsRunner } from "../../migrations"; import { D2Api } from "../../types/d2-api"; import { debug } from "../../utils/debug"; @@ -86,6 +85,8 @@ const App = () => { const [migrationsState, setMigrationsState] = useState({ type: "checking" }); const [showShareButton, setShowShareButton] = useState(false); + const appTitle = process.env.REACT_APP_PRESENTATION_TITLE; + useEffect(() => { const run = async () => { const appConfig = await fetch("app-config.json", { @@ -129,7 +130,7 @@ const App = () => { - +
    From 22d45f39962113cf150490e6ef6687c332c567eb Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Fri, 27 Nov 2020 06:34:14 +0000 Subject: [PATCH 26/32] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cdfa83ab3..a9d388be6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "metadata-synchronization", "description": "Advanced metadata & data synchronization utility", - "version": "2.4.0", + "version": "2.5.0", "license": "GPL-3.0", "author": "EyeSeeTea team", "homepage": ".", From f1b9781e9a930c69b66dfa503bec6d83fb14efdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 27 Nov 2020 10:55:49 +0100 Subject: [PATCH 27/32] Show in summary the api error message to sync documents --- src/data/file/FileD2Repository.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/data/file/FileD2Repository.ts b/src/data/file/FileD2Repository.ts index c1017139b..a9ada357b 100644 --- a/src/data/file/FileD2Repository.ts +++ b/src/data/file/FileD2Repository.ts @@ -69,8 +69,12 @@ export class FileD2Repository implements FileRepository { fetchOptions ); if (!response.ok) { + const responseBody = JSON.parse(await response.text()); + + const bodyError = responseBody.message ? `: ${responseBody.message}` : ""; + throw Error( - `An error has ocurred saving the resource file of the document '${file.name}' in ${this.instance.name}` + `An error has ocurred saving the resource file of the document '${file.name}' in ${this.instance.name}${bodyError}` ); } else { const apiResponse: SaveApiResponse = JSON.parse(await response.text()); From 555babbfbf5e3abd675c40da3cfff2d679b268f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 27 Nov 2020 10:56:37 +0100 Subject: [PATCH 28/32] Apply changes in package status when is local instance --- .../package-list-table/PackageListTable.tsx | 105 +++++++++++++----- .../package-list-table/PackageModuleItem.ts | 7 +- 2 files changed, 83 insertions(+), 29 deletions(-) diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index 88b370e4b..c13042268 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -417,7 +417,7 @@ export const PackagesListTable: React.FC = ({ ] ); - const getInstallStatusText = (installStatus: InstallStatus) => { + const getInstallStatusText = (installStatus: InstallStatus): string => { switch (installStatus) { case "Installed": return i18n.t("Installed"); @@ -425,6 +425,10 @@ export const PackagesListTable: React.FC = ({ return i18n.t("Not Installed"); case "Upgrade": return i18n.t("Upgrade Available"); + case "InstalledLocalPackage": + return i18n.t("Local Package (Installed)"); + case "NotInstalledLocalPackage": + return i18n.t("Local Package (Not Installed)"); } }; @@ -731,7 +735,9 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages .list(globalAdmin, remoteInstance) .then(packages => { - setInstancePackages(mapPackagesToPackageItems(packages, importedPackages)); + setInstancePackages( + mapPackagesToPackageItems(modules, packages, importedPackages, packageSource) + ); }) .catch((error: Error) => { snackbar.error(error.message); @@ -745,6 +751,8 @@ export const PackagesListTable: React.FC = ({ globalAdmin, importedPackages, remoteStore, + modules, + packageSource, ]); useEffect(() => { @@ -753,7 +761,14 @@ export const PackagesListTable: React.FC = ({ compositionRoot.packages.listStore(remoteStore.id).then(validation => { validation.match({ success: packages => { - setStorePackages(mapPackagesToPackageItems(packages, importedPackages)); + setStorePackages( + mapPackagesToPackageItems( + modules, + packages, + importedPackages, + packageSource + ) + ); }, error: () => { snackbar.error(i18n.t("Can't connect to store")); @@ -764,7 +779,16 @@ export const PackagesListTable: React.FC = ({ } else { setStorePackages([]); } - }, [compositionRoot, snackbar, remoteStore, importedPackages, remoteInstance, resetKey]); + }, [ + compositionRoot, + snackbar, + remoteStore, + importedPackages, + remoteInstance, + resetKey, + modules, + packageSource, + ]); useEffect(() => { compositionRoot.importedPackages.list().then(result => @@ -842,39 +866,64 @@ export const PackagesListTable: React.FC = ({ }; function mapPackagesToPackageItems( + modules: Module[], packages: ListPackage[], - importedPackages: ImportedPackage[] + importedPackages: ImportedPackage[], + packageSource?: PackageSource ): PackageItem[] { - const listPackages = packages.map(pkg => { - const installed = importedPackages.some(imported => { - return ( + const verifyIfPackageIsImported = (pkg: ListPackage) => { + return importedPackages.some( + imported => imported.module.id === pkg.module.id && imported.version === pkg.version && imported.dhisVersion === pkg.dhisVersion - ); - }); + ); + }; + + if (packageSource) { + const listPackages = packages.map(pkg => { + const installed = verifyIfPackageIsImported(pkg); + + const newUpdates = importedPackages.some(imported => { + const importedVersion = semver.parse(imported.version); + const packageVersion = semver.parse(pkg.version); + + return ( + imported.module.id === pkg.module.id && + importedVersion && + packageVersion && + imported.dhisVersion === pkg.dhisVersion && + importedVersion < packageVersion + ); + }); - const newUpdates = importedPackages.some(imported => { - const importedVersion = semver.parse(imported.version); - const packageVersion = semver.parse(pkg.version); + const installStatus: InstallStatus = installed + ? "Installed" + : newUpdates + ? "Upgrade" + : "NotInstalled"; - return ( - imported.module.id === pkg.module.id && - importedVersion && - packageVersion && - imported.dhisVersion === pkg.dhisVersion && - importedVersion < packageVersion - ); + return { ...pkg, installStatus }; }); - const installStatus: InstallStatus = installed - ? "Installed" - : newUpdates - ? "Upgrade" - : "NotInstalled"; + return listPackages; + } else { + const listPackages = packages.map(pkg => { + const isPackageImported = verifyIfPackageIsImported(pkg); - return { ...pkg, installStatus }; - }); + const module = modules.find(module => module.id === pkg.module.id); + + const isPackageFromFile = module && module.autogenerated; + + const installed = !isPackageFromFile || (isPackageFromFile && isPackageImported); + + const installStatus: InstallStatus = installed + ? "InstalledLocalPackage" + : "NotInstalledLocalPackage"; + + return { ...pkg, installStatus }; + }); - return listPackages; + return listPackages; + } } diff --git a/src/presentation/react/components/package-list-table/PackageModuleItem.ts b/src/presentation/react/components/package-list-table/PackageModuleItem.ts index fff291b89..d5864086e 100644 --- a/src/presentation/react/components/package-list-table/PackageModuleItem.ts +++ b/src/presentation/react/components/package-list-table/PackageModuleItem.ts @@ -10,7 +10,12 @@ export interface ModuleItem { packages: PackageItem[]; } -export type InstallStatus = "Installed" | "NotInstalled" | "Upgrade"; +export type InstallStatus = + | "Installed" + | "NotInstalled" + | "Upgrade" + | "InstalledLocalPackage" + | "NotInstalledLocalPackage"; export type PackageItem = Omit & { installStatus: InstallStatus }; export const isPackageItem = (item: PackageModuleItem): item is PackageItem => { From 3f18c8db529d2f4486089c9428b80c5b24429289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 27 Nov 2020 11:15:55 +0100 Subject: [PATCH 29/32] Update locale with new installed texts --- i18n/en.pot | 10 ++++++++-- i18n/es.po | 8 +++++++- i18n/fr.po | 8 +++++++- i18n/pt.po | 8 +++++++- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 29984fbcb..0b0e174d9 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: 2020-11-26T11:58:52.696Z\n" -"PO-Revision-Date: 2020-11-26T11:58:52.696Z\n" +"POT-Creation-Date: 2020-11-27T10:12:16.920Z\n" +"PO-Revision-Date: 2020-11-27T10:12:16.920Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -668,6 +668,12 @@ msgstr "" msgid "Upgrade Available" msgstr "" +msgid "Local Package (Installed)" +msgstr "" + +msgid "Local Package (Not Installed)" +msgstr "" + msgid "Version" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index ae9dd06da..87034b8da 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" +"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -672,6 +672,12 @@ msgstr "" msgid "Upgrade Available" msgstr "" +msgid "Local Package (Installed)" +msgstr "" + +msgid "Local Package (Not Installed)" +msgstr "" + msgid "Version" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 46316fe27..d25c465bc 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" +"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -669,6 +669,12 @@ msgstr "" msgid "Upgrade Available" msgstr "" +msgid "Local Package (Installed)" +msgstr "" + +msgid "Local Package (Not Installed)" +msgstr "" + msgid "Version" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 46316fe27..d25c465bc 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-26T11:47:30.422Z\n" +"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -669,6 +669,12 @@ msgstr "" msgid "Upgrade Available" msgstr "" +msgid "Local Package (Installed)" +msgstr "" + +msgid "Local Package (Not Installed)" +msgstr "" + msgid "Version" msgstr "" From dba2f3073eaa94856aad390bf59bfd7615c7bf84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 27 Nov 2020 13:12:09 +0100 Subject: [PATCH 30/32] Solve bugs in history with package imports: - Show (Package importation) in the first column - Assign user and types --- i18n/en.pot | 7 +++++-- i18n/es.po | 6 +++++- i18n/fr.po | 5 ++++- i18n/pt.po | 5 ++++- .../entities/SynchronizationReport.ts | 1 + src/models/syncReport.ts | 14 +++++++++++-- .../PackageImportDialog.tsx | 21 ++++++++++++++----- .../package-list-table/PackageListTable.tsx | 17 ++++++++++----- .../webapp/pages/history/HistoryPage.tsx | 12 +++++------ 9 files changed, 65 insertions(+), 23 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0b0e174d9..dd7bb2a9e 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: 2020-11-27T10:12:16.920Z\n" -"PO-Revision-Date: 2020-11-27T10:12:16.920Z\n" +"POT-Creation-Date: 2020-11-27T12:10:01.803Z\n" +"PO-Revision-Date: 2020-11-27T12:10:01.803Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1204,6 +1204,9 @@ msgstr "" msgid "Sync Rule" msgstr "" +msgid "(package importation)" +msgstr "" + msgid "(manual synchronization)" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 87034b8da..b204e49e8 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" +"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1210,6 +1210,10 @@ msgstr "" msgid "Sync Rule" msgstr "" +#, fuzzy +msgid "(package importation)" +msgstr "Paquetes" + msgid "(manual synchronization)" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index d25c465bc..6f62d460d 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" +"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1207,6 +1207,9 @@ msgstr "" msgid "Sync Rule" msgstr "" +msgid "(package importation)" +msgstr "" + msgid "(manual synchronization)" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index d25c465bc..6f62d460d 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T09:52:57.304Z\n" +"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1207,6 +1207,9 @@ msgstr "" msgid "Sync Rule" msgstr "" +msgid "(package importation)" +msgstr "" + msgid "(manual synchronization)" msgstr "" diff --git a/src/domain/synchronization/entities/SynchronizationReport.ts b/src/domain/synchronization/entities/SynchronizationReport.ts index b3c1e1e48..39812bd80 100644 --- a/src/domain/synchronization/entities/SynchronizationReport.ts +++ b/src/domain/synchronization/entities/SynchronizationReport.ts @@ -7,6 +7,7 @@ export interface SynchronizationReport { status: SynchronizationReportStatus; types: string[]; syncRule?: string; + packageImport?: boolean; deletedSyncRuleLabel?: string; type: SynchronizationType; dataStats?: AggregatedDataStats[] | EventsDataStats[]; diff --git a/src/models/syncReport.ts b/src/models/syncReport.ts index a7eefe2b8..ad8ded1bc 100644 --- a/src/models/syncReport.ts +++ b/src/models/syncReport.ts @@ -43,16 +43,22 @@ export default class SyncReport { "deletedSyncRuleLabel", "type", "dataStats", + "packageImport", ]), }; } - public static create(type: SynchronizationType = "metadata"): SyncReport { + public static create( + type: SynchronizationType = "metadata", + user = "", + packageImport?: boolean + ): SyncReport { return new SyncReport({ - user: "", + user, status: "READY" as SynchronizationReportStatus, types: [], type, + packageImport, }); } @@ -112,6 +118,10 @@ export default class SyncReport { this.syncReport.status = status; } + public setTypes(types: string[]): void { + this.syncReport.types = types; + } + public addSyncResult(...result: SynchronizationResult[]): void { this.results = _.unionBy( [...result], diff --git a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx index 397d1a3bb..f4c08047c 100644 --- a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx +++ b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx @@ -1,5 +1,6 @@ import DialogContent from "@material-ui/core/DialogContent"; import { ConfirmationDialog, useLoading, useSnackbar } from "d2-ui-components"; +import _ from "lodash"; import React, { useEffect, useState } from "react"; import { Either } from "../../../../domain/common/entities/Either"; import { NamedRef } from "../../../../domain/common/entities/Ref"; @@ -94,14 +95,20 @@ const PackageImportDialog: React.FC = ({ // 3 - Save Result (using ResultRepository) // 4 - Save ImportedPackage (using ImportedPackageRepository) const importedPackages: Package[] = []; - const report = SyncReport.create("metadata"); + + const currentUser = await api.currentUser + .get({ fields: { id: true, userCredentials: { username: true } } }) + .getData(); + + const report = SyncReport.create( + "metadata", + currentUser.userCredentials.username ?? "Unknown", + true + ); + const storePackageUrls: Record = {}; try { - const currentUser = await api.currentUser - .get({ fields: { id: true, userCredentials: { username: true } } }) - .getData(); - const author = { id: currentUser.id, name: currentUser.userCredentials.username }; const executePackageImport = async (packageId: string) => { @@ -144,6 +151,10 @@ const PackageImportDialog: React.FC = ({ originDataSource ); + report.setTypes( + _.uniq([...report.syncReport.types, ..._.keys(originPackage.contents)]) + ); + report.setStatus( result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index c13042268..71807d92b 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -335,6 +335,10 @@ export const PackagesListTable: React.FC = ({ result.match({ success: async originPackage => { try { + const currentUser = await api.currentUser + .get({ fields: { id: true, userCredentials: { username: true } } }) + .getData(); + loading.show( true, i18n.t("Importing package {{name}}", { name: originPackage.name }) @@ -360,7 +364,14 @@ export const PackagesListTable: React.FC = ({ originDataSource ); - const report = SyncReport.create("metadata"); + const report = SyncReport.create( + "metadata", + currentUser.userCredentials.username ?? "Unknown", + true + ); + + report.setTypes(_.keys(originPackage.contents)); + report.setStatus( result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" @@ -375,10 +386,6 @@ export const PackagesListTable: React.FC = ({ await report.save(api); if (result.status === "SUCCESS") { - const currentUser = await api.currentUser - .get({ fields: { id: true, userCredentials: { username: true } } }) - .getData(); - const author = { id: currentUser.id, name: currentUser.userCredentials.username, diff --git a/src/presentation/webapp/pages/history/HistoryPage.tsx b/src/presentation/webapp/pages/history/HistoryPage.tsx index 142c03b67..283d9ce5d 100644 --- a/src/presentation/webapp/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/pages/history/HistoryPage.tsx @@ -120,12 +120,12 @@ const HistoryPage: React.FC = () => { name: "syncRule", text: i18n.t("Sync Rule"), sortable: true, - getValue: ({ syncRule: id, deletedSyncRuleLabel }) => { - return ( - deletedSyncRuleLabel ?? - _.find(syncRules, { id })?.name ?? - i18n.t("(manual synchronization)") - ); + getValue: ({ syncRule: id, deletedSyncRuleLabel, packageImport }) => { + return packageImport + ? i18n.t("(package importation)") + : deletedSyncRuleLabel ?? + _.find(syncRules, { id })?.name ?? + i18n.t("(manual synchronization)"); }, }, { name: "date", text: i18n.t("Timestamp"), sortable: true }, From 5eb016ae48e19bb48d47023e4dd7969ff791dde7 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Mon, 30 Nov 2020 08:23:43 +0000 Subject: [PATCH 31/32] term --- i18n/en.pot | 6 +++--- i18n/es.po | 4 ++-- i18n/fr.po | 4 ++-- i18n/pt.po | 4 ++-- src/presentation/webapp/pages/history/HistoryPage.tsx | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index dd7bb2a9e..1effabb51 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: 2020-11-27T12:10:01.803Z\n" -"PO-Revision-Date: 2020-11-27T12:10:01.803Z\n" +"POT-Creation-Date: 2020-11-30T08:20:56.222Z\n" +"PO-Revision-Date: 2020-11-30T08:20:56.222Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1204,7 +1204,7 @@ msgstr "" msgid "Sync Rule" msgstr "" -msgid "(package importation)" +msgid "(package import)" msgstr "" msgid "(manual synchronization)" diff --git a/i18n/es.po b/i18n/es.po index b204e49e8..f8d34f536 100644 --- a/i18n/es.po +++ b/i18n/es.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" +"POT-Creation-Date: 2020-11-30T08:20:56.222Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1211,7 +1211,7 @@ msgid "Sync Rule" msgstr "" #, fuzzy -msgid "(package importation)" +msgid "(package import)" msgstr "Paquetes" msgid "(manual synchronization)" diff --git a/i18n/fr.po b/i18n/fr.po index 6f62d460d..de1e56d69 100644 --- a/i18n/fr.po +++ b/i18n/fr.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" +"POT-Creation-Date: 2020-11-30T08:20:56.222Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1207,7 +1207,7 @@ msgstr "" msgid "Sync Rule" msgstr "" -msgid "(package importation)" +msgid "(package import)" msgstr "" msgid "(manual synchronization)" diff --git a/i18n/pt.po b/i18n/pt.po index 6f62d460d..de1e56d69 100644 --- a/i18n/pt.po +++ b/i18n/pt.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Project-Id-Version: i18next-conv\n" -"POT-Creation-Date: 2020-11-27T12:08:32.836Z\n" +"POT-Creation-Date: 2020-11-30T08:20:56.222Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1207,7 +1207,7 @@ msgstr "" msgid "Sync Rule" msgstr "" -msgid "(package importation)" +msgid "(package import)" msgstr "" msgid "(manual synchronization)" diff --git a/src/presentation/webapp/pages/history/HistoryPage.tsx b/src/presentation/webapp/pages/history/HistoryPage.tsx index 283d9ce5d..e4241a5a6 100644 --- a/src/presentation/webapp/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/pages/history/HistoryPage.tsx @@ -122,7 +122,7 @@ const HistoryPage: React.FC = () => { sortable: true, getValue: ({ syncRule: id, deletedSyncRuleLabel, packageImport }) => { return packageImport - ? i18n.t("(package importation)") + ? i18n.t("(package import)") : deletedSyncRuleLabel ?? _.find(syncRules, { id })?.name ?? i18n.t("(manual synchronization)"); From 1bfbef61601dd7c4ee1b12f651b4dbac784aedbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 30 Nov 2020 09:55:48 +0100 Subject: [PATCH 32/32] Ignore package key --- .../react/components/metadata-drop-zone/MetadataDropZone.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx b/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx index 18f169949..7046d9599 100644 --- a/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx +++ b/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx @@ -26,6 +26,7 @@ const MetadataDropZone: React.FC = ({ onChange }) => { const contentsFile = await file.text(); const contentsJson = JSON.parse(contentsFile); delete contentsJson.date; + delete contentsJson.package; onChange(file.name, contentsJson as MetadataPackage); setFile(file);