From fb0c72e6e4c37edc446d0439d8ed2b0ceda8dfe5 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Nov 2020 08:26:25 +0100 Subject: [PATCH 001/163] Refactor folder structure --- src/data/packages/GitHubOctokitRepository.ts | 4 ++-- ...stantRepository.ts => StorageConstantClient.ts} | 0 ...toreRepository.ts => StorageDataStoreClient.ts} | 0 src/data/stores/StoreD2ApiRepository.ts | 5 +++++ .../__tests__/integration/helpers.ts | 2 +- .../package-import/entities/PackageSource.ts | 2 +- .../packages/repositories/GitHubRepository.ts | 4 ++-- src/domain/packages/usecases/DiffPackageUseCase.ts | 2 +- .../packages/usecases/DownloadPackageUseCase.ts | 2 +- .../packages/usecases/GetStorePackageUseCase.ts | 2 +- .../packages/usecases/ListStorePackagesUseCase.ts | 2 +- .../usecases/PublishStorePackageUseCase.ts | 2 +- src/domain/{packages => stores}/entities/Store.ts | 0 .../entities/StorePermissions.ts | 0 src/domain/stores/repositories/StoreRepository.ts | 3 +++ .../usecases/DeleteStoreUseCase.ts | 0 .../usecases/GetStoreUseCase.ts | 0 .../usecases/ListStoresUseCase.ts | 0 .../usecases/SaveStoreUseCase.ts | 4 ++-- .../usecases/SetStoreAsDefaultUseCase.ts | 0 .../usecases/ValidateStoreUseCase.ts | 2 +- .../entities/SynchronizationResult.ts | 2 +- src/migrations/tasks/05.multiple-stores.ts | 2 +- src/presentation/CompositionRoot.ts | 14 +++++++------- .../InstanceSelectionDropdown.tsx | 2 +- .../ModulePackageListTable.tsx | 2 +- .../steps/InstanceStoreSelectionStep.tsx | 2 +- .../package-list-table/PackageListTable.tsx | 2 +- .../packages-diff-dialog/PackagesDiffDialog.tsx | 2 +- .../store-creation/StoreCreationDialog.tsx | 2 +- .../react/components/sync-summary/SyncSummary.tsx | 2 +- .../sync-wizard/common/GeneralInfoStep.tsx | 2 +- .../webapp/pages/manual-sync/ManualSyncPage.tsx | 2 +- .../module-package-list/ModulePackageListPage.tsx | 2 +- .../responsibles-list/ResponsiblesListPage.tsx | 2 +- .../pages/store-creation/StoreCreationPage.tsx | 2 +- .../webapp/pages/store-list/StoreListPage.tsx | 2 +- 37 files changed, 44 insertions(+), 36 deletions(-) rename src/data/storage/{StorageConstantRepository.ts => StorageConstantClient.ts} (100%) rename src/data/storage/{StorageDataStoreRepository.ts => StorageDataStoreClient.ts} (100%) create mode 100644 src/data/stores/StoreD2ApiRepository.ts rename src/domain/{packages => stores}/entities/Store.ts (100%) rename src/domain/{packages => stores}/entities/StorePermissions.ts (100%) create mode 100644 src/domain/stores/repositories/StoreRepository.ts rename src/domain/{packages => stores}/usecases/DeleteStoreUseCase.ts (100%) rename src/domain/{packages => stores}/usecases/GetStoreUseCase.ts (100%) rename src/domain/{packages => stores}/usecases/ListStoresUseCase.ts (100%) rename src/domain/{packages => stores}/usecases/SaveStoreUseCase.ts (91%) rename src/domain/{packages => stores}/usecases/SetStoreAsDefaultUseCase.ts (100%) rename src/domain/{packages => stores}/usecases/ValidateStoreUseCase.ts (80%) diff --git a/src/data/packages/GitHubOctokitRepository.ts b/src/data/packages/GitHubOctokitRepository.ts index fbd67d05b..c00183b9e 100644 --- a/src/data/packages/GitHubOctokitRepository.ts +++ b/src/data/packages/GitHubOctokitRepository.ts @@ -4,8 +4,8 @@ import { Either } from "../../domain/common/entities/Either"; import { GitHubError, GitHubListError } from "../../domain/packages/entities/Errors"; import { GithubBranch } from "../../domain/packages/entities/GithubBranch"; import { GithubFile } from "../../domain/packages/entities/GithubFile"; -import { Store } from "../../domain/packages/entities/Store"; -import { StorePermissions } from "../../domain/packages/entities/StorePermissions"; +import { Store } from "../../domain/stores/entities/Store"; +import { StorePermissions } from "../../domain/stores/entities/StorePermissions"; import { GitHubRepository } from "../../domain/packages/repositories/GitHubRepository"; import { cache } from "../../utils/cache"; diff --git a/src/data/storage/StorageConstantRepository.ts b/src/data/storage/StorageConstantClient.ts similarity index 100% rename from src/data/storage/StorageConstantRepository.ts rename to src/data/storage/StorageConstantClient.ts diff --git a/src/data/storage/StorageDataStoreRepository.ts b/src/data/storage/StorageDataStoreClient.ts similarity index 100% rename from src/data/storage/StorageDataStoreRepository.ts rename to src/data/storage/StorageDataStoreClient.ts diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts new file mode 100644 index 000000000..f102ee884 --- /dev/null +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -0,0 +1,5 @@ +import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; + +export class StoreD2ApiRepository implements StoreRepository { + +} \ No newline at end of file diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 4621fb809..72b4d26b4 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -8,7 +8,7 @@ import { RepositoryFactory } from "../../../../domain/common/factories/Repositor import { Repositories } from "../../../../domain/Repositories"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; import { AnyRegistry } from "miragejs/-types"; diff --git a/src/domain/package-import/entities/PackageSource.ts b/src/domain/package-import/entities/PackageSource.ts index 952c27864..1ff18458f 100644 --- a/src/domain/package-import/entities/PackageSource.ts +++ b/src/domain/package-import/entities/PackageSource.ts @@ -1,5 +1,5 @@ import { Instance } from "../../instance/entities/Instance"; -import { Store } from "../../packages/entities/Store"; +import { Store } from "../../stores/entities/Store"; export type PackageSource = Instance | Store; diff --git a/src/domain/packages/repositories/GitHubRepository.ts b/src/domain/packages/repositories/GitHubRepository.ts index 61f726b7a..91237d77f 100644 --- a/src/domain/packages/repositories/GitHubRepository.ts +++ b/src/domain/packages/repositories/GitHubRepository.ts @@ -2,8 +2,8 @@ import { Either } from "../../common/entities/Either"; import { GitHubError, GitHubListError } from "../entities/Errors"; import { GithubBranch } from "../entities/GithubBranch"; import { GithubFile } from "../entities/GithubFile"; -import { Store } from "../entities/Store"; -import { StorePermissions } from "../entities/StorePermissions"; +import { Store } from "../../stores/entities/Store"; +import { StorePermissions } from "../../stores/entities/StorePermissions"; export interface GitHubRepositoryConstructor { new (): GitHubRepository; diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index 078b74260..e923e9b61 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -7,7 +7,7 @@ import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; import { getMetadataPackageDiff, MetadataPackageDiff } from "../entities/MetadataPackageDiff"; -import { Store } from "../entities/Store"; +import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; import { CompositionRoot } from "./../../../presentation/CompositionRoot"; import { Either } from "./../../common/entities/Either"; diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index 32a8f7544..ea9338823 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -10,7 +10,7 @@ import { Namespace } from "../../storage/Namespaces"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; import { BasePackage } from "../entities/Package"; -import { Store } from "../entities/Store"; +import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; export class DownloadPackageUseCase implements UseCase { diff --git a/src/domain/packages/usecases/GetStorePackageUseCase.ts b/src/domain/packages/usecases/GetStorePackageUseCase.ts index f6f526714..1732d9535 100644 --- a/src/domain/packages/usecases/GetStorePackageUseCase.ts +++ b/src/domain/packages/usecases/GetStorePackageUseCase.ts @@ -9,7 +9,7 @@ import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; import { BasePackage, Package } from "../entities/Package"; -import { Store } from "../entities/Store"; +import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; export class GetStorePackageUseCase implements UseCase { diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index 5274a4751..b80369938 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -14,7 +14,7 @@ import { Namespace } from "../../storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; import { GitHubError, GitHubListError } from "../entities/Errors"; import { ListPackage, Package } from "../entities/Package"; -import { Store } from "../entities/Store"; +import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor, moduleFile } from "../repositories/GitHubRepository"; export type ListStorePackagesError = GitHubError | "STORE_NOT_FOUND"; diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index d053b86e1..f5801a404 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -10,7 +10,7 @@ import { Namespace } from "../../storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; import { GitHubError } from "../entities/Errors"; import { BasePackage } from "../entities/Package"; -import { Store } from "../entities/Store"; +import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor, moduleFile } from "../repositories/GitHubRepository"; export type PublishStorePackageError = diff --git a/src/domain/packages/entities/Store.ts b/src/domain/stores/entities/Store.ts similarity index 100% rename from src/domain/packages/entities/Store.ts rename to src/domain/stores/entities/Store.ts diff --git a/src/domain/packages/entities/StorePermissions.ts b/src/domain/stores/entities/StorePermissions.ts similarity index 100% rename from src/domain/packages/entities/StorePermissions.ts rename to src/domain/stores/entities/StorePermissions.ts diff --git a/src/domain/stores/repositories/StoreRepository.ts b/src/domain/stores/repositories/StoreRepository.ts new file mode 100644 index 000000000..790a51804 --- /dev/null +++ b/src/domain/stores/repositories/StoreRepository.ts @@ -0,0 +1,3 @@ +export interface StoreRepository { + +} diff --git a/src/domain/packages/usecases/DeleteStoreUseCase.ts b/src/domain/stores/usecases/DeleteStoreUseCase.ts similarity index 100% rename from src/domain/packages/usecases/DeleteStoreUseCase.ts rename to src/domain/stores/usecases/DeleteStoreUseCase.ts diff --git a/src/domain/packages/usecases/GetStoreUseCase.ts b/src/domain/stores/usecases/GetStoreUseCase.ts similarity index 100% rename from src/domain/packages/usecases/GetStoreUseCase.ts rename to src/domain/stores/usecases/GetStoreUseCase.ts diff --git a/src/domain/packages/usecases/ListStoresUseCase.ts b/src/domain/stores/usecases/ListStoresUseCase.ts similarity index 100% rename from src/domain/packages/usecases/ListStoresUseCase.ts rename to src/domain/stores/usecases/ListStoresUseCase.ts diff --git a/src/domain/packages/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts similarity index 91% rename from src/domain/packages/usecases/SaveStoreUseCase.ts rename to src/domain/stores/usecases/SaveStoreUseCase.ts index 601d5bc25..d596e30d2 100644 --- a/src/domain/packages/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -3,9 +3,9 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { Namespace } from "../../storage/Namespaces"; import { StorageRepository } from "../../storage/repositories/StorageRepository"; -import { GitHubError } from "../entities/Errors"; +import { GitHubError } from "../../packages/entities/Errors"; import { Store } from "../entities/Store"; -import { GitHubRepository } from "../repositories/GitHubRepository"; +import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; export class SaveStoreUseCase implements UseCase { constructor( diff --git a/src/domain/packages/usecases/SetStoreAsDefaultUseCase.ts b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts similarity index 100% rename from src/domain/packages/usecases/SetStoreAsDefaultUseCase.ts rename to src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts diff --git a/src/domain/packages/usecases/ValidateStoreUseCase.ts b/src/domain/stores/usecases/ValidateStoreUseCase.ts similarity index 80% rename from src/domain/packages/usecases/ValidateStoreUseCase.ts rename to src/domain/stores/usecases/ValidateStoreUseCase.ts index d052db168..3fde1d469 100644 --- a/src/domain/packages/usecases/ValidateStoreUseCase.ts +++ b/src/domain/stores/usecases/ValidateStoreUseCase.ts @@ -1,6 +1,6 @@ import { UseCase } from "../../common/entities/UseCase"; import { Store } from "../entities/Store"; -import { GitHubRepository } from "../repositories/GitHubRepository"; +import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; export class ValidateStoreUseCase implements UseCase { constructor(private githubRepository: GitHubRepository) {} diff --git a/src/domain/synchronization/entities/SynchronizationResult.ts b/src/domain/synchronization/entities/SynchronizationResult.ts index 9226b9c41..7b163b918 100644 --- a/src/domain/synchronization/entities/SynchronizationResult.ts +++ b/src/domain/synchronization/entities/SynchronizationResult.ts @@ -1,6 +1,6 @@ import { NamedRef } from "../../common/entities/Ref"; import { PublicInstance } from "../../instance/entities/Instance"; -import { Store } from "../../packages/entities/Store"; +import { Store } from "../../stores/entities/Store"; import { SynchronizationType } from "./SynchronizationType"; export type SynchronizationStatus = "PENDING" | "SUCCESS" | "WARNING" | "ERROR" | "NETWORK ERROR"; diff --git a/src/migrations/tasks/05.multiple-stores.ts b/src/migrations/tasks/05.multiple-stores.ts index 7ca7ab9ff..61fdc1bfa 100644 --- a/src/migrations/tasks/05.multiple-stores.ts +++ b/src/migrations/tasks/05.multiple-stores.ts @@ -1,5 +1,5 @@ import { generateUid } from "d2/uid"; -import { Store } from "../../domain/packages/entities/Store"; +import { Store } from "../../domain/stores/entities/Store"; import { deleteDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index ca4905718..c276cd083 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -5,7 +5,7 @@ import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepositor import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; -import { StorageDataStoreRepository } from "../data/storage/StorageDataStoreRepository"; +import { StorageDataStoreRepository } from "../data/storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; @@ -50,20 +50,20 @@ import { ListImportedPackagesUseCase } from "../domain/package-import/usecases/L import { SaveImportedPackagesUseCase } from "../domain/package-import/usecases/SaveImportedPackagesUseCase"; import { CreatePackageUseCase } from "../domain/packages/usecases/CreatePackageUseCase"; import { DeletePackageUseCase } from "../domain/packages/usecases/DeletePackageUseCase"; -import { DeleteStoreUseCase } from "../domain/packages/usecases/DeleteStoreUseCase"; +import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; import { DiffPackageUseCase } from "../domain/packages/usecases/DiffPackageUseCase"; import { DownloadPackageUseCase } from "../domain/packages/usecases/DownloadPackageUseCase"; import { GetPackageUseCase } from "../domain/packages/usecases/GetPackageUseCase"; import { GetStorePackageUseCase } from "../domain/packages/usecases/GetStorePackageUseCase"; -import { GetStoreUseCase } from "../domain/packages/usecases/GetStoreUseCase"; +import { GetStoreUseCase } from "../domain/stores/usecases/GetStoreUseCase"; import { ImportPackageUseCase } from "../domain/packages/usecases/ImportPackageUseCase"; import { ListPackagesUseCase } from "../domain/packages/usecases/ListPackagesUseCase"; import { ListStorePackagesUseCase } from "../domain/packages/usecases/ListStorePackagesUseCase"; -import { ListStoresUseCase } from "../domain/packages/usecases/ListStoresUseCase"; +import { ListStoresUseCase } from "../domain/stores/usecases/ListStoresUseCase"; import { PublishStorePackageUseCase } from "../domain/packages/usecases/PublishStorePackageUseCase"; -import { SaveStoreUseCase } from "../domain/packages/usecases/SaveStoreUseCase"; -import { SetStoreAsDefaultUseCase } from "../domain/packages/usecases/SetStoreAsDefaultUseCase"; -import { ValidateStoreUseCase } from "../domain/packages/usecases/ValidateStoreUseCase"; +import { SaveStoreUseCase } from "../domain/stores/usecases/SaveStoreUseCase"; +import { SetStoreAsDefaultUseCase } from "../domain/stores/usecases/SetStoreAsDefaultUseCase"; +import { ValidateStoreUseCase } from "../domain/stores/usecases/ValidateStoreUseCase"; import { Repositories } from "../domain/Repositories"; import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/CreatePullRequestUseCase"; diff --git a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx b/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx index edc32a9e5..63ac7e1a0 100644 --- a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx +++ b/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx @@ -5,7 +5,7 @@ import i18n from "../../../../locales"; import { Maybe } from "../../../../types/utils"; import Dropdown, { DropdownViewOption } from "../dropdown/Dropdown"; import { useAppContext } from "../../contexts/AppContext"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; export type InstanceSelectionOption = "local" | "remote" | "store"; diff --git a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx b/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx index 4c96aa369..9b2ee092c 100644 --- a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx +++ b/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx @@ -12,7 +12,7 @@ import { InstanceSelectionOption, } from "../instance-selection-dropdown/InstanceSelectionDropdown"; import { useViewSelector, ViewSelectorConfig } from "./useViewSelector"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; export interface ModulePackageListTableProps { onCreate?(): void; diff --git a/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx b/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx index 2548f7d6f..0bb2dc537 100644 --- a/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx +++ b/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx @@ -1,7 +1,7 @@ import { Box, Icon, IconButton } from "@material-ui/core"; import React, { useState } from "react"; import { PackageSource } from "../../../../../domain/package-import/entities/PackageSource"; -import { Store } from "../../../../../domain/packages/entities/Store"; +import { Store } from "../../../../../domain/stores/entities/Store"; import i18n from "../../../../../locales"; import { InstanceSelectionDropdown, diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index fc9a6d87d..240f54aaf 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -26,7 +26,7 @@ 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 { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import SyncReport from "../../../../models/syncReport"; import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; diff --git a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx b/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx index e7c1599f4..26891591e 100644 --- a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx +++ b/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx @@ -9,7 +9,7 @@ import { MetadataPackageDiff, ModelDiff, } from "../../../../domain/packages/entities/MetadataPackageDiff"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { useAppContext } from "../../contexts/AppContext"; import SyncSummary from "../sync-summary/SyncSummary"; diff --git a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx b/src/presentation/react/components/store-creation/StoreCreationDialog.tsx index 5d27d4fbb..9ca7da7ca 100644 --- a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx +++ b/src/presentation/react/components/store-creation/StoreCreationDialog.tsx @@ -17,7 +17,7 @@ import { } from "d2-ui-components"; import React, { useCallback, useMemo, useState } from "react"; import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { useAppContext } from "../../contexts/AppContext"; import Linkify from "react-linkify"; diff --git a/src/presentation/react/components/sync-summary/SyncSummary.tsx b/src/presentation/react/components/sync-summary/SyncSummary.tsx index f4e72d870..aaf9b8cf1 100644 --- a/src/presentation/react/components/sync-summary/SyncSummary.tsx +++ b/src/presentation/react/components/sync-summary/SyncSummary.tsx @@ -18,7 +18,7 @@ import _ from "lodash"; import React, { useEffect, useState } from "react"; import ReactJson from "react-json-view"; import { PublicInstance } from "../../../../domain/instance/entities/Instance"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import { ErrorMessage, SynchronizationResult, diff --git a/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx index 4368d8e80..e2b8a8f2e 100644 --- a/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx +++ b/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx @@ -1,7 +1,7 @@ import { makeStyles, TextField } from "@material-ui/core"; import React, { useCallback, useState } from "react"; import { Instance } from "../../../../../domain/instance/entities/Instance"; -import { Store } from "../../../../../domain/packages/entities/Store"; +import { Store } from "../../../../../domain/stores/entities/Store"; import i18n from "../../../../../locales"; import SyncRule from "../../../../../models/syncRule"; import { Dictionary } from "../../../../../types/utils"; diff --git a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx index 987e7066b..853df789d 100644 --- a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx @@ -39,7 +39,7 @@ import SyncDialog from "../../../react/components/sync-dialog/SyncDialog"; import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; import InstancesSelectors from "./InstancesSelectors"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; const config: Record< SynchronizationType, diff --git a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx index c6161c7d8..4182c7530 100644 --- a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx @@ -3,7 +3,7 @@ import { PaginationOptions } from "d2-ui-components"; import React, { ReactNode, useCallback, 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"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import SyncReport from "../../../../models/syncReport"; import { CreatePackageFromFileDialog } from "../../../react/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; diff --git a/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx b/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx index c3415cea0..e35b4c085 100644 --- a/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx +++ b/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataResponsible } from "../../../../domain/metadata/entities/MetadataResponsible"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { DataSetModel, ProgramModel } from "../../../../models/dhis/metadata"; import { isAppConfigurator } from "../../../../utils/permissions"; diff --git a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx index 9eb18ea4d..e26608595 100644 --- a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import Linkify from "react-linkify"; import { useHistory, useParams } from "react-router-dom"; import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { useAppContext } from "../../../react/contexts/AppContext"; import PageHeader from "../../../react/components/page-header/PageHeader"; diff --git a/src/presentation/webapp/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/pages/store-list/StoreListPage.tsx index 9d881dc57..59efed840 100644 --- a/src/presentation/webapp/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/pages/store-list/StoreListPage.tsx @@ -13,7 +13,7 @@ import { import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/packages/entities/Store"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import PageHeader from "../../../react/components/page-header/PageHeader"; import { useAppContext } from "../../../react/contexts/AppContext"; From 5c0cf8bb27080b6a90589abf4e8da6f09bf677d5 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Nov 2020 09:04:39 +0100 Subject: [PATCH 002/163] Update some methods to use the new repository --- src/data/storage/StorageConstantClient.ts | 2 +- src/data/storage/StorageDataStoreClient.ts | 2 +- src/data/stores/StoreD2ApiRepository.ts | 41 ++++++++++++++++++- .../__tests__/integration/helpers.ts | 4 +- .../storage/repositories/StorageRepository.ts | 3 +- .../stores/repositories/StoreRepository.ts | 8 +++- .../stores/usecases/DeleteStoreUseCase.ts | 24 ++--------- .../stores/usecases/ListStoresUseCase.ts | 11 ++--- .../stores/usecases/SaveStoreUseCase.ts | 10 ++--- src/presentation/CompositionRoot.ts | 14 ++++--- 10 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index 734dc8cca..ef547dca3 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -11,7 +11,7 @@ interface Constant { const defaultName = "Bulk Load Storage"; -export class StorageConstantRepository extends StorageRepository { +export class StorageConstantClient extends StorageRepository { constructor(private api: D2Api) { super(); } diff --git a/src/data/storage/StorageDataStoreClient.ts b/src/data/storage/StorageDataStoreClient.ts index 68010613d..99f36656c 100644 --- a/src/data/storage/StorageDataStoreClient.ts +++ b/src/data/storage/StorageDataStoreClient.ts @@ -5,7 +5,7 @@ import { getD2APiFromInstance } from "../../utils/d2-utils"; const dataStoreNamespace = "metadata-synchronization"; -export class StorageDataStoreRepository extends StorageRepository { +export class StorageDataStoreClient extends StorageRepository { private api: D2Api; private dataStore: DataStore; diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index f102ee884..4d44a1bc6 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,5 +1,44 @@ +import { Instance } from "../../domain/instance/entities/Instance"; +import { Namespace } from "../../domain/storage/Namespaces"; +import { StorageRepository } from "../../domain/storage/repositories/StorageRepository"; +import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; +import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class StoreD2ApiRepository implements StoreRepository { + private storageClient: StorageRepository; -} \ No newline at end of file + constructor(instance: Instance) { + this.storageClient = new StorageDataStoreClient(instance); + } + + public async list(): Promise { + const stores = await this.storageClient.listObjectsInCollection(Namespace.STORES); + return stores.filter(store => !store.deleted); + } + + public async getById(_id: string): Promise { + throw new Error("Method not implemented."); + } + + public async delete(id: string): Promise { + const store = await this.storageClient.getObjectInCollection(Namespace.STORES, id); + + if (!store) return false; + + await this.storageClient.saveObjectInCollection(Namespace.STORES, { + ...store, + deleted: true, + }); + + return true; + } + + public async save(store: Store): Promise { + await this.storageClient.saveObjectInCollection(Namespace.STORES, store); + } + + public async getDefault(): Promise { + throw new Error("Method not implemented."); + } +} diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 72b4d26b4..fd5e4dec0 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -8,7 +8,7 @@ import { RepositoryFactory } from "../../../../domain/common/factories/Repositor import { Repositories } from "../../../../domain/Repositories"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreClient"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; import { AnyRegistry } from "miragejs/-types"; @@ -17,7 +17,7 @@ import { startDhis } from "../../../../utils/dhisServer"; export function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/domain/storage/repositories/StorageRepository.ts b/src/domain/storage/repositories/StorageRepository.ts index da49333bb..74625f3a6 100644 --- a/src/domain/storage/repositories/StorageRepository.ts +++ b/src/domain/storage/repositories/StorageRepository.ts @@ -15,7 +15,8 @@ export abstract class StorageRepository { public abstract removeObject(key: string): Promise; public async listObjectsInCollection(key: string): Promise { - return (await this.getObject(key)) ?? []; + const collection = await this.getObject(key); + return collection ?? []; } public async getObjectInCollection( diff --git a/src/domain/stores/repositories/StoreRepository.ts b/src/domain/stores/repositories/StoreRepository.ts index 790a51804..4aa6f86e8 100644 --- a/src/domain/stores/repositories/StoreRepository.ts +++ b/src/domain/stores/repositories/StoreRepository.ts @@ -1,3 +1,9 @@ -export interface StoreRepository { +import { Store } from "../entities/Store"; +export interface StoreRepository { + list(): Promise; + getById(id: string): Promise; + delete(id: string): Promise; + save(store: Store): Promise; + getDefault(): Promise; } diff --git a/src/domain/stores/usecases/DeleteStoreUseCase.ts b/src/domain/stores/usecases/DeleteStoreUseCase.ts index 75084eedf..746af5b45 100644 --- a/src/domain/stores/usecases/DeleteStoreUseCase.ts +++ b/src/domain/stores/usecases/DeleteStoreUseCase.ts @@ -1,28 +1,10 @@ import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; -import { Store } from "../entities/Store"; +import { StoreRepository } from "../repositories/StoreRepository"; export class DeleteStoreUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storeRepository: StoreRepository) {} public async execute(id: string): Promise { - const store = await this.storageRepository.getObjectInCollection( - Namespace.STORES, - id - ); - - try { - if (!store) return false; - - await this.storageRepository.saveObjectInCollection(Namespace.STORES, { - ...store, - deleted: true, - }); - } catch (error) { - return false; - } - - return true; + return this.storeRepository.delete(id); } } diff --git a/src/domain/stores/usecases/ListStoresUseCase.ts b/src/domain/stores/usecases/ListStoresUseCase.ts index cac0ef121..dcf3df279 100644 --- a/src/domain/stores/usecases/ListStoresUseCase.ts +++ b/src/domain/stores/usecases/ListStoresUseCase.ts @@ -1,16 +1,11 @@ import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; import { Store } from "../entities/Store"; +import { StoreRepository } from "../repositories/StoreRepository"; export class ListStoresUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storeRepository: StoreRepository) {} public async execute(): Promise { - const stores = await this.storageRepository.listObjectsInCollection( - Namespace.STORES - ); - - return stores.filter(store => !store.deleted); + return this.storeRepository.list(); } } diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index d596e30d2..03b37406d 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -21,11 +21,11 @@ export class SaveStoreUseCase implements UseCase { const isFirstStore = await this.isFirstStore(store); - const isNew = !store.id; - - const storeToSave = isNew - ? { ...store, id: generateUid(), default: isFirstStore ? true : store.default } - : store; + const storeToSave = { + ...store, + id: store.id || generateUid(), + default: isFirstStore ? true : store.default, + }; await this.storageRepository.saveObjectInCollection(Namespace.STORES, storeToSave); diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index c276cd083..734486177 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -5,7 +5,7 @@ import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepositor import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; -import { StorageDataStoreRepository } from "../data/storage/StorageDataStoreClient"; +import { StorageDataStoreClient } from "../data/storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; @@ -70,6 +70,7 @@ import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/Cre import { PrepareSyncUseCase } from "../domain/synchronization/usecases/PrepareSyncUseCase"; import { SynchronizationBuilder } from "../types/synchronization"; import { cache } from "../utils/cache"; +import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; export class CompositionRoot { private repositoryFactory: RepositoryFactory; @@ -77,7 +78,7 @@ export class CompositionRoot { constructor(public readonly localInstance: Instance, private encryptionKey: string) { this.repositoryFactory = new RepositoryFactory(); this.repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - this.repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + this.repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); this.repositoryFactory.bind(Repositories.DownloadRepository, DownloadWebRepository); this.repositoryFactory.bind(Repositories.GitHubRepository, GitHubOctokitRepository); this.repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); @@ -161,14 +162,15 @@ export class CompositionRoot { @cache() public get store() { const github = new GitHubOctokitRepository(); - const storage = new StorageDataStoreRepository(this.localInstance); + const storage = new StorageDataStoreClient(this.localInstance); + const storeRepository = new StoreD2ApiRepository(this.localInstance); return getExecute({ get: new GetStoreUseCase(storage), update: new SaveStoreUseCase(github, storage), validate: new ValidateStoreUseCase(github), - list: new ListStoresUseCase(storage), - delete: new DeleteStoreUseCase(storage), + list: new ListStoresUseCase(storeRepository), + delete: new DeleteStoreUseCase(storeRepository), setAsDefault: new SetStoreAsDefaultUseCase(storage), }); } @@ -284,7 +286,7 @@ export class CompositionRoot { @cache() public get mapping() { - const storage = new StorageDataStoreRepository(this.localInstance); + const storage = new StorageDataStoreClient(this.localInstance); return getExecute({ get: new GetMappingByOwnerUseCase(storage), From cc5a678d4355714d9554ffc693f639497a8e8add Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Nov 2020 09:08:19 +0100 Subject: [PATCH 003/163] Add githook for dep check --- .githooks/dep-check | 10 ++++++++++ package.json | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 .githooks/dep-check diff --git a/.githooks/dep-check b/.githooks/dep-check new file mode 100644 index 000000000..ef6dd39ba --- /dev/null +++ b/.githooks/dep-check @@ -0,0 +1,10 @@ +#!/bin/sh + +changed() { + git diff --name-only HEAD@{1} HEAD | grep "^$1" > /dev/null 2>&1 +} + +if changed 'yarn.lock'; then + echo "Lockfile changes detected. Installing updates..." + yarn install +fi diff --git a/package.json b/package.json index 3bf632e3d..f9c18bb6c 100644 --- a/package.json +++ b/package.json @@ -71,14 +71,14 @@ "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", "cy:e2e:run": "CYPRESS_E2E=true cypress run --browser chrome" }, "husky": { "hooks": { - "pre-push": "yarn pre-push" + "pre-push": "yarn prettify && yarn lint && yarn update-po && yarn test", + "post-merge": "./.githooks/dep-check" } }, "devDependencies": { From 2644527af03b405d7f30278dd41d981ea3bac9cf Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 12 Nov 2020 09:11:10 +0100 Subject: [PATCH 004/163] Fix tests --- .../metadata/__tests__/integration/sync-aggregated.spec.ts | 4 ++-- src/data/metadata/__tests__/integration/sync-events.spec.ts | 4 ++-- src/data/metadata/__tests__/integration/sync-metadata.spec.ts | 4 ++-- .../__tests__/integration/transformations-api-30.spec.ts | 4 ++-- .../__tests__/integration/transformations-api-32.spec.ts | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts index cb692ead9..0cad5dbde 100644 --- a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts @@ -9,7 +9,7 @@ import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -245,7 +245,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-events.spec.ts b/src/data/metadata/__tests__/integration/sync-events.spec.ts index 53017847c..c9e475572 100644 --- a/src/data/metadata/__tests__/integration/sync-events.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-events.spec.ts @@ -10,7 +10,7 @@ import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; import { EventsD2ApiRepository } from "../../../events/EventsD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -277,7 +277,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.EventsRepository, EventsD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts index f416fa0b4..abbd0d3e7 100644 --- a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts @@ -8,7 +8,7 @@ import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -144,7 +144,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts index 6da6656cd..543805de8 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts @@ -9,7 +9,7 @@ import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; const repositoryFactory = buildRepositoryFactory(); @@ -398,7 +398,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts index 7a3c4762d..680e8073f 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts @@ -9,7 +9,7 @@ import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreRepository } from "../../../storage/StorageDataStoreRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; const repositoryFactory = buildRepositoryFactory(); @@ -276,7 +276,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; From c9d52cb203b28987ec70f70ed1dc1c6babef2e2c Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 23 Nov 2020 08:43:55 +0100 Subject: [PATCH 005/163] Update assets path --- .../assets/img/help-store-github.png | Bin .../store-creation/StoreCreationDialog.tsx | 2 +- .../pages/store-creation/StoreCreationPage.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => presentation}/assets/img/help-store-github.png (100%) diff --git a/src/assets/img/help-store-github.png b/src/presentation/assets/img/help-store-github.png similarity index 100% rename from src/assets/img/help-store-github.png rename to src/presentation/assets/img/help-store-github.png diff --git a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx b/src/presentation/react/components/store-creation/StoreCreationDialog.tsx index 9ca7da7ca..44a1268c7 100644 --- a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx +++ b/src/presentation/react/components/store-creation/StoreCreationDialog.tsx @@ -21,7 +21,7 @@ import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { useAppContext } from "../../contexts/AppContext"; import Linkify from "react-linkify"; -import helpStoreGithub from "../../../../assets/img/help-store-github.png"; +import helpStoreGithub from "../../../assets/img/help-store-github.png"; interface StoreCreationDialogProps { isOpen: boolean; diff --git a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx index e26608595..1e127d761 100644 --- a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx @@ -14,7 +14,7 @@ import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { useAppContext } from "../../../react/contexts/AppContext"; import PageHeader from "../../../react/components/page-header/PageHeader"; -import helpStoreGithub from "../../../../assets/img/help-store-github.png"; +import helpStoreGithub from "../../../assets/img/help-store-github.png"; const StoreCreationPage: React.FC = () => { const { compositionRoot } = useAppContext(); From 4ddced6f1cf7a6f1400353c4282cf7f7dce69f96 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 23 Nov 2020 08:46:07 +0100 Subject: [PATCH 006/163] Rename storage repository to client --- src/data/storage/StorageConstantClient.ts | 4 ++-- src/data/storage/StorageDataStoreClient.ts | 4 ++-- src/data/stores/StoreD2ApiRepository.ts | 4 ++-- src/domain/instance/usecases/DeleteInstanceUseCase.ts | 2 +- src/domain/instance/usecases/GetInstanceByIdUseCase.ts | 2 +- src/domain/instance/usecases/ListInstancesUseCase.ts | 2 +- src/domain/instance/usecases/SaveInstanceUseCase.ts | 2 +- src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts | 4 ++-- src/domain/mapping/usecases/SaveMappingUseCase.ts | 4 ++-- src/domain/metadata/usecases/GetResponsiblesUseCase.ts | 2 +- src/domain/metadata/usecases/ListResponsiblesUseCase.ts | 2 +- src/domain/metadata/usecases/SetResponsiblesUseCase.ts | 2 +- src/domain/modules/usecases/DeleteModuleUseCase.ts | 2 +- src/domain/modules/usecases/GetModuleUseCase.ts | 2 +- src/domain/modules/usecases/ListModulesUseCase.ts | 2 +- src/domain/modules/usecases/SaveModuleUseCase.ts | 2 +- src/domain/notifications/usecases/CancelPullRequestUseCase.ts | 2 +- src/domain/notifications/usecases/ImportPullRequestUseCase.ts | 2 +- src/domain/notifications/usecases/ListNotificationsUseCase.ts | 2 +- .../notifications/usecases/MarkReadNotificationsUseCase.ts | 2 +- .../notifications/usecases/UpdatePullRequestStatusUseCase.ts | 2 +- .../package-import/usecases/ListImportedPackagesUseCase.ts | 2 +- .../package-import/usecases/SaveImportedPackagesUseCase.ts | 2 +- src/domain/packages/usecases/CreatePackageUseCase.ts | 2 +- src/domain/packages/usecases/DeletePackageUseCase.ts | 2 +- src/domain/packages/usecases/DiffPackageUseCase.ts | 2 +- src/domain/packages/usecases/DownloadPackageUseCase.ts | 2 +- src/domain/packages/usecases/GetPackageUseCase.ts | 2 +- src/domain/packages/usecases/GetStorePackageUseCase.ts | 2 +- src/domain/packages/usecases/ListPackagesUseCase.ts | 2 +- src/domain/packages/usecases/ListStorePackagesUseCase.ts | 2 +- src/domain/packages/usecases/PublishStorePackageUseCase.ts | 2 +- .../repositories/{StorageRepository.ts => StorageClient.ts} | 4 ++-- src/domain/stores/usecases/GetStoreUseCase.ts | 4 ++-- src/domain/stores/usecases/SaveStoreUseCase.ts | 4 ++-- src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts | 4 ++-- .../synchronization/usecases/CreatePullRequestUseCase.ts | 2 +- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 2 +- src/domain/synchronization/usecases/PrepareSyncUseCase.ts | 2 +- 39 files changed, 48 insertions(+), 48 deletions(-) rename src/domain/storage/repositories/{StorageRepository.ts => StorageClient.ts} (97%) diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index ef547dca3..e71e6edc8 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -1,5 +1,5 @@ import { generateUid } from "d2/uid"; -import { StorageRepository } from "../../domain/storage/repositories/StorageRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { D2Api } from "../../types/d2-api"; interface Constant { @@ -11,7 +11,7 @@ interface Constant { const defaultName = "Bulk Load Storage"; -export class StorageConstantClient extends StorageRepository { +export class StorageConstantClient extends StorageClient { constructor(private api: D2Api) { super(); } diff --git a/src/data/storage/StorageDataStoreClient.ts b/src/data/storage/StorageDataStoreClient.ts index 99f36656c..9653cc844 100644 --- a/src/data/storage/StorageDataStoreClient.ts +++ b/src/data/storage/StorageDataStoreClient.ts @@ -1,11 +1,11 @@ import { Instance } from "../../domain/instance/entities/Instance"; -import { StorageRepository } from "../../domain/storage/repositories/StorageRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { D2Api, DataStore } from "../../types/d2-api"; import { getD2APiFromInstance } from "../../utils/d2-utils"; const dataStoreNamespace = "metadata-synchronization"; -export class StorageDataStoreClient extends StorageRepository { +export class StorageDataStoreClient extends StorageClient { private api: D2Api; private dataStore: DataStore; diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index 4d44a1bc6..5c6f962bf 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,12 +1,12 @@ import { Instance } from "../../domain/instance/entities/Instance"; import { Namespace } from "../../domain/storage/Namespaces"; -import { StorageRepository } from "../../domain/storage/repositories/StorageRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class StoreD2ApiRepository implements StoreRepository { - private storageClient: StorageRepository; + private storageClient: StorageClient; constructor(instance: Instance) { this.storageClient = new StorageDataStoreClient(instance); diff --git a/src/domain/instance/usecases/DeleteInstanceUseCase.ts b/src/domain/instance/usecases/DeleteInstanceUseCase.ts index 494bf7e16..78adbfd98 100644 --- a/src/domain/instance/usecases/DeleteInstanceUseCase.ts +++ b/src/domain/instance/usecases/DeleteInstanceUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance } from "../entities/Instance"; export class DeleteInstanceUseCase implements UseCase { diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index 5fbf6cd22..1a84c6220 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance, InstanceData } from "../entities/Instance"; import { Either } from "../../common/entities/Either"; diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index d35a49b47..3fa1f56e3 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -3,7 +3,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance, InstanceData } from "../entities/Instance"; export interface ListInstancesUseCaseProps { diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index bb4ae40b3..9bcdb862a 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -3,7 +3,7 @@ import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance } from "../entities/Instance"; export class SaveInstanceUseCase implements UseCase { diff --git a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts index df227edcb..f2b9dcb8d 100644 --- a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts +++ b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts @@ -1,12 +1,12 @@ import { UseCase } from "../../common/entities/UseCase"; import { Instance } from "../../instance/entities/Instance"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; +import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore, MappingOwner } from "../entities/MappingOwner"; export class GetMappingByOwnerUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storageRepository: StorageClient) {} public async execute(owner: MappingOwner): Promise { if (isMappingOwnerStore(owner)) { diff --git a/src/domain/mapping/usecases/SaveMappingUseCase.ts b/src/domain/mapping/usecases/SaveMappingUseCase.ts index 3f082219b..296532274 100644 --- a/src/domain/mapping/usecases/SaveMappingUseCase.ts +++ b/src/domain/mapping/usecases/SaveMappingUseCase.ts @@ -2,14 +2,14 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; +import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore } from "../entities/MappingOwner"; export type SaveMappingError = "UNEXPECTED_ERROR" | "INSTANCE_NOT_FOUND"; export class SaveMappingUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storageRepository: StorageClient) {} public async execute(mapping: DataSourceMapping): Promise> { if (isMappingOwnerStore(mapping.owner)) { diff --git a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts index 5eb216194..d322e43de 100644 --- a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts @@ -3,7 +3,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; export class GetResponsiblesUseCase implements UseCase { diff --git a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts index e9c7621c0..6484b7ece 100644 --- a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataResponsible } from "../entities/MetadataResponsible"; import { MetadataRepositoryConstructor } from "../repositories/MetadataRepository"; diff --git a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts index a17ddabea..001987ab6 100644 --- a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts @@ -7,7 +7,7 @@ import { Instance } from "../../instance/entities/Instance"; import { ReceivedPullRequestNotification } from "../../notifications/entities/PullRequestNotification"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; export class SetResponsiblesUseCase implements UseCase { diff --git a/src/domain/modules/usecases/DeleteModuleUseCase.ts b/src/domain/modules/usecases/DeleteModuleUseCase.ts index ffb51e170..7ee2f8014 100644 --- a/src/domain/modules/usecases/DeleteModuleUseCase.ts +++ b/src/domain/modules/usecases/DeleteModuleUseCase.ts @@ -5,7 +5,7 @@ import { Instance } from "../../instance/entities/Instance"; import { BasePackage, Package } from "../../packages/entities/Package"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; export class DeleteModuleUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} diff --git a/src/domain/modules/usecases/GetModuleUseCase.ts b/src/domain/modules/usecases/GetModuleUseCase.ts index 4a04dbb65..cec276739 100644 --- a/src/domain/modules/usecases/GetModuleUseCase.ts +++ b/src/domain/modules/usecases/GetModuleUseCase.ts @@ -3,7 +3,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index 6c74b1c8b..4f7699d7a 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -5,7 +5,7 @@ import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; diff --git a/src/domain/modules/usecases/SaveModuleUseCase.ts b/src/domain/modules/usecases/SaveModuleUseCase.ts index bee4a1878..c5500e6f0 100644 --- a/src/domain/modules/usecases/SaveModuleUseCase.ts +++ b/src/domain/modules/usecases/SaveModuleUseCase.ts @@ -6,7 +6,7 @@ import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Module } from "../entities/Module"; export class SaveModuleUseCase implements UseCase { diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index 52195156c..84e3e1b56 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -5,7 +5,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; import { SentPullRequestNotification, diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index c308896d5..e8b229828 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -12,7 +12,7 @@ import { } from "../../metadata/repositories/MetadataRepository"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { AppNotification } from "../entities/Notification"; diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 9fd7b8355..42009f851 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -6,7 +6,7 @@ import { Instance, InstanceData } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; export class ListNotificationsUseCase implements UseCase { diff --git a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts index 003eba330..63e12ba64 100644 --- a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts @@ -5,7 +5,7 @@ import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; export class MarkReadNotificationsUseCase implements UseCase { diff --git a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts index e999e4c01..ba4ddcebd 100644 --- a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts +++ b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts @@ -8,7 +8,7 @@ import { InstanceRepositoryConstructor } from "../../instance/repositories/Insta import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { PullRequestStatus, ReceivedPullRequestNotification, diff --git a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts index 46501fd6c..aa8b33be1 100644 --- a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackageData } from "../entities/ImportedPackage"; type ListImportedPackageError = "UNEXPECTED_ERROR"; diff --git a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts index cc2f80f8d..6edc02071 100644 --- a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackage } from "../entities/ImportedPackage"; type SavePackageError = "UNEXPECTED_ERROR"; diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index ac6f702d8..96a3f44bf 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -12,7 +12,7 @@ import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Module } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { Package } from "../entities/Package"; diff --git a/src/domain/packages/usecases/DeletePackageUseCase.ts b/src/domain/packages/usecases/DeletePackageUseCase.ts index df6697d2f..639dcaf3a 100644 --- a/src/domain/packages/usecases/DeletePackageUseCase.ts +++ b/src/domain/packages/usecases/DeletePackageUseCase.ts @@ -3,7 +3,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; export class DeletePackageUseCase implements UseCase { diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index e923e9b61..c8fe32da7 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -5,7 +5,7 @@ import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { getMetadataPackageDiff, MetadataPackageDiff } from "../entities/MetadataPackageDiff"; import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index ea9338823..6f75e5eff 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -8,7 +8,7 @@ import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; diff --git a/src/domain/packages/usecases/GetPackageUseCase.ts b/src/domain/packages/usecases/GetPackageUseCase.ts index efbf0c410..52c340914 100644 --- a/src/domain/packages/usecases/GetPackageUseCase.ts +++ b/src/domain/packages/usecases/GetPackageUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; export class GetPackageUseCase implements UseCase { diff --git a/src/domain/packages/usecases/GetStorePackageUseCase.ts b/src/domain/packages/usecases/GetStorePackageUseCase.ts index 1732d9535..c87c5f293 100644 --- a/src/domain/packages/usecases/GetStorePackageUseCase.ts +++ b/src/domain/packages/usecases/GetStorePackageUseCase.ts @@ -7,7 +7,7 @@ import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; import { Store } from "../../stores/entities/Store"; import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; diff --git a/src/domain/packages/usecases/ListPackagesUseCase.ts b/src/domain/packages/usecases/ListPackagesUseCase.ts index 4524952c4..3d68beab0 100644 --- a/src/domain/packages/usecases/ListPackagesUseCase.ts +++ b/src/domain/packages/usecases/ListPackagesUseCase.ts @@ -6,7 +6,7 @@ import { InstanceRepositoryConstructor } from "../../instance/repositories/Insta import { MetadataModule } from "../../modules/entities/MetadataModule"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; export class ListPackagesUseCase implements UseCase { diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index b80369938..69d67e4f3 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -11,7 +11,7 @@ import { MetadataModule } from "../../modules/entities/MetadataModule"; import { BaseModule } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { GitHubError, GitHubListError } from "../entities/Errors"; import { ListPackage, Package } from "../entities/Package"; import { Store } from "../../stores/entities/Store"; diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index f5801a404..765006fd3 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -7,7 +7,7 @@ import { Instance } from "../../instance/entities/Instance"; import { BaseModule } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { GitHubError } from "../entities/Errors"; import { BasePackage } from "../entities/Package"; import { Store } from "../../stores/entities/Store"; diff --git a/src/domain/storage/repositories/StorageRepository.ts b/src/domain/storage/repositories/StorageClient.ts similarity index 97% rename from src/domain/storage/repositories/StorageRepository.ts rename to src/domain/storage/repositories/StorageClient.ts index 74625f3a6..cd733aab5 100644 --- a/src/domain/storage/repositories/StorageRepository.ts +++ b/src/domain/storage/repositories/StorageClient.ts @@ -4,10 +4,10 @@ import { Instance } from "../../instance/entities/Instance"; import { Namespace, NamespaceProperties } from "../Namespaces"; export interface StorageRepositoryConstructor { - new (instance: Instance): StorageRepository; + new (instance: Instance): StorageClient; } -export abstract class StorageRepository { +export abstract class StorageClient { // Object operations public abstract getObject(key: string): Promise; public abstract getOrCreateObject(key: string, defaultValue: T): Promise; diff --git a/src/domain/stores/usecases/GetStoreUseCase.ts b/src/domain/stores/usecases/GetStoreUseCase.ts index 1be33b4fa..9ee8b8548 100644 --- a/src/domain/stores/usecases/GetStoreUseCase.ts +++ b/src/domain/stores/usecases/GetStoreUseCase.ts @@ -1,10 +1,10 @@ import { UseCase } from "../../common/entities/UseCase"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; +import { StorageClient } from "../../storage/repositories/StorageClient"; import { Store } from "../entities/Store"; export class GetStoreUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storageRepository: StorageClient) {} public async execute(id: string): Promise { const store = this.storageRepository.getObjectInCollection(Namespace.STORES, id); diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index 03b37406d..377f95d9c 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -2,7 +2,7 @@ import { generateUid } from "d2/uid"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; +import { StorageClient } from "../../storage/repositories/StorageClient"; import { GitHubError } from "../../packages/entities/Errors"; import { Store } from "../entities/Store"; import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; @@ -10,7 +10,7 @@ import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; export class SaveStoreUseCase implements UseCase { constructor( private githubRepository: GitHubRepository, - private storageRepository: StorageRepository + private storageRepository: StorageClient ) {} public async execute(store: Store, validate = true): Promise> { diff --git a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts index b34016681..e4fcc3991 100644 --- a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts +++ b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts @@ -1,7 +1,7 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepository } from "../../storage/repositories/StorageRepository"; +import { StorageClient } from "../../storage/repositories/StorageClient"; import { Store } from "../entities/Store"; type SetStoreAsDefaultError = { @@ -9,7 +9,7 @@ type SetStoreAsDefaultError = { }; export class SetStoreAsDefaultUseCase implements UseCase { - constructor(private storageRepository: StorageRepository) {} + constructor(private storageRepository: StorageClient) {} public async execute(id: string): Promise> { try { diff --git a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts index e0c1ff9d6..b3b3ffede 100644 --- a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts +++ b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts @@ -14,7 +14,7 @@ import { } from "../../notifications/entities/PullRequestNotification"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; interface CreatePullRequestParams { diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 5105aeb9f..1ed3726b1 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -24,7 +24,7 @@ import { DeletedMetadataSyncUseCase } from "../../metadata/usecases/DeletedMetad import { MetadataSyncUseCase } from "../../metadata/usecases/MetadataSyncUseCase"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { AggregatedDataStats, diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index ebb6e8841..6ec28fb7b 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -8,7 +8,7 @@ import { InstanceRepositoryConstructor } from "../../instance/repositories/Insta import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; import { Repositories } from "../../Repositories"; import { Namespace } from "../../storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; export type PrepareSyncError = "PULL_REQUEST" | "PULL_REQUEST_RESPONSIBLE" | "INSTANCE_NOT_FOUND"; From 640da0246ea5aee0e8b770ab3ab27df6afb56702 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 23 Nov 2020 08:47:32 +0100 Subject: [PATCH 007/163] Move namespaces to data layer --- src/{domain => data}/storage/Namespaces.ts | 0 src/data/stores/StoreD2ApiRepository.ts | 2 +- src/domain/instance/usecases/DeleteInstanceUseCase.ts | 2 +- src/domain/instance/usecases/GetInstanceByIdUseCase.ts | 2 +- src/domain/instance/usecases/ListInstancesUseCase.ts | 2 +- src/domain/instance/usecases/SaveInstanceUseCase.ts | 2 +- src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts | 2 +- src/domain/mapping/usecases/SaveMappingUseCase.ts | 2 +- src/domain/metadata/usecases/GetResponsiblesUseCase.ts | 2 +- src/domain/metadata/usecases/ListResponsiblesUseCase.ts | 2 +- src/domain/metadata/usecases/SetResponsiblesUseCase.ts | 2 +- src/domain/modules/usecases/DeleteModuleUseCase.ts | 2 +- src/domain/modules/usecases/GetModuleUseCase.ts | 2 +- src/domain/modules/usecases/ListModulesUseCase.ts | 2 +- src/domain/modules/usecases/SaveModuleUseCase.ts | 2 +- src/domain/notifications/usecases/CancelPullRequestUseCase.ts | 2 +- src/domain/notifications/usecases/ImportPullRequestUseCase.ts | 2 +- src/domain/notifications/usecases/ListNotificationsUseCase.ts | 2 +- .../notifications/usecases/MarkReadNotificationsUseCase.ts | 2 +- .../notifications/usecases/UpdatePullRequestStatusUseCase.ts | 2 +- .../package-import/usecases/ListImportedPackagesUseCase.ts | 2 +- .../package-import/usecases/SaveImportedPackagesUseCase.ts | 2 +- src/domain/packages/usecases/CreatePackageUseCase.ts | 2 +- src/domain/packages/usecases/DeletePackageUseCase.ts | 2 +- src/domain/packages/usecases/DiffPackageUseCase.ts | 2 +- src/domain/packages/usecases/DownloadPackageUseCase.ts | 2 +- src/domain/packages/usecases/GetPackageUseCase.ts | 2 +- src/domain/packages/usecases/GetStorePackageUseCase.ts | 2 +- src/domain/packages/usecases/ListPackagesUseCase.ts | 2 +- src/domain/packages/usecases/ListStorePackagesUseCase.ts | 2 +- src/domain/packages/usecases/PublishStorePackageUseCase.ts | 2 +- src/domain/storage/repositories/StorageClient.ts | 2 +- src/domain/stores/usecases/GetStoreUseCase.ts | 2 +- src/domain/stores/usecases/SaveStoreUseCase.ts | 2 +- src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts | 2 +- src/domain/synchronization/usecases/CreatePullRequestUseCase.ts | 2 +- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 2 +- src/domain/synchronization/usecases/PrepareSyncUseCase.ts | 2 +- src/models/syncReport.ts | 2 +- 39 files changed, 38 insertions(+), 38 deletions(-) rename src/{domain => data}/storage/Namespaces.ts (100%) diff --git a/src/domain/storage/Namespaces.ts b/src/data/storage/Namespaces.ts similarity index 100% rename from src/domain/storage/Namespaces.ts rename to src/data/storage/Namespaces.ts diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index 5c6f962bf..05162a017 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,5 +1,5 @@ import { Instance } from "../../domain/instance/entities/Instance"; -import { Namespace } from "../../domain/storage/Namespaces"; +import { Namespace } from "../storage/Namespaces"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; diff --git a/src/domain/instance/usecases/DeleteInstanceUseCase.ts b/src/domain/instance/usecases/DeleteInstanceUseCase.ts index 78adbfd98..e6030df0e 100644 --- a/src/domain/instance/usecases/DeleteInstanceUseCase.ts +++ b/src/domain/instance/usecases/DeleteInstanceUseCase.ts @@ -1,7 +1,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance } from "../entities/Instance"; diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index 1a84c6220..380027f4c 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -1,7 +1,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance, InstanceData } from "../entities/Instance"; import { Either } from "../../common/entities/Either"; diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index 3fa1f56e3..7bc12ecfa 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance, InstanceData } from "../entities/Instance"; diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index 9bcdb862a..4f259787d 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance } from "../entities/Instance"; diff --git a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts index f2b9dcb8d..7be4cd710 100644 --- a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts +++ b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts @@ -1,6 +1,6 @@ import { UseCase } from "../../common/entities/UseCase"; import { Instance } from "../../instance/entities/Instance"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore, MappingOwner } from "../entities/MappingOwner"; diff --git a/src/domain/mapping/usecases/SaveMappingUseCase.ts b/src/domain/mapping/usecases/SaveMappingUseCase.ts index 296532274..4983caf4a 100644 --- a/src/domain/mapping/usecases/SaveMappingUseCase.ts +++ b/src/domain/mapping/usecases/SaveMappingUseCase.ts @@ -1,7 +1,7 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore } from "../entities/MappingOwner"; diff --git a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts index d322e43de..0404072e2 100644 --- a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; diff --git a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts index 6484b7ece..5b3c49a09 100644 --- a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts @@ -3,7 +3,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataResponsible } from "../entities/MetadataResponsible"; diff --git a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts index 001987ab6..059eb6583 100644 --- a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts @@ -6,7 +6,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { ReceivedPullRequestNotification } from "../../notifications/entities/PullRequestNotification"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; diff --git a/src/domain/modules/usecases/DeleteModuleUseCase.ts b/src/domain/modules/usecases/DeleteModuleUseCase.ts index 7ee2f8014..ffdf847ab 100644 --- a/src/domain/modules/usecases/DeleteModuleUseCase.ts +++ b/src/domain/modules/usecases/DeleteModuleUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BasePackage, Package } from "../../packages/entities/Package"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; export class DeleteModuleUseCase implements UseCase { diff --git a/src/domain/modules/usecases/GetModuleUseCase.ts b/src/domain/modules/usecases/GetModuleUseCase.ts index cec276739..143f031d9 100644 --- a/src/domain/modules/usecases/GetModuleUseCase.ts +++ b/src/domain/modules/usecases/GetModuleUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index 4f7699d7a..e5645bdc6 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; diff --git a/src/domain/modules/usecases/SaveModuleUseCase.ts b/src/domain/modules/usecases/SaveModuleUseCase.ts index c5500e6f0..0464f8584 100644 --- a/src/domain/modules/usecases/SaveModuleUseCase.ts +++ b/src/domain/modules/usecases/SaveModuleUseCase.ts @@ -5,7 +5,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Module } from "../entities/Module"; diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index 84e3e1b56..d86d3bda4 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -4,7 +4,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; import { diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index e8b229828..94a4f1668 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -11,7 +11,7 @@ import { MetadataRepositoryConstructor, } from "../../metadata/repositories/MetadataRepository"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 42009f851..5ac524bb7 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -5,7 +5,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; diff --git a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts index 63e12ba64..d15b92e77 100644 --- a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; diff --git a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts index ba4ddcebd..cbc320eb3 100644 --- a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts +++ b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts @@ -7,7 +7,7 @@ import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { PullRequestStatus, diff --git a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts index aa8b33be1..4ee8370fd 100644 --- a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts @@ -3,7 +3,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackageData } from "../entities/ImportedPackage"; diff --git a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts index 6edc02071..dace093df 100644 --- a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts @@ -3,7 +3,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackage } from "../entities/ImportedPackage"; diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index 96a3f44bf..836798c97 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -11,7 +11,7 @@ import { InstanceRepositoryConstructor } from "../../instance/repositories/Insta import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Module } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { Package } from "../entities/Package"; diff --git a/src/domain/packages/usecases/DeletePackageUseCase.ts b/src/domain/packages/usecases/DeletePackageUseCase.ts index 639dcaf3a..ce06a87ed 100644 --- a/src/domain/packages/usecases/DeletePackageUseCase.ts +++ b/src/domain/packages/usecases/DeletePackageUseCase.ts @@ -2,7 +2,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index c8fe32da7..089ebc7c3 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -4,7 +4,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { getMetadataPackageDiff, MetadataPackageDiff } from "../entities/MetadataPackageDiff"; import { Store } from "../../stores/entities/Store"; diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index 6f75e5eff..594efe404 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -6,7 +6,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; diff --git a/src/domain/packages/usecases/GetPackageUseCase.ts b/src/domain/packages/usecases/GetPackageUseCase.ts index 52c340914..153f149c9 100644 --- a/src/domain/packages/usecases/GetPackageUseCase.ts +++ b/src/domain/packages/usecases/GetPackageUseCase.ts @@ -3,7 +3,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; diff --git a/src/domain/packages/usecases/GetStorePackageUseCase.ts b/src/domain/packages/usecases/GetStorePackageUseCase.ts index c87c5f293..4556d0851 100644 --- a/src/domain/packages/usecases/GetStorePackageUseCase.ts +++ b/src/domain/packages/usecases/GetStorePackageUseCase.ts @@ -6,7 +6,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; import { Store } from "../../stores/entities/Store"; diff --git a/src/domain/packages/usecases/ListPackagesUseCase.ts b/src/domain/packages/usecases/ListPackagesUseCase.ts index 3d68beab0..3db712790 100644 --- a/src/domain/packages/usecases/ListPackagesUseCase.ts +++ b/src/domain/packages/usecases/ListPackagesUseCase.ts @@ -5,7 +5,7 @@ import { Instance } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataModule } from "../../modules/entities/MetadataModule"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index 69d67e4f3..e3bca3a46 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -10,7 +10,7 @@ import { InstanceRepositoryConstructor } from "../../instance/repositories/Insta import { MetadataModule } from "../../modules/entities/MetadataModule"; import { BaseModule } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { GitHubError, GitHubListError } from "../entities/Errors"; import { ListPackage, Package } from "../entities/Package"; diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index 765006fd3..46b1b7ab3 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -6,7 +6,7 @@ import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BaseModule } from "../../modules/entities/Module"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { GitHubError } from "../entities/Errors"; import { BasePackage } from "../entities/Package"; diff --git a/src/domain/storage/repositories/StorageClient.ts b/src/domain/storage/repositories/StorageClient.ts index cd733aab5..95ef45f45 100644 --- a/src/domain/storage/repositories/StorageClient.ts +++ b/src/domain/storage/repositories/StorageClient.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { Ref } from "../../common/entities/Ref"; import { Instance } from "../../instance/entities/Instance"; -import { Namespace, NamespaceProperties } from "../Namespaces"; +import { Namespace, NamespaceProperties } from "../../../data/storage/Namespaces"; export interface StorageRepositoryConstructor { new (instance: Instance): StorageClient; diff --git a/src/domain/stores/usecases/GetStoreUseCase.ts b/src/domain/stores/usecases/GetStoreUseCase.ts index 9ee8b8548..e8cfeb15e 100644 --- a/src/domain/stores/usecases/GetStoreUseCase.ts +++ b/src/domain/stores/usecases/GetStoreUseCase.ts @@ -1,5 +1,5 @@ import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { Store } from "../entities/Store"; diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index 377f95d9c..d09587683 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -1,7 +1,7 @@ import { generateUid } from "d2/uid"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { GitHubError } from "../../packages/entities/Errors"; import { Store } from "../entities/Store"; diff --git a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts index e4fcc3991..e9e6b755c 100644 --- a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts +++ b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts @@ -1,6 +1,6 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { Store } from "../entities/Store"; diff --git a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts index b3b3ffede..f95af318b 100644 --- a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts +++ b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts @@ -13,7 +13,7 @@ import { SentPullRequestNotification, } from "../../notifications/entities/PullRequestNotification"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 1ed3726b1..ffd74535d 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -23,7 +23,7 @@ import { MetadataRepositoryConstructor } from "../../metadata/repositories/Metad import { DeletedMetadataSyncUseCase } from "../../metadata/usecases/DeletedMetadataSyncUseCase"; import { MetadataSyncUseCase } from "../../metadata/usecases/MetadataSyncUseCase"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 6ec28fb7b..5125e5822 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -7,7 +7,7 @@ import { Instance, InstanceData } from "../../instance/entities/Instance"; import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; import { Repositories } from "../../Repositories"; -import { Namespace } from "../../storage/Namespaces"; +import { Namespace } from "../../../data/storage/Namespaces"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; diff --git a/src/models/syncReport.ts b/src/models/syncReport.ts index a7eefe2b8..b9802a5d3 100644 --- a/src/models/syncReport.ts +++ b/src/models/syncReport.ts @@ -1,7 +1,7 @@ import { TableInitialState, TablePagination } from "d2-ui-components"; import { generateUid } from "d2/uid"; import _ from "lodash"; -import { Namespace } from "../domain/storage/Namespaces"; +import { Namespace } from "../data/storage/Namespaces"; import { SynchronizationReport, SynchronizationReportStatus, From 2c5146c1212c6ca7c3d0e51c3fb0bc1b0d27030c Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 23 Nov 2020 09:39:03 +0100 Subject: [PATCH 008/163] Add missing use cases --- src/data/stores/StoreD2ApiRepository.ts | 27 ++++++++++++++---- .../stores/repositories/StoreRepository.ts | 5 ++-- src/domain/stores/usecases/GetStoreUseCase.ts | 9 ++---- .../stores/usecases/SaveStoreUseCase.ts | 28 ++++--------------- .../usecases/SetStoreAsDefaultUseCase.ts | 15 ++-------- src/presentation/CompositionRoot.ts | 7 ++--- .../webapp/pages/store-list/StoreListPage.tsx | 2 +- 7 files changed, 39 insertions(+), 54 deletions(-) diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index 05162a017..bc12b356e 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -4,6 +4,7 @@ import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; +import { generateUid } from "d2/uid"; export class StoreD2ApiRepository implements StoreRepository { private storageClient: StorageClient; @@ -17,13 +18,13 @@ export class StoreD2ApiRepository implements StoreRepository { return stores.filter(store => !store.deleted); } - public async getById(_id: string): Promise { - throw new Error("Method not implemented."); + public async getById(id: string): Promise { + const stores = await this.storageClient.listObjectsInCollection(Namespace.STORES); + return stores.find(store => store.id === id); } public async delete(id: string): Promise { const store = await this.storageClient.getObjectInCollection(Namespace.STORES, id); - if (!store) return false; await this.storageClient.saveObjectInCollection(Namespace.STORES, { @@ -35,10 +36,24 @@ export class StoreD2ApiRepository implements StoreRepository { } public async save(store: Store): Promise { - await this.storageClient.saveObjectInCollection(Namespace.STORES, store); + const currentStores = await this.list(); + const isFirstStore = !store.id && currentStores.length === 0; + + await this.storageClient.saveObjectInCollection(Namespace.STORES, { + ...store, + id: store.id || generateUid(), + default: isFirstStore || store.default, + }); + } + + public async getDefault(): Promise { + const stores = await this.list(); + return stores.find(store => store.default); } - public async getDefault(): Promise { - throw new Error("Method not implemented."); + public async setDefault(id: string): Promise { + const stores = await this.list(); + const newStores = stores.map(store => ({ ...store, default: store.id === id })); + await this.storageClient.saveObject(Namespace.STORES, newStores); } } diff --git a/src/domain/stores/repositories/StoreRepository.ts b/src/domain/stores/repositories/StoreRepository.ts index 4aa6f86e8..08b0bd576 100644 --- a/src/domain/stores/repositories/StoreRepository.ts +++ b/src/domain/stores/repositories/StoreRepository.ts @@ -2,8 +2,9 @@ import { Store } from "../entities/Store"; export interface StoreRepository { list(): Promise; - getById(id: string): Promise; + getById(id: string): Promise; delete(id: string): Promise; save(store: Store): Promise; - getDefault(): Promise; + getDefault(): Promise; + setDefault(id: string): Promise; } diff --git a/src/domain/stores/usecases/GetStoreUseCase.ts b/src/domain/stores/usecases/GetStoreUseCase.ts index e8cfeb15e..0c73fbc12 100644 --- a/src/domain/stores/usecases/GetStoreUseCase.ts +++ b/src/domain/stores/usecases/GetStoreUseCase.ts @@ -1,14 +1,11 @@ import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageClient } from "../../storage/repositories/StorageClient"; import { Store } from "../entities/Store"; +import { StoreRepository } from "../repositories/StoreRepository"; export class GetStoreUseCase implements UseCase { - constructor(private storageRepository: StorageClient) {} + constructor(private storeRepository: StoreRepository) {} public async execute(id: string): Promise { - const store = this.storageRepository.getObjectInCollection(Namespace.STORES, id); - - return store; + return this.storeRepository.getById(id); } } diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index d09587683..c609d2f7a 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -1,16 +1,14 @@ -import { generateUid } from "d2/uid"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageClient } from "../../storage/repositories/StorageClient"; import { GitHubError } from "../../packages/entities/Errors"; -import { Store } from "../entities/Store"; import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; +import { Store } from "../entities/Store"; +import { StoreRepository } from "../repositories/StoreRepository"; export class SaveStoreUseCase implements UseCase { constructor( private githubRepository: GitHubRepository, - private storageRepository: StorageClient + private storeRepository: StoreRepository ) {} public async execute(store: Store, validate = true): Promise> { @@ -19,24 +17,8 @@ export class SaveStoreUseCase implements UseCase { if (validation.isError()) return Either.error(validation.value.error ?? "UNKNOWN"); } - const isFirstStore = await this.isFirstStore(store); - - const storeToSave = { - ...store, - id: store.id || generateUid(), - default: isFirstStore ? true : store.default, - }; - - await this.storageRepository.saveObjectInCollection(Namespace.STORES, storeToSave); - - return Either.success(storeToSave); - } - - private async isFirstStore(store: Store) { - const currentStores = ( - await this.storageRepository.getObject(Namespace.STORES) - )?.filter(store => !store.deleted); + await this.storeRepository.save(store); - return !store.id && (!currentStores || currentStores.length === 0); + return Either.success(store); } } diff --git a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts index e9e6b755c..82feb4df6 100644 --- a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts +++ b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts @@ -1,26 +1,17 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageClient } from "../../storage/repositories/StorageClient"; -import { Store } from "../entities/Store"; +import { StoreRepository } from "../repositories/StoreRepository"; type SetStoreAsDefaultError = { kind: "SetStoreAsDefaultError"; }; export class SetStoreAsDefaultUseCase implements UseCase { - constructor(private storageRepository: StorageClient) {} + constructor(private storeRepository: StoreRepository) {} public async execute(id: string): Promise> { try { - const stores = await this.storageRepository.listObjectsInCollection( - Namespace.STORES - ); - - const newStores = stores.map(store => ({ ...store, default: store.id === id })); - - await this.storageRepository.saveObject(Namespace.STORES, newStores); - + await this.storeRepository.setDefault(id); return Either.success(undefined); } catch { return Either.error({ diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 734486177..df3822485 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -162,16 +162,15 @@ export class CompositionRoot { @cache() public get store() { const github = new GitHubOctokitRepository(); - const storage = new StorageDataStoreClient(this.localInstance); const storeRepository = new StoreD2ApiRepository(this.localInstance); return getExecute({ - get: new GetStoreUseCase(storage), - update: new SaveStoreUseCase(github, storage), + get: new GetStoreUseCase(storeRepository), + update: new SaveStoreUseCase(github, storeRepository), validate: new ValidateStoreUseCase(github), list: new ListStoresUseCase(storeRepository), delete: new DeleteStoreUseCase(storeRepository), - setAsDefault: new SetStoreAsDefaultUseCase(storage), + setAsDefault: new SetStoreAsDefaultUseCase(storeRepository), }); } diff --git a/src/presentation/webapp/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/pages/store-list/StoreListPage.tsx index 59efed840..55c35c29c 100644 --- a/src/presentation/webapp/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/pages/store-list/StoreListPage.tsx @@ -155,7 +155,7 @@ export const StoreListPage: React.FC = () => { icon: delete, }, { - name: "setAdDefault", + name: "setAsDefault", text: i18n.t("Set as default"), multiple: false, onClick: (ids: string[]) => handleSetStoreAsDefault(ids[0]), From 4bab87203ea4cb173dc7bacb2a0c9e78e96dc70b Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 24 Nov 2020 09:30:57 +0100 Subject: [PATCH 009/163] Unify most use cases into a common class structure --- src/domain/Repositories.ts | 1 + src/domain/common/entities/UseCase.ts | 106 ++++++++++++++++++ .../usecases/DeleteInstanceUseCase.ts | 22 ++-- .../usecases/GetInstanceApiUseCase.ts | 17 +-- .../usecases/GetInstanceByIdUseCase.ts | 28 ++--- .../usecases/GetInstanceVersionUseCase.ts | 17 +-- .../usecases/GetRootOrgUnitUseCase.ts | 17 +-- .../instance/usecases/GetUserGroupsUseCase.ts | 17 +-- .../instance/usecases/ListInstancesUseCase.ts | 25 ++--- .../instance/usecases/SaveInstanceUseCase.ts | 21 ++-- .../usecases/ValidateInstanceUseCase.ts | 16 +-- .../mapping/usecases/GenericMappingUseCase.ts | 44 ++------ .../usecases/GetResponsiblesUseCase.ts | 23 ++-- .../usecases/ImportMetadataUseCase.ts | 29 ++--- .../usecases/ListAllMetadataUseCase.ts | 36 ++---- .../metadata/usecases/ListMetadataUseCase.ts | 37 ++---- .../usecases/ListResponsiblesUseCase.ts | 36 ++---- .../usecases/SetResponsiblesUseCase.ts | 36 +++--- .../modules/usecases/DeleteModuleUseCase.ts | 35 ++---- .../usecases/DownloadModuleSnapshotUseCase.ts | 25 ++--- .../modules/usecases/GetModuleUseCase.ts | 19 ++-- .../modules/usecases/ListModulesUseCase.ts | 30 +---- .../modules/usecases/SaveModuleUseCase.ts | 32 ++---- .../usecases/CancelPullRequestUseCase.ts | 25 ++--- .../usecases/ImportPullRequestUseCase.ts | 49 ++------ .../usecases/ListNotificationsUseCase.ts | 31 ++--- .../usecases/MarkReadNotificationsUseCase.ts | 42 +++---- .../UpdatePullRequestStatusUseCase.ts | 30 +---- .../usecases/ListImportedPackagesUseCase.ts | 23 ++-- .../usecases/SaveImportedPackagesUseCase.ts | 19 ++-- .../packages/usecases/CreatePackageUseCase.ts | 50 +++------ .../packages/usecases/DeletePackageUseCase.ts | 21 ++-- .../packages/usecases/DiffPackageUseCase.ts | 38 ++----- .../usecases/DownloadPackageUseCase.ts | 45 ++------ .../packages/usecases/GetPackageUseCase.ts | 19 ++-- .../usecases/GetStorePackageUseCase.ts | 35 +----- .../packages/usecases/ImportPackageUseCase.ts | 38 ++----- .../packages/usecases/ListPackagesUseCase.ts | 30 +---- .../usecases/ListStorePackagesUseCase.ts | 46 ++------ .../usecases/PublishStorePackageUseCase.ts | 50 +++------ .../stores/repositories/StoreRepository.ts | 5 + .../usecases/CreatePullRequestUseCase.ts | 30 +---- .../usecases/GenericSyncUseCase.ts | 56 +++------ .../usecases/PrepareSyncUseCase.ts | 50 +++------ 44 files changed, 477 insertions(+), 924 deletions(-) diff --git a/src/domain/Repositories.ts b/src/domain/Repositories.ts index a0b80cb18..6b8a63d93 100644 --- a/src/domain/Repositories.ts +++ b/src/domain/Repositories.ts @@ -1,5 +1,6 @@ export const Repositories = { InstanceRepository: "instanceRepository", + StoreRepository: "storeRepository", StorageRepository: "storageRepository", DownloadRepository: "downloadRepository", GitHubRepository: "githubRepository", diff --git a/src/domain/common/entities/UseCase.ts b/src/domain/common/entities/UseCase.ts index 84d48700e..c7001ca68 100644 --- a/src/domain/common/entities/UseCase.ts +++ b/src/domain/common/entities/UseCase.ts @@ -1,3 +1,109 @@ +import { cache } from "../../../utils/cache"; +import { + AggregatedRepository, + AggregatedRepositoryConstructor, +} from "../../aggregated/repositories/AggregatedRepository"; +import { + EventsRepository, + EventsRepositoryConstructor, +} from "../../events/repositories/EventsRepository"; +import { DataSource } from "../../instance/entities/DataSource"; +import { Instance } from "../../instance/entities/Instance"; +import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; +import { + MetadataRepository, + MetadataRepositoryConstructor, +} from "../../metadata/repositories/MetadataRepository"; +import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; +import { Repositories } from "../../Repositories"; +import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; +import { + TransformationRepository, + TransformationRepositoryConstructor, +} from "../../transformations/repositories/TransformationRepository"; +import { RepositoryFactory } from "../factories/RepositoryFactory"; + export interface UseCase { execute: Function; } + +export abstract class DefaultUseCase { + constructor(protected repositoryFactory: RepositoryFactory) {} + + @cache() + protected gitRepository() { + return this.repositoryFactory.get( + Repositories.GitHubRepository, + [] + ); + } + + @cache() + protected storageRepository(instance: Instance) { + return this.repositoryFactory.get( + Repositories.StorageRepository, + [instance] + ); + } + + @cache() + protected downloadRepository() { + return this.repositoryFactory.get( + Repositories.DownloadRepository, + [] + ); + } + + @cache() + protected storeRepository(instance: Instance) { + return this.repositoryFactory.get( + Repositories.StoreRepository, + [instance] + ); + } + + @cache() + protected instanceRepository(instance: Instance) { + return this.repositoryFactory.get( + Repositories.InstanceRepository, + [instance, ""] + ); + } + + @cache() + protected transformationRepository(): TransformationRepository { + return this.repositoryFactory.get( + Repositories.TransformationRepository, + [] + ); + } + + @cache() + protected metadataRepository(instance: DataSource): MetadataRepository { + const tag = instance.type === "json" ? "json" : undefined; + + return this.repositoryFactory.get( + Repositories.MetadataRepository, + [instance, this.transformationRepository()], + tag + ); + } + + @cache() + protected aggregatedRepository(instance: Instance): AggregatedRepository { + return this.repositoryFactory.get( + Repositories.AggregatedRepository, + [instance] + ); + } + + @cache() + protected eventsRepository(instance: Instance): EventsRepository { + return this.repositoryFactory.get( + Repositories.EventsRepository, + [instance] + ); + } +} diff --git a/src/domain/instance/usecases/DeleteInstanceUseCase.ts b/src/domain/instance/usecases/DeleteInstanceUseCase.ts index e6030df0e..2d82c125f 100644 --- a/src/domain/instance/usecases/DeleteInstanceUseCase.ts +++ b/src/domain/instance/usecases/DeleteInstanceUseCase.ts @@ -1,21 +1,19 @@ -import { UseCase } from "../../common/entities/UseCase"; -import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class DeleteInstanceUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class DeleteInstanceUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(id: string): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - try { - await storageRepository.removeObjectInCollection(Namespace.INSTANCES, id); + await this.storageRepository(this.localInstance).removeObjectInCollection( + Namespace.INSTANCES, + id + ); } catch (error) { console.error(error); return false; diff --git a/src/domain/instance/usecases/GetInstanceApiUseCase.ts b/src/domain/instance/usecases/GetInstanceApiUseCase.ts index 35e09e39e..94a54f2ad 100644 --- a/src/domain/instance/usecases/GetInstanceApiUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceApiUseCase.ts @@ -1,19 +1,14 @@ import { D2Api } from "../../../types/d2-api"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Instance } from "../entities/Instance"; -import { InstanceRepositoryConstructor } from "../repositories/InstanceRepository"; -export class GetInstanceApiUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetInstanceApiUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public execute(instance = this.localInstance): D2Api { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - - return instanceRepository.getApi(); + return this.instanceRepository(instance).getApi(); } } diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index 380027f4c..74bf74850 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -1,30 +1,24 @@ -import { UseCase } from "../../common/entities/UseCase"; -import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -import { Instance, InstanceData } from "../entities/Instance"; import { Either } from "../../common/entities/Either"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance, InstanceData } from "../entities/Instance"; -export class GetInstanceByIdUseCase implements UseCase { +export class GetInstanceByIdUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute(id: string): Promise> { if (id === "LOCAL") return Either.success(this.localInstance); - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const data = await storageRepository.getObjectInCollection( - Namespace.INSTANCES, - id - ); + const data = await this.storageRepository(this.localInstance).getObjectInCollection< + InstanceData + >(Namespace.INSTANCES, id); if (!data) return Either.error("NOT_FOUND"); diff --git a/src/domain/instance/usecases/GetInstanceVersionUseCase.ts b/src/domain/instance/usecases/GetInstanceVersionUseCase.ts index 1d37bcb93..5f0e25fd2 100644 --- a/src/domain/instance/usecases/GetInstanceVersionUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceVersionUseCase.ts @@ -1,19 +1,14 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Instance } from "../entities/Instance"; -import { InstanceRepositoryConstructor } from "../repositories/InstanceRepository"; -export class GetInstanceVersionUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetInstanceVersionUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(instance = this.localInstance): Promise { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - - const buildVersion = await instanceRepository.getVersion(); + const buildVersion = await this.instanceRepository(instance).getVersion(); const [major, minor] = buildVersion.split("."); return `${major}.${minor}`; } diff --git a/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts b/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts index ac16d2b88..8bc421258 100644 --- a/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts +++ b/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts @@ -1,18 +1,13 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Instance } from "../entities/Instance"; -import { InstanceRepositoryConstructor } from "../repositories/InstanceRepository"; -export class GetRootOrgUnitUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetRootOrgUnitUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(instance = this.localInstance) { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - - return instanceRepository.getOrgUnitRoots(); + return this.instanceRepository(instance).getOrgUnitRoots(); } } diff --git a/src/domain/instance/usecases/GetUserGroupsUseCase.ts b/src/domain/instance/usecases/GetUserGroupsUseCase.ts index db0291410..65c3aa47b 100644 --- a/src/domain/instance/usecases/GetUserGroupsUseCase.ts +++ b/src/domain/instance/usecases/GetUserGroupsUseCase.ts @@ -1,20 +1,15 @@ import _ from "lodash"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Instance } from "../entities/Instance"; -import { InstanceRepositoryConstructor } from "../repositories/InstanceRepository"; -export class GetUserGroupsUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetUserGroupsUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(instance = this.localInstance) { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - - const userGroups = await instanceRepository.getUserGroups(); + const userGroups = await this.instanceRepository(instance).getUserGroups(); return _.sortBy(userGroups, "name"); } } diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index 7bc12ecfa..4486dce9a 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -1,31 +1,26 @@ import _ from "lodash"; -import { UseCase } from "../../common/entities/UseCase"; -import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../entities/Instance"; export interface ListInstancesUseCaseProps { search?: string; } -export class ListInstancesUseCase implements UseCase { +export class ListInstancesUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute({ search }: ListInstancesUseCaseProps = {}): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const objects = await storageRepository.listObjectsInCollection( - Namespace.INSTANCES - ); + const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< + InstanceData + >(Namespace.INSTANCES); const filteredData = search ? _.filter(objects, o => diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index 4f259787d..e5d0445f4 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -1,28 +1,23 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Instance } from "../entities/Instance"; -export class SaveInstanceUseCase implements UseCase { +export class SaveInstanceUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute(instance: Instance): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - const validations = instance.validate(); if (validations.length === 0) { - await storageRepository.saveObjectInCollection( + await this.storageRepository(this.localInstance).saveObjectInCollection( Namespace.INSTANCES, instance.encryptPassword(this.encryptionKey).toObject() ); diff --git a/src/domain/instance/usecases/ValidateInstanceUseCase.ts b/src/domain/instance/usecases/ValidateInstanceUseCase.ts index 7d4f62280..62c8a44a1 100644 --- a/src/domain/instance/usecases/ValidateInstanceUseCase.ts +++ b/src/domain/instance/usecases/ValidateInstanceUseCase.ts @@ -1,25 +1,15 @@ import i18n from "../../../locales"; import { debug } from "../../../utils/debug"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; -import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Repositories } from "../../Repositories"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { DataSource, isJSONDataSource } from "../entities/DataSource"; -import { InstanceRepositoryConstructor } from "../repositories/InstanceRepository"; - -export class ValidateInstanceUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory) {} +export class ValidateInstanceUseCase extends DefaultUseCase implements UseCase { public async execute(instance: DataSource): Promise> { if (isJSONDataSource(instance)) return Either.success(undefined); try { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - - const version = await instanceRepository.getVersion(); + const version = await this.instanceRepository(instance).getVersion(); if (version) { return Either.success(undefined); diff --git a/src/domain/mapping/usecases/GenericMappingUseCase.ts b/src/domain/mapping/usecases/GenericMappingUseCase.ts index 39a7fc27e..91321d438 100644 --- a/src/domain/mapping/usecases/GenericMappingUseCase.ts +++ b/src/domain/mapping/usecases/GenericMappingUseCase.ts @@ -5,28 +5,24 @@ import { } from "../../../presentation/react/components/mapping-table/utils"; import { Dictionary } from "../../../types/utils"; import { NamedRef } from "../../common/entities/Ref"; +import { DefaultUseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; -import { - MetadataRepository, - MetadataRepositoryConstructor, -} from "../../metadata/repositories/MetadataRepository"; -import { Repositories } from "../../Repositories"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataMapping, MetadataMappingDictionary } from "../entities/MetadataMapping"; -export abstract class GenericMappingUseCase { - constructor( - protected repositoryFactory: RepositoryFactory, - protected localInstance: Instance - ) {} +export abstract class GenericMappingUseCase extends DefaultUseCase { + constructor(repositoryFactory: RepositoryFactory, protected localInstance: Instance) { + super(repositoryFactory); + } protected async getMetadata(instance: DataSource, ids: string[]) { - return this.getMetadataRepository(instance).getMetadataByIds< - Omit - >(ids, fields, true); + return this.metadataRepository(instance).getMetadataByIds>( + ids, + fields, + true + ); } protected createMetadataDictionary(metadata: MetadataPackage) { @@ -48,22 +44,6 @@ export abstract class GenericMappingUseCase { return _.values(dictionary); } - protected getMetadataRepository( - remoteInstance: DataSource = this.localInstance - ): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - const tag = remoteInstance.type === "json" ? "json" : undefined; - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance, transformationRepository], - tag - ); - } - protected async buildMapping({ metadata, originInstance, @@ -152,7 +132,7 @@ export abstract class GenericMappingUseCase { const programStages = this.getProgramStages(metadata[0]); const programStageDataElements = this.getProgramStageDataElements(metadata[0]); - const defaultValues = await this.getMetadataRepository(instance).getDefaultIds(); + const defaultValues = await this.metadataRepository(instance).getDefaultIds(); return _.union(categoryOptions, options, programStages, programStageDataElements) .map(({ id }) => id) @@ -178,7 +158,7 @@ export abstract class GenericMappingUseCase { const selectedItem = originMetadata[selectedItemId]; if (!selectedItem) return []; - const destinationMetadata = await this.getMetadataRepository( + const destinationMetadata = await this.metadataRepository( destinationInstance ).lookupSimilar(selectedItem); diff --git a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts index 0404072e2..159fc75a1 100644 --- a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts @@ -1,26 +1,21 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -export class GetResponsiblesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetResponsiblesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( ids: string[], instance = this.localInstance ): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - - const items = await storageRepository.listObjectsInCollection( - Namespace.RESPONSIBLES - ); + const items = await this.storageRepository(instance).listObjectsInCollection< + MetadataResponsible + >(Namespace.RESPONSIBLES); return items.filter(({ id }) => ids.includes(id)); } diff --git a/src/domain/metadata/usecases/ImportMetadataUseCase.ts b/src/domain/metadata/usecases/ImportMetadataUseCase.ts index fe3b0613c..525ab31b0 100644 --- a/src/domain/metadata/usecases/ImportMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ImportMetadataUseCase.ts @@ -1,33 +1,18 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataPackage } from "../entities/MetadataEntities"; -import { - MetadataRepository, - MetadataRepositoryConstructor, -} from "../repositories/MetadataRepository"; -export class ImportMetadataUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ImportMetadataUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( payload: MetadataPackage, - instance?: Instance + instance = this.localInstance ): Promise { - return this.getMetadataRepository(instance).save(payload); - } - - private getMetadataRepository(remoteInstance = this.localInstance): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance, transformationRepository] - ); + return this.metadataRepository(instance).save(payload); } } diff --git a/src/domain/metadata/usecases/ListAllMetadataUseCase.ts b/src/domain/metadata/usecases/ListAllMetadataUseCase.ts index f013ecd4e..8bdc91c97 100644 --- a/src/domain/metadata/usecases/ListAllMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ListAllMetadataUseCase.ts @@ -1,39 +1,19 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataEntity } from "../entities/MetadataEntities"; -import { - ListMetadataParams, - MetadataRepository, - MetadataRepositoryConstructor, -} from "../repositories/MetadataRepository"; +import { ListMetadataParams } from "../repositories/MetadataRepository"; -export class ListAllMetadataUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListAllMetadataUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( params: ListMetadataParams, - instance?: DataSource + instance: DataSource = this.localInstance ): Promise { - return this.getMetadataRepository(instance).listAllMetadata(params); - } - - private getMetadataRepository( - remoteInstance: DataSource = this.localInstance - ): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - const tag = remoteInstance.type === "json" ? "json" : undefined; - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance, transformationRepository], - tag - ); + return this.metadataRepository(instance).listAllMetadata(params); } } diff --git a/src/domain/metadata/usecases/ListMetadataUseCase.ts b/src/domain/metadata/usecases/ListMetadataUseCase.ts index 5c2ea898d..d4dfb42ca 100644 --- a/src/domain/metadata/usecases/ListMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ListMetadataUseCase.ts @@ -1,39 +1,18 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; -import { - ListMetadataParams, - ListMetadataResponse, - MetadataRepository, - MetadataRepositoryConstructor, -} from "../repositories/MetadataRepository"; +import { ListMetadataParams, ListMetadataResponse } from "../repositories/MetadataRepository"; -export class ListMetadataUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListMetadataUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( params: ListMetadataParams, - instance?: DataSource + instance: DataSource = this.localInstance ): Promise { - return this.getMetadataRepository(instance).listMetadata(params); - } - - private getMetadataRepository( - remoteInstance: DataSource = this.localInstance - ): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - const tag = remoteInstance.type === "json" ? "json" : undefined; - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance, transformationRepository], - tag - ); + return this.metadataRepository(instance).listMetadata(params); } } diff --git a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts index 5b3c49a09..d3b9067c6 100644 --- a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts @@ -1,26 +1,19 @@ import _ from "lodash"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -import { MetadataRepositoryConstructor } from "../repositories/MetadataRepository"; -export class ListResponsiblesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListResponsiblesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(instance = this.localInstance): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - - const items = await storageRepository.listObjectsInCollection( - Namespace.RESPONSIBLES - ); + const items = await this.storageRepository(instance).listObjectsInCollection< + MetadataResponsible + >(Namespace.RESPONSIBLES); const names = await this.getDisplayNames( instance, @@ -31,16 +24,7 @@ export class ListResponsiblesUseCase implements UseCase { } private async getDisplayNames(instance: Instance, ids: string[]) { - const transformationsRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - const metadataRepository = this.repositoryFactory.get( - Repositories.MetadataRepository, - [instance, transformationsRepository] - ); - - const metadata = await metadataRepository.getMetadataByIds<{ + const metadata = await this.metadataRepository(instance).getMetadataByIds<{ id: string; displayName: string; }>(ids, "id,displayName"); diff --git a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts index 059eb6583..153e26a3e 100644 --- a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts @@ -1,25 +1,27 @@ import _ from "lodash"; -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { ReceivedPullRequestNotification } from "../../notifications/entities/PullRequestNotification"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -export class SetResponsiblesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class SetResponsiblesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(responsible: MetadataResponsible): Promise { const { id, users, userGroups } = responsible; if (users.length === 0 && userGroups.length === 0) { - await this.storageRepository.removeObjectInCollection(Namespace.RESPONSIBLES, id); + await this.storageRepository(this.localInstance).removeObjectInCollection( + Namespace.RESPONSIBLES, + id + ); } else { - await this.storageRepository.saveObjectInCollection( + await this.storageRepository(this.localInstance).saveObjectInCollection( Namespace.RESPONSIBLES, responsible ); @@ -28,22 +30,14 @@ export class SetResponsiblesUseCase implements UseCase { await this.updatePendingPullRequests(responsible); } - @cache() - private get storageRepository() { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - } - private async updatePendingPullRequests({ id, users, userGroups, }: MetadataResponsible): Promise { - const notifications = await this.storageRepository.listObjectsInCollection< - ReceivedPullRequestNotification - >(Namespace.NOTIFICATIONS); + const notifications = await this.storageRepository( + this.localInstance + ).listObjectsInCollection(Namespace.NOTIFICATIONS); const relatedPullRequests = notifications.filter( ({ type, selectedIds }) => type === "received-pull-request" && selectedIds.includes(id) @@ -57,7 +51,7 @@ export class SetResponsiblesUseCase implements UseCase { userGroups: _.uniqBy([...notification.userGroups, ...userGroups], "id"), }; - await this.storageRepository.saveObjectInCollection( + await this.storageRepository(this.localInstance).saveObjectInCollection( Namespace.NOTIFICATIONS, newNotification ); diff --git a/src/domain/modules/usecases/DeleteModuleUseCase.ts b/src/domain/modules/usecases/DeleteModuleUseCase.ts index ffdf847ab..80bdfdb41 100644 --- a/src/domain/modules/usecases/DeleteModuleUseCase.ts +++ b/src/domain/modules/usecases/DeleteModuleUseCase.ts @@ -1,21 +1,18 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BasePackage, Package } from "../../packages/entities/Package"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -export class DeleteModuleUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class DeleteModuleUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(id: string, instance = this.localInstance): Promise { try { - await this.buildStorageRepository(instance).removeObjectInCollection( - Namespace.MODULES, - id - ); + await this.storageRepository(instance).removeObjectInCollection(Namespace.MODULES, id); await this.deletePackagesFromModule(id, instance); } catch (error) { return false; @@ -25,9 +22,9 @@ export class DeleteModuleUseCase implements UseCase { } private async deletePackagesFromModule(id: string, instance: Instance): Promise { - const packages = await this.buildStorageRepository(instance).listObjectsInCollection< - Package - >(Namespace.PACKAGES); + const packages = await this.storageRepository(instance).listObjectsInCollection( + Namespace.PACKAGES + ); const newPackages = packages .filter(({ module }) => module.id === id) @@ -37,17 +34,7 @@ export class DeleteModuleUseCase implements UseCase { })); await promiseMap(newPackages, async (item: BasePackage) => { - await this.buildStorageRepository(instance).saveObjectInCollection( - Namespace.PACKAGES, - item - ); + await this.storageRepository(instance).saveObjectInCollection(Namespace.PACKAGES, item); }); } - - private buildStorageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } } diff --git a/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts b/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts index e6b636eab..f9ca51a29 100644 --- a/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts +++ b/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts @@ -1,30 +1,19 @@ import _ from "lodash"; import moment from "moment"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Package } from "../../packages/entities/Package"; -import { Repositories } from "../../Repositories"; -import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; import { Module } from "../entities/Module"; -export class DownloadModuleSnapshotUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class DownloadModuleSnapshotUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(module: Module, contents: MetadataPackage) { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, ""] - ); - - const downloadRepository = this.repositoryFactory.get( - Repositories.DownloadRepository, - [] - ); - - const user = await instanceRepository.getUser(); + const user = await this.instanceRepository(this.localInstance).getUser(); const item = Package.build({ module, lastUpdatedBy: user, @@ -35,6 +24,6 @@ export class DownloadModuleSnapshotUseCase implements UseCase { const date = moment().format("YYYYMMDDHHmm"); const name = `snapshot-${ruleName}-${module.type}-${date}.json`; const payload = { package: item, ...contents }; - return downloadRepository.downloadFile(name, payload); + return this.downloadRepository().downloadFile(name, payload); } } diff --git a/src/domain/modules/usecases/GetModuleUseCase.ts b/src/domain/modules/usecases/GetModuleUseCase.ts index 143f031d9..93e06b1be 100644 --- a/src/domain/modules/usecases/GetModuleUseCase.ts +++ b/src/domain/modules/usecases/GetModuleUseCase.ts @@ -1,22 +1,17 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; -export class GetModuleUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetModuleUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(id: string, instance = this.localInstance): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - - const module = await storageRepository.getObjectInCollection( + const module = await this.storageRepository(instance).getObjectInCollection( Namespace.MODULES, id ); diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index e5645bdc6..d7f89558e 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -1,16 +1,14 @@ -import { cache } from "../../../utils/cache"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; -export class ListModulesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListModulesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( bypassSharingSettings = false, @@ -38,20 +36,4 @@ export class ListModulesUseCase implements UseCase { module => bypassSharingSettings || module.hasPermissions("read", userId, userGroups) ); } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } } diff --git a/src/domain/modules/usecases/SaveModuleUseCase.ts b/src/domain/modules/usecases/SaveModuleUseCase.ts index 0464f8584..f5bf3ac3a 100644 --- a/src/domain/modules/usecases/SaveModuleUseCase.ts +++ b/src/domain/modules/usecases/SaveModuleUseCase.ts @@ -1,34 +1,23 @@ import _ from "lodash"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { Module } from "../entities/Module"; -export class SaveModuleUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class SaveModuleUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(module: Module): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, ""] - ); - const validations = module.validate(); if (validations.length === 0) { - const user = await instanceRepository.getUser(); + const user = await this.instanceRepository(this.localInstance).getUser(); const newModule = module.update({ - instance: instanceRepository.getBaseUrl(), + instance: this.instanceRepository(this.localInstance).getBaseUrl(), lastUpdated: new Date(), lastUpdatedBy: user, user: module.user.id ? module.user : user, @@ -45,7 +34,10 @@ export class SaveModuleUseCase implements UseCase { ), }); - await storageRepository.saveObjectInCollection(Namespace.MODULES, newModule); + await this.storageRepository(this.localInstance).saveObjectInCollection( + Namespace.MODULES, + newModule + ); } return validations; diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index d86d3bda4..14109ded3 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -1,15 +1,12 @@ -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; import { - SentPullRequestNotification, ReceivedPullRequestNotification, + SentPullRequestNotification, } from "../entities/PullRequestNotification"; export type CancelPullRequestError = @@ -19,12 +16,14 @@ export type CancelPullRequestError = | "REMOTE_NOT_FOUND" | "REMOTE_INVALID"; -export class CancelPullRequestUseCase implements UseCase { +export class CancelPullRequestUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute(id: string): Promise> { const notification = await this.getNotification(this.localInstance, id); @@ -75,14 +74,6 @@ export class CancelPullRequestUseCase implements UseCase { return Either.success(undefined); } - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - private async getNotification( instance: Instance, id: string diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index 94a4f1668..2cf86b7e3 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -1,20 +1,11 @@ import _ from "lodash"; -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; -import { - MetadataRepository, - MetadataRepositoryConstructor, -} from "../../metadata/repositories/MetadataRepository"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { AppNotification } from "../entities/Notification"; import { PullRequestStatus, @@ -30,12 +21,14 @@ export type ImportPullRequestError = | "ALREADY_IMPORTED" | "NOT_APPROVED"; -export class ImportPullRequestUseCase implements UseCase { +export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute( notificationId: string @@ -86,34 +79,6 @@ export class ImportPullRequestUseCase implements UseCase { return Either.success({ ...result, origin: remoteInstance.toPublicObject() }); } - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } - - @cache() - private metadataRepository(instance: Instance): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [instance, transformationRepository] - ); - } - private async getInstanceById(id: string): Promise { const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< InstanceData diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 5ac524bb7..0750784af 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -1,23 +1,22 @@ import _ from "lodash"; +import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; -export class ListNotificationsUseCase implements UseCase { +export class ListNotificationsUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute(): Promise { - const { id, userGroups } = await this.instanceRepository().getUser(); + const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); const notifications = await this.getInstanceNotifications(); const sentPullRequestNotifications = notifications.filter( @@ -43,20 +42,6 @@ export class ListNotificationsUseCase implements UseCase { ); } - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - private instanceRepository() { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, this.encryptionKey] - ); - } - private async getInstanceNotifications(): Promise { return this.storageRepository(this.localInstance).listObjectsInCollection( Namespace.NOTIFICATIONS diff --git a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts index d15b92e77..a11ff6e03 100644 --- a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts @@ -1,25 +1,19 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { AppNotification } from "../entities/Notification"; -export class MarkReadNotificationsUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class MarkReadNotificationsUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(ids: string[], read: boolean): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const notifications = await storageRepository.listObjectsInCollection( - Namespace.NOTIFICATIONS - ); + const notifications = await this.storageRepository( + this.localInstance + ).listObjectsInCollection(Namespace.NOTIFICATIONS); if (!notifications) return; const targetNotifications = notifications.filter(({ id }) => ids.includes(id)); @@ -28,20 +22,18 @@ export class MarkReadNotificationsUseCase implements UseCase { const hasPermissions = await this.hasPermissions(notification); if (!hasPermissions) return; - await storageRepository.saveObjectInCollection(Namespace.NOTIFICATIONS, { - ...notification, - read, - }); + await this.storageRepository(this.localInstance).saveObjectInCollection( + Namespace.NOTIFICATIONS, + { + ...notification, + read, + } + ); }); } private async hasPermissions(notification: AppNotification) { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, ""] - ); - - const { id, userGroups } = await instanceRepository.getUser(); + const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); if ( notification.owner.id !== id && diff --git a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts index cbc320eb3..059712952 100644 --- a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts +++ b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts @@ -1,14 +1,10 @@ import _ from "lodash"; -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { PullRequestStatus, ReceivedPullRequestNotification, @@ -16,8 +12,10 @@ import { export type UpdatePullRequestStatusError = "NOT_FOUND" | "PERMISSIONS" | "INVALID"; -export class UpdatePullRequestStatusUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class UpdatePullRequestStatusUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( id: string, @@ -50,22 +48,6 @@ export class UpdatePullRequestStatusUseCase implements UseCase { return Either.success(undefined); } - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } - private async hasPermissions(ids: string[]) { const responsibles = await this.getResponsibles(this.localInstance, ids); const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); diff --git a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts index 4ee8370fd..b913dfb01 100644 --- a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts @@ -1,27 +1,22 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackageData } from "../entities/ImportedPackage"; type ListImportedPackageError = "UNEXPECTED_ERROR"; -export class ListImportedPackagesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListImportedPackagesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(): Promise> { try { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const items = await storageRepository.listObjectsInCollection( - Namespace.IMPORTEDPACKAGES - ); + const items = await this.storageRepository(this.localInstance).listObjectsInCollection< + ImportedPackageData + >(Namespace.IMPORTEDPACKAGES); return Either.success(items); } catch (error) { diff --git a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts index dace093df..b2ee74218 100644 --- a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts @@ -1,27 +1,22 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { ImportedPackage } from "../entities/ImportedPackage"; type SavePackageError = "UNEXPECTED_ERROR"; -export class SaveImportedPackagesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class SaveImportedPackagesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( importedPackages: ImportedPackage[] ): Promise> { try { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - await storageRepository.saveObjectsInCollection( + await this.storageRepository(this.localInstance).saveObjectsInCollection( Namespace.IMPORTEDPACKAGES, importedPackages ); diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index 836798c97..5e60e9e91 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -1,27 +1,24 @@ import { generateUid } from "d2/uid"; +import { Namespace } from "../../../data/storage/Namespaces"; import { metadataTransformations } from "../../../data/transformations/PackageTransformations"; import { CompositionRoot } from "../../../presentation/CompositionRoot"; -import { cache } from "../../../utils/cache"; import { getMajorVersion } from "../../../utils/d2-utils"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Module } from "../../modules/entities/Module"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { Package } from "../entities/Package"; -export class CreatePackageUseCase implements UseCase { +export class CreatePackageUseCase extends DefaultUseCase implements UseCase { constructor( private compositionRoot: CompositionRoot, - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance - ) {} + ) { + super(repositoryFactory); + } public async execute( originInstance: string, @@ -31,7 +28,6 @@ export class CreatePackageUseCase implements UseCase { contents?: MetadataPackage ): Promise { const apiVersion = getMajorVersion(dhisVersion); - const transformationRepository = this.getTransformationRepository(); const basePayload = contents ? contents @@ -41,7 +37,7 @@ export class CreatePackageUseCase implements UseCase { targetInstances: [], }).buildPayload(); - const versionedPayload = transformationRepository.mapPackageTo( + const versionedPayload = this.transformationRepository().mapPackageTo( apiVersion, basePayload, metadataTransformations @@ -49,20 +45,10 @@ export class CreatePackageUseCase implements UseCase { const payload = sourcePackage.update({ contents: versionedPayload, dhisVersion }); - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, ""] - ); - const validations = payload.validate(); if (validations.length === 0) { - const user = await instanceRepository.getUser(); + const user = await this.instanceRepository(this.localInstance).getUser(); const newPackage = payload.update({ id: generateUid(), lastUpdated: new Date(), @@ -70,20 +56,18 @@ export class CreatePackageUseCase implements UseCase { user: payload.user.id ? payload.user : user, }); - await storageRepository.saveObjectInCollection(Namespace.PACKAGES, newPackage); + await this.storageRepository(this.localInstance).saveObjectInCollection( + Namespace.PACKAGES, + newPackage + ); const newModule = module.update({ lastPackageVersion: newPackage.version }); - await storageRepository.saveObjectInCollection(Namespace.MODULES, newModule); + await this.storageRepository(this.localInstance).saveObjectInCollection( + Namespace.MODULES, + newModule + ); } return validations; } - - @cache() - protected getTransformationRepository() { - return this.repositoryFactory.get( - Repositories.TransformationRepository, - [] - ); - } } diff --git a/src/domain/packages/usecases/DeletePackageUseCase.ts b/src/domain/packages/usecases/DeletePackageUseCase.ts index ce06a87ed..7c1064319 100644 --- a/src/domain/packages/usecases/DeletePackageUseCase.ts +++ b/src/domain/packages/usecases/DeletePackageUseCase.ts @@ -1,29 +1,24 @@ -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; -export class DeletePackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class DeletePackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(id: string, instance = this.localInstance): Promise { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - try { - const item = await storageRepository.getObjectInCollection( + const item = await this.storageRepository(instance).getObjectInCollection( Namespace.PACKAGES, id ); if (!item) return false; - await storageRepository.saveObjectInCollection(Namespace.PACKAGES, { + await this.storageRepository(instance).saveObjectInCollection(Namespace.PACKAGES, { ...item, deleted: true, contents: {}, diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index 089ebc7c3..4b5149c96 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -1,14 +1,9 @@ -import { cache } from "../../../utils/cache"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { getMetadataPackageDiff, MetadataPackageDiff } from "../entities/MetadataPackageDiff"; -import { Store } from "../../stores/entities/Store"; -import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; import { CompositionRoot } from "./../../../presentation/CompositionRoot"; import { Either } from "./../../common/entities/Either"; import { MetadataModule } from "./../../modules/entities/MetadataModule"; @@ -17,12 +12,14 @@ import { BasePackage } from "./../entities/Package"; type DiffPackageUseCaseError = "PACKAGE_NOT_FOUND" | "MODULE_NOT_FOUND" | "NETWORK_ERROR"; -export class DiffPackageUseCase implements UseCase { +export class DiffPackageUseCase extends DefaultUseCase implements UseCase { constructor( private compositionRoot: CompositionRoot, - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance - ) {} + ) { + super(repositoryFactory); + } public async execute( packageIdBase: string | undefined, @@ -77,10 +74,7 @@ export class DiffPackageUseCase implements UseCase { } private async getStorePackage(storeId: string, url: string) { - const store = ( - await this.storageRepository(this.localInstance).getObject(Namespace.STORES) - )?.find(store => store.id === storeId); - + const store = await this.storeRepository(this.localInstance).getById(storeId); if (!store) return undefined; const { encoding, content } = await this.gitRepository().request<{ @@ -96,20 +90,4 @@ export class DiffPackageUseCase implements UseCase { const { package: basePackage, ...contents } = validation.value.data; return { ...basePackage, contents }; } - - @cache() - private gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } } diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index 594efe404..198de0508 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -1,20 +1,16 @@ import _ from "lodash"; import moment from "moment"; -import { cache } from "../../../utils/cache"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage } from "../entities/Package"; -import { Store } from "../../stores/entities/Store"; -import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; -export class DownloadPackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class DownloadPackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(storeId: string | undefined, id: string, instance = this.localInstance) { const element = storeId @@ -38,10 +34,7 @@ export class DownloadPackageUseCase implements UseCase { } private async getStorePackage(storeId: string, url: string) { - const store = ( - await this.storageRepository(this.localInstance).getObject(Namespace.STORES) - )?.find(store => store.id === storeId); - + const store = await this.storeRepository(this.localInstance).getById(storeId); if (!store) return undefined; const { encoding, content } = await this.gitRepository().request<{ @@ -57,28 +50,4 @@ export class DownloadPackageUseCase implements UseCase { const { package: basePackage, ...contents } = validation.value.data; return { ...basePackage, contents }; } - - @cache() - private gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private downloadRepository() { - return this.repositoryFactory.get( - Repositories.DownloadRepository, - [] - ); - } } diff --git a/src/domain/packages/usecases/GetPackageUseCase.ts b/src/domain/packages/usecases/GetPackageUseCase.ts index 153f149c9..55a5707bd 100644 --- a/src/domain/packages/usecases/GetPackageUseCase.ts +++ b/src/domain/packages/usecases/GetPackageUseCase.ts @@ -1,25 +1,20 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; -export class GetPackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetPackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( id: string, instance = this.localInstance ): Promise> { - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - - const data = await storageRepository.getObjectInCollection( + const data = await this.storageRepository(instance).getObjectInCollection( Namespace.PACKAGES, id ); diff --git a/src/domain/packages/usecases/GetStorePackageUseCase.ts b/src/domain/packages/usecases/GetStorePackageUseCase.ts index 4556d0851..f52c093a0 100644 --- a/src/domain/packages/usecases/GetStorePackageUseCase.ts +++ b/src/domain/packages/usecases/GetStorePackageUseCase.ts @@ -1,28 +1,21 @@ import _ from "lodash"; -import { cache } from "../../../utils/cache"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; -import { Store } from "../../stores/entities/Store"; -import { GitHubRepositoryConstructor } from "../repositories/GitHubRepository"; -export class GetStorePackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class GetStorePackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( storeId: string, packageId: string ): Promise> { - const store = ( - await this.storageRepository(this.localInstance).getObject(Namespace.STORES) - )?.find(store => store.id === storeId); - + const store = await this.storeRepository(this.localInstance).getById(storeId); if (!store) return Either.error("NOT_FOUND"); const { encoding, content } = await this.gitRepository().request<{ @@ -43,20 +36,4 @@ export class GetStorePackageUseCase implements UseCase { return Either.success(packageToReturn); } - - @cache() - private gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } } diff --git a/src/domain/packages/usecases/ImportPackageUseCase.ts b/src/domain/packages/usecases/ImportPackageUseCase.ts index 45c0d6ac1..0a0fecf5f 100644 --- a/src/domain/packages/usecases/ImportPackageUseCase.ts +++ b/src/domain/packages/usecases/ImportPackageUseCase.ts @@ -1,32 +1,28 @@ import { debug } from "../../../utils/debug"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; import { MappingMapper } from "../../mapping/helpers/MappingMapper"; -import { - MetadataRepository, - MetadataRepositoryConstructor, -} from "../../metadata/repositories/MetadataRepository"; -import { Repositories } from "../../Repositories"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { Package } from "../entities/Package"; -export class ImportPackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ImportPackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( item: Package, mapping: MetadataMappingDictionary = {}, originInstance: DataSource, - destinationInstance?: DataSource + destinationInstance: DataSource = this.localInstance ): Promise { - const originCategoryOptionCombos = await this.getMetadataRepository( + const originCategoryOptionCombos = await this.metadataRepository( originInstance ).getCategoryOptionCombos(); - const destinationCategoryOptionCombos = await this.getMetadataRepository( + const destinationCategoryOptionCombos = await this.metadataRepository( destinationInstance ).getCategoryOptionCombos(); @@ -37,7 +33,7 @@ export class ImportPackageUseCase implements UseCase { ); const payload = mapper.applyMapping(item.contents); - const result = await this.getMetadataRepository(destinationInstance).save(payload); + const result = await this.metadataRepository(destinationInstance).save(payload); debug("Import package", { originInstance, @@ -50,20 +46,4 @@ export class ImportPackageUseCase implements UseCase { return result; } - - protected getMetadataRepository( - remoteInstance: DataSource = this.localInstance - ): MetadataRepository { - const transformationRepository = this.repositoryFactory.get< - TransformationRepositoryConstructor - >(Repositories.TransformationRepository, []); - - const tag = remoteInstance.type === "json" ? "json" : undefined; - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance, transformationRepository], - tag - ); - } } diff --git a/src/domain/packages/usecases/ListPackagesUseCase.ts b/src/domain/packages/usecases/ListPackagesUseCase.ts index 3db712790..a9ad03e0f 100644 --- a/src/domain/packages/usecases/ListPackagesUseCase.ts +++ b/src/domain/packages/usecases/ListPackagesUseCase.ts @@ -1,16 +1,14 @@ -import { cache } from "../../../utils/cache"; -import { UseCase } from "../../common/entities/UseCase"; +import { Namespace } from "../../../data/storage/Namespaces"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataModule } from "../../modules/entities/MetadataModule"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { BasePackage, Package } from "../entities/Package"; -export class ListPackagesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListPackagesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( bypassSharingSettings = false, @@ -32,20 +30,4 @@ export class ListPackagesUseCase implements UseCase { MetadataModule.build(module).hasPermissions("read", userId, userGroups) ); } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } } diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index e3bca3a46..48796ece8 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -1,32 +1,26 @@ import _ from "lodash"; import moment from "moment"; -import { cache } from "../../../utils/cache"; import { promiseMap } from "../../../utils/common"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataModule } from "../../modules/entities/MetadataModule"; import { BaseModule } from "../../modules/entities/Module"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { Store } from "../../stores/entities/Store"; import { GitHubError, GitHubListError } from "../entities/Errors"; import { ListPackage, Package } from "../entities/Package"; -import { Store } from "../../stores/entities/Store"; -import { GitHubRepositoryConstructor, moduleFile } from "../repositories/GitHubRepository"; +import { moduleFile } from "../repositories/GitHubRepository"; export type ListStorePackagesError = GitHubError | "STORE_NOT_FOUND"; -export class ListStorePackagesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class ListStorePackagesUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute(storeId: string): Promise> { - const store = ( - await this.storageRepository(this.localInstance).getObject(Namespace.STORES) - )?.find(store => store.id === storeId); - + const store = await this.storeRepository(this.localInstance).getById(storeId); if (!store) return Either.error("STORE_NOT_FOUND"); const userGroups = await this.instanceRepository(this.localInstance).getUserGroups(); @@ -48,30 +42,6 @@ export class ListStorePackagesUseCase implements UseCase { return Either.success(packages); } - @cache() - private gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } - private async getPackages( store: Store, userGroup: string diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index 46b1b7ab3..79b920bf7 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -1,17 +1,14 @@ import moment from "moment"; -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BaseModule } from "../../modules/entities/Module"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { Store } from "../../stores/entities/Store"; import { GitHubError } from "../entities/Errors"; import { BasePackage } from "../entities/Package"; -import { Store } from "../../stores/entities/Store"; -import { GitHubRepositoryConstructor, moduleFile } from "../repositories/GitHubRepository"; +import { moduleFile } from "../repositories/GitHubRepository"; export type PublishStorePackageError = | GitHubError @@ -19,18 +16,17 @@ export type PublishStorePackageError = | "PACKAGE_NOT_FOUND" | "ALREADY_PUBLISHED"; -export class PublishStorePackageUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class PublishStorePackageUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute( packageId: string, force = false ): Promise> { - const store = ( - await this.storageRepository(this.localInstance).getObject(Namespace.STORES) - )?.find(store => store.default && !store.deleted); - - if (!store) return Either.error("DEFAULT_STORE_NOT_FOUND"); + const defaultStore = await this.storeRepository(this.localInstance).getDefault(); + if (!defaultStore) return Either.error("DEFAULT_STORE_NOT_FOUND"); const storedPackage = await this.storageRepository( this.localInstance @@ -44,11 +40,11 @@ export class PublishStorePackageUseCase implements UseCase { const path = `${item.module.name}/${fileName}.json`; const branch = item.module.department.name.replace(/\s/g, "-"); - const existingFileCheck = await this.gitRepository().readFile(store, branch, path); + const existingFileCheck = await this.gitRepository().readFile(defaultStore, branch, path); if (existingFileCheck.isSuccess()) return Either.error("ALREADY_PUBLISHED"); const validation = await this.gitRepository().writeFile( - store, + defaultStore, branch, path, JSON.stringify(payload, null, 4) @@ -56,9 +52,9 @@ export class PublishStorePackageUseCase implements UseCase { if (validation.isError()) { if (force && validation.value.error === "BRANCH_NOT_FOUND") { - await this.gitRepository().createBranch(store, branch); + await this.gitRepository().createBranch(defaultStore, branch); const validation = await this.gitRepository().writeFile( - store, + defaultStore, branch, path, JSON.stringify(payload, null, 4) @@ -71,7 +67,7 @@ export class PublishStorePackageUseCase implements UseCase { } await this.createModuleFileIfRequired( - store, + defaultStore, branch, `${item.module.name}/${moduleFile}`, item.module @@ -97,20 +93,4 @@ export class PublishStorePackageUseCase implements UseCase { return console.warn("An error creating the module file has ocurred"); } } - - @cache() - private gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } } diff --git a/src/domain/stores/repositories/StoreRepository.ts b/src/domain/stores/repositories/StoreRepository.ts index 08b0bd576..44b4a445c 100644 --- a/src/domain/stores/repositories/StoreRepository.ts +++ b/src/domain/stores/repositories/StoreRepository.ts @@ -1,5 +1,10 @@ +import { Instance } from "../../instance/entities/Instance"; import { Store } from "../entities/Store"; +export interface StoreRepositoryConstructor { + new (instance: Instance): StoreRepository; +} + export interface StoreRepository { list(): Promise; getById(id: string): Promise; diff --git a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts index f95af318b..a18687d79 100644 --- a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts +++ b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts @@ -1,10 +1,9 @@ import _ from "lodash"; -import { cache } from "../../../utils/cache"; +import { Namespace } from "../../../data/storage/Namespaces"; import { NamedRef } from "../../common/entities/Ref"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; import { MessageNotification } from "../../notifications/entities/Notification"; @@ -12,9 +11,6 @@ import { ReceivedPullRequestNotification, SentPullRequestNotification, } from "../../notifications/entities/PullRequestNotification"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; interface CreatePullRequestParams { @@ -27,8 +23,10 @@ interface CreatePullRequestParams { notificationUsers: Pick; } -export class CreatePullRequestUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} +export class CreatePullRequestUseCase extends DefaultUseCase implements UseCase { + constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { + super(repositoryFactory); + } public async execute({ instance, @@ -78,22 +76,6 @@ export class CreatePullRequestUseCase implements UseCase { await this.sendMessage(instance, receivedPullRequest); } - @cache() - private storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - private instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } - private async getOwner(): Promise { const { id, name } = await this.instanceRepository(this.localInstance).getUser(); return { id, name }; diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index ffd74535d..2e8290ae5 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -1,5 +1,6 @@ import { D2Api } from "d2-api/2.30"; import _ from "lodash"; +import { Namespace } from "../../../data/storage/Namespaces"; import i18n from "../../../locales"; import SyncReport from "../../../models/syncReport"; import SyncRule from "../../../models/syncRule"; @@ -9,23 +10,16 @@ import { promiseMap } from "../../../utils/common"; import { getD2APiFromInstance } from "../../../utils/d2-utils"; import { debug } from "../../../utils/debug"; import { AggregatedPackage } from "../../aggregated/entities/AggregatedPackage"; -import { AggregatedRepositoryConstructor } from "../../aggregated/repositories/AggregatedRepository"; import { AggregatedSyncUseCase } from "../../aggregated/usecases/AggregatedSyncUseCase"; +import { DefaultUseCase } from "../../common/entities/UseCase"; 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 { Instance, InstanceData } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataMapping, MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; -import { MetadataRepositoryConstructor } from "../../metadata/repositories/MetadataRepository"; import { DeletedMetadataSyncUseCase } from "../../metadata/usecases/DeletedMetadataSyncUseCase"; import { MetadataSyncUseCase } from "../../metadata/usecases/MetadataSyncUseCase"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -import { TransformationRepositoryConstructor } from "../../transformations/repositories/TransformationRepository"; import { AggregatedDataStats, EventsDataStats, @@ -41,7 +35,7 @@ export type SyncronizationClass = | typeof DeletedMetadataSyncUseCase; export type SyncronizationPayload = MetadataPackage | AggregatedPackage | EventsPackage; -export abstract class GenericSyncUseCase { +export abstract class GenericSyncUseCase extends DefaultUseCase { public abstract readonly type: SynchronizationType; public readonly fields: string = "id,name"; protected readonly api: D2Api; @@ -52,6 +46,7 @@ export abstract class GenericSyncUseCase { protected readonly localInstance: Instance, protected readonly encryptionKey: string ) { + super(repositoryFactory); this.api = getD2APiFromInstance(localInstance); } @@ -79,45 +74,30 @@ export abstract class GenericSyncUseCase { @cache() protected async getInstanceRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [remoteInstance ?? defaultInstance, ""] - ); + return this.instanceRepository(remoteInstance ?? defaultInstance); } @cache() protected getTransformationRepository() { - return this.repositoryFactory.get( - Repositories.TransformationRepository, - [] - ); + return this.transformationRepository(); } @cache() protected async getMetadataRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [remoteInstance ?? defaultInstance, this.getTransformationRepository()] - ); + return this.metadataRepository(remoteInstance ?? defaultInstance); } @cache() protected async getAggregatedRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.repositoryFactory.get( - Repositories.AggregatedRepository, - [remoteInstance ?? defaultInstance] - ); + return this.aggregatedRepository(remoteInstance ?? defaultInstance); } @cache() protected async getEventsRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.repositoryFactory.get( - Repositories.EventsRepository, - [remoteInstance ?? defaultInstance] - ); + return this.eventsRepository(remoteInstance ?? defaultInstance); } @cache() @@ -182,26 +162,16 @@ export abstract class GenericSyncUseCase { private async getInstanceById(id: string): Promise { if (id === "LOCAL") return this.localInstance; - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const data = await storageRepository.getObjectInCollection( - Namespace.INSTANCES, - id - ); + const data = await this.storageRepository(this.localInstance).getObjectInCollection< + InstanceData + >(Namespace.INSTANCES, id); if (!data) return undefined; const instance = Instance.build(data).decryptPassword(this.encryptionKey); - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); try { - const version = await instanceRepository.getVersion(); + const version = await this.instanceRepository(instance).getVersion(); return instance.update({ version }); } catch (error) { return instance; diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 5125e5822..a6a389af9 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -1,24 +1,23 @@ import _ from "lodash"; +import { Namespace } from "../../../data/storage/Namespaces"; import { SynchronizationBuilder } from "../../../types/synchronization"; import { Either } from "../../common/entities/Either"; -import { UseCase } from "../../common/entities/UseCase"; +import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; -import { Repositories } from "../../Repositories"; -import { Namespace } from "../../../data/storage/Namespaces"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { SynchronizationType } from "../entities/SynchronizationType"; export type PrepareSyncError = "PULL_REQUEST" | "PULL_REQUEST_RESPONSIBLE" | "INSTANCE_NOT_FOUND"; -export class PrepareSyncUseCase implements UseCase { +export class PrepareSyncUseCase extends DefaultUseCase implements UseCase { constructor( - private repositoryFactory: RepositoryFactory, + repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) {} + ) { + super(repositoryFactory); + } public async execute( type: SynchronizationType, @@ -66,12 +65,7 @@ export class PrepareSyncUseCase implements UseCase { } private async getCurrentUser() { - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [this.localInstance, ""] - ); - - return instanceRepository.getUser(); + return this.instanceRepository(this.localInstance).getUser(); } private async getResponsiblesForInstance( @@ -80,14 +74,9 @@ export class PrepareSyncUseCase implements UseCase { const instance = await this.getInstanceById(instanceId); if (instance.isError() || !instance.value.data) return Either.error("INSTANCE_NOT_FOUND"); - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [instance.value.data] - ); - - const responsibles = await storageRepository.listObjectsInCollection( - Namespace.RESPONSIBLES - ); + const responsibles = await this.storageRepository( + instance.value.data + ).listObjectsInCollection(Namespace.RESPONSIBLES); return Either.success(responsibles); } @@ -95,25 +84,16 @@ export class PrepareSyncUseCase implements UseCase { private async getInstanceById(id: string): Promise> { if (id === "LOCAL") return Either.success(this.localInstance); - const storageRepository = this.repositoryFactory.get( - Repositories.StorageRepository, - [this.localInstance] - ); - - const objects = await storageRepository.listObjectsInCollection( - Namespace.INSTANCES - ); + const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< + InstanceData + >(Namespace.INSTANCES); const data = objects.find(data => data.id === id); if (!data) return Either.error("INSTANCE_NOT_FOUND"); const instance = Instance.build(data).decryptPassword(this.encryptionKey); - const instanceRepository = this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); + const version = await this.instanceRepository(instance).getVersion(); - const version = await instanceRepository.getVersion(); return Either.success(instance.update({ version })); } } From f7aa18bb0caa53cef624e7fc57c5ee4e72daabe4 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 07:06:40 +0100 Subject: [PATCH 010/163] Move repository helpers to factory --- .../__tests__/integration/helpers.ts | 17 +-- src/domain/Repositories.ts | 11 -- src/domain/common/entities/UseCase.ts | 106 ------------------ .../common/factories/RepositoryFactory.ts | 102 ++++++++++++++++- .../usecases/DeleteInstanceUseCase.ts | 15 +-- .../usecases/GetInstanceApiUseCase.ts | 10 +- .../usecases/GetInstanceByIdUseCase.ts | 16 ++- .../usecases/GetInstanceVersionUseCase.ts | 10 +- .../usecases/GetRootOrgUnitUseCase.ts | 10 +- .../instance/usecases/GetUserGroupsUseCase.ts | 12 +- .../instance/usecases/ListInstancesUseCase.ts | 16 ++- .../instance/usecases/SaveInstanceUseCase.ts | 20 ++-- .../usecases/ValidateInstanceUseCase.ts | 9 +- .../mapping/usecases/GenericMappingUseCase.ts | 26 ++--- .../usecases/GetResponsiblesUseCase.ts | 14 +-- .../usecases/ImportMetadataUseCase.ts | 10 +- .../usecases/ListAllMetadataUseCase.ts | 10 +- .../metadata/usecases/ListMetadataUseCase.ts | 10 +- .../usecases/ListResponsiblesUseCase.ts | 24 ++-- .../usecases/SetResponsiblesUseCase.ts | 35 +++--- .../modules/usecases/DeleteModuleUseCase.ts | 22 ++-- .../usecases/DownloadModuleSnapshotUseCase.ts | 12 +- .../modules/usecases/GetModuleUseCase.ts | 15 +-- .../modules/usecases/ListModulesUseCase.ts | 22 ++-- .../modules/usecases/SaveModuleUseCase.ts | 23 ++-- .../usecases/CancelPullRequestUseCase.ts | 37 +++--- .../usecases/ImportPullRequestUseCase.ts | 61 +++++----- .../usecases/ListNotificationsUseCase.ts | 42 +++---- .../usecases/MarkReadNotificationsUseCase.ts | 27 +++-- .../UpdatePullRequestStatusUseCase.ts | 31 +++-- .../usecases/ListImportedPackagesUseCase.ts | 14 +-- .../usecases/SaveImportedPackagesUseCase.ts | 15 +-- .../packages/usecases/CreatePackageUseCase.ts | 36 +++--- .../packages/usecases/DeletePackageUseCase.ts | 27 +++-- .../packages/usecases/DiffPackageUseCase.ts | 35 +++--- .../usecases/DownloadPackageUseCase.ts | 29 +++-- .../packages/usecases/GetPackageUseCase.ts | 15 +-- .../usecases/GetStorePackageUseCase.ts | 20 ++-- .../packages/usecases/ImportPackageUseCase.ts | 24 ++-- .../packages/usecases/ListPackagesUseCase.ts | 22 ++-- .../usecases/ListStorePackagesUseCase.ts | 26 +++-- .../usecases/PublishStorePackageUseCase.ts | 51 ++++----- .../usecases/CreatePullRequestUseCase.ts | 34 +++--- .../usecases/GenericSyncUseCase.ts | 23 ++-- .../usecases/PrepareSyncUseCase.ts | 26 ++--- src/presentation/CompositionRoot.ts | 13 +-- 46 files changed, 559 insertions(+), 626 deletions(-) delete mode 100644 src/domain/Repositories.ts diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index fd5e4dec0..93f568ceb 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -1,18 +1,19 @@ -import { SynchronizationBuilder } from "./../../../../types/synchronization"; import _ from "lodash"; -import { Server, Request } from "miragejs"; +import { Request, Server } from "miragejs"; +import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; - +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; -import { Repositories } from "../../../../domain/Repositories"; +import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; +import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; -import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { AnyRegistry } from "miragejs/-types"; -import { startDhis } from "../../../../utils/dhisServer"; +import { SynchronizationBuilder } from "./../../../../types/synchronization"; export function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(); diff --git a/src/domain/Repositories.ts b/src/domain/Repositories.ts deleted file mode 100644 index 6b8a63d93..000000000 --- a/src/domain/Repositories.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const Repositories = { - InstanceRepository: "instanceRepository", - StoreRepository: "storeRepository", - StorageRepository: "storageRepository", - DownloadRepository: "downloadRepository", - GitHubRepository: "githubRepository", - AggregatedRepository: "aggregatedRepository", - EventsRepository: "eventsRepository", - MetadataRepository: "metadataRepository", - TransformationRepository: "transformationsRepository", -}; diff --git a/src/domain/common/entities/UseCase.ts b/src/domain/common/entities/UseCase.ts index c7001ca68..84d48700e 100644 --- a/src/domain/common/entities/UseCase.ts +++ b/src/domain/common/entities/UseCase.ts @@ -1,109 +1,3 @@ -import { cache } from "../../../utils/cache"; -import { - AggregatedRepository, - AggregatedRepositoryConstructor, -} from "../../aggregated/repositories/AggregatedRepository"; -import { - EventsRepository, - EventsRepositoryConstructor, -} from "../../events/repositories/EventsRepository"; -import { DataSource } from "../../instance/entities/DataSource"; -import { Instance } from "../../instance/entities/Instance"; -import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; -import { - MetadataRepository, - MetadataRepositoryConstructor, -} from "../../metadata/repositories/MetadataRepository"; -import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; -import { Repositories } from "../../Repositories"; -import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; -import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; -import { - TransformationRepository, - TransformationRepositoryConstructor, -} from "../../transformations/repositories/TransformationRepository"; -import { RepositoryFactory } from "../factories/RepositoryFactory"; - export interface UseCase { execute: Function; } - -export abstract class DefaultUseCase { - constructor(protected repositoryFactory: RepositoryFactory) {} - - @cache() - protected gitRepository() { - return this.repositoryFactory.get( - Repositories.GitHubRepository, - [] - ); - } - - @cache() - protected storageRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StorageRepository, - [instance] - ); - } - - @cache() - protected downloadRepository() { - return this.repositoryFactory.get( - Repositories.DownloadRepository, - [] - ); - } - - @cache() - protected storeRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.StoreRepository, - [instance] - ); - } - - @cache() - protected instanceRepository(instance: Instance) { - return this.repositoryFactory.get( - Repositories.InstanceRepository, - [instance, ""] - ); - } - - @cache() - protected transformationRepository(): TransformationRepository { - return this.repositoryFactory.get( - Repositories.TransformationRepository, - [] - ); - } - - @cache() - protected metadataRepository(instance: DataSource): MetadataRepository { - const tag = instance.type === "json" ? "json" : undefined; - - return this.repositoryFactory.get( - Repositories.MetadataRepository, - [instance, this.transformationRepository()], - tag - ); - } - - @cache() - protected aggregatedRepository(instance: Instance): AggregatedRepository { - return this.repositoryFactory.get( - Repositories.AggregatedRepository, - [instance] - ); - } - - @cache() - protected eventsRepository(instance: Instance): EventsRepository { - return this.repositoryFactory.get( - Repositories.EventsRepository, - [instance] - ); - } -} diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 9f7a79151..4bf797046 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -1,17 +1,40 @@ import { cache } from "../../../utils/cache"; +import { + AggregatedRepository, + AggregatedRepositoryConstructor, +} from "../../aggregated/repositories/AggregatedRepository"; +import { + EventsRepository, + EventsRepositoryConstructor, +} from "../../events/repositories/EventsRepository"; +import { DataSource } from "../../instance/entities/DataSource"; +import { Instance } from "../../instance/entities/Instance"; +import { InstanceRepositoryConstructor } from "../../instance/repositories/InstanceRepository"; +import { + MetadataRepository, + MetadataRepositoryConstructor, +} from "../../metadata/repositories/MetadataRepository"; +import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; +import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; +import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; +import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; +import { + TransformationRepository, + TransformationRepositoryConstructor, +} from "../../transformations/repositories/TransformationRepository"; type ClassType = new (...args: any[]) => any; export class RepositoryFactory { - private repositories: Map = new Map(); + private repositories: Map = new Map(); // TODO: TS 4.1 `${RepositoryKeys}-${string}` - public bind(repository: string, implementation: ClassType, tag = "default") { + public bind(repository: RepositoryKeys, implementation: ClassType, tag = "default") { this.repositories.set(`${repository}-${tag}`, implementation); } @cache() public get( - repository: string, + repository: RepositoryKeys, params: ConstructorParameters, tag?: Key ): InstanceType { @@ -20,4 +43,77 @@ export class RepositoryFactory { if (!Implementation) throw new Error(`Repository ${repositoryName} not found`); return new Implementation(...params); } + + @cache() + public gitRepository() { + return this.get(Repositories.GitHubRepository, []); + } + + @cache() + public storageRepository(instance: Instance) { + return this.get(Repositories.StorageRepository, [instance]); + } + + @cache() + public downloadRepository() { + return this.get(Repositories.DownloadRepository, []); + } + + @cache() + public storeRepository(instance: Instance) { + return this.get(Repositories.StoreRepository, [instance]); + } + + @cache() + public instanceRepository(instance: Instance) { + return this.get(Repositories.InstanceRepository, [ + instance, + "", + ]); + } + + @cache() + public transformationRepository(): TransformationRepository { + return this.get( + Repositories.TransformationRepository, + [] + ); + } + + @cache() + public metadataRepository(instance: DataSource): MetadataRepository { + const tag = instance.type === "json" ? "json" : undefined; + + return this.get( + Repositories.MetadataRepository, + [instance, this.transformationRepository()], + tag + ); + } + + @cache() + public aggregatedRepository(instance: Instance): AggregatedRepository { + return this.get(Repositories.AggregatedRepository, [ + instance, + ]); + } + + @cache() + public eventsRepository(instance: Instance): EventsRepository { + return this.get(Repositories.EventsRepository, [instance]); + } } + +type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; + +export const Repositories = { + InstanceRepository: "instanceRepository", + StoreRepository: "storeRepository", + StorageRepository: "storageRepository", + DownloadRepository: "downloadRepository", + GitHubRepository: "githubRepository", + AggregatedRepository: "aggregatedRepository", + EventsRepository: "eventsRepository", + MetadataRepository: "metadataRepository", + TransformationRepository: "transformationsRepository", +} as const; diff --git a/src/domain/instance/usecases/DeleteInstanceUseCase.ts b/src/domain/instance/usecases/DeleteInstanceUseCase.ts index 2d82c125f..3d0f8cda3 100644 --- a/src/domain/instance/usecases/DeleteInstanceUseCase.ts +++ b/src/domain/instance/usecases/DeleteInstanceUseCase.ts @@ -1,19 +1,16 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class DeleteInstanceUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class DeleteInstanceUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string): Promise { try { - await this.storageRepository(this.localInstance).removeObjectInCollection( - Namespace.INSTANCES, - id - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .removeObjectInCollection(Namespace.INSTANCES, id); } catch (error) { console.error(error); return false; diff --git a/src/domain/instance/usecases/GetInstanceApiUseCase.ts b/src/domain/instance/usecases/GetInstanceApiUseCase.ts index 94a54f2ad..d47678d0c 100644 --- a/src/domain/instance/usecases/GetInstanceApiUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceApiUseCase.ts @@ -1,14 +1,12 @@ import { D2Api } from "../../../types/d2-api"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class GetInstanceApiUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetInstanceApiUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public execute(instance = this.localInstance): D2Api { - return this.instanceRepository(instance).getApi(); + return this.repositoryFactory.instanceRepository(instance).getApi(); } } diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index 74bf74850..b1373c631 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -1,24 +1,22 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../entities/Instance"; -export class GetInstanceByIdUseCase extends DefaultUseCase implements UseCase { +export class GetInstanceByIdUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute(id: string): Promise> { if (id === "LOCAL") return Either.success(this.localInstance); - const data = await this.storageRepository(this.localInstance).getObjectInCollection< - InstanceData - >(Namespace.INSTANCES, id); + const data = await this.repositoryFactory + .storageRepository(this.localInstance) + .getObjectInCollection(Namespace.INSTANCES, id); if (!data) return Either.error("NOT_FOUND"); diff --git a/src/domain/instance/usecases/GetInstanceVersionUseCase.ts b/src/domain/instance/usecases/GetInstanceVersionUseCase.ts index 5f0e25fd2..6b74799a2 100644 --- a/src/domain/instance/usecases/GetInstanceVersionUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceVersionUseCase.ts @@ -1,14 +1,12 @@ -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class GetInstanceVersionUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetInstanceVersionUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(instance = this.localInstance): Promise { - const buildVersion = await this.instanceRepository(instance).getVersion(); + const buildVersion = await this.repositoryFactory.instanceRepository(instance).getVersion(); const [major, minor] = buildVersion.split("."); return `${major}.${minor}`; } diff --git a/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts b/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts index 8bc421258..51c54997b 100644 --- a/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts +++ b/src/domain/instance/usecases/GetRootOrgUnitUseCase.ts @@ -1,13 +1,11 @@ -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class GetRootOrgUnitUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetRootOrgUnitUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(instance = this.localInstance) { - return this.instanceRepository(instance).getOrgUnitRoots(); + return this.repositoryFactory.instanceRepository(instance).getOrgUnitRoots(); } } diff --git a/src/domain/instance/usecases/GetUserGroupsUseCase.ts b/src/domain/instance/usecases/GetUserGroupsUseCase.ts index 65c3aa47b..d6d2efdb4 100644 --- a/src/domain/instance/usecases/GetUserGroupsUseCase.ts +++ b/src/domain/instance/usecases/GetUserGroupsUseCase.ts @@ -1,15 +1,15 @@ import _ from "lodash"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class GetUserGroupsUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetUserGroupsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(instance = this.localInstance) { - const userGroups = await this.instanceRepository(instance).getUserGroups(); + const userGroups = await this.repositoryFactory + .instanceRepository(instance) + .getUserGroups(); return _.sortBy(userGroups, "name"); } } diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index 4486dce9a..cdad5dd6a 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../entities/Instance"; @@ -8,19 +8,17 @@ export interface ListInstancesUseCaseProps { search?: string; } -export class ListInstancesUseCase extends DefaultUseCase implements UseCase { +export class ListInstancesUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute({ search }: ListInstancesUseCaseProps = {}): Promise { - const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< - InstanceData - >(Namespace.INSTANCES); + const objects = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.INSTANCES); const filteredData = search ? _.filter(objects, o => diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index e5d0445f4..56dd923cd 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -1,26 +1,26 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../entities/Instance"; -export class SaveInstanceUseCase extends DefaultUseCase implements UseCase { +export class SaveInstanceUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute(instance: Instance): Promise { const validations = instance.validate(); if (validations.length === 0) { - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.INSTANCES, - instance.encryptPassword(this.encryptionKey).toObject() - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection( + Namespace.INSTANCES, + instance.encryptPassword(this.encryptionKey).toObject() + ); } return validations; diff --git a/src/domain/instance/usecases/ValidateInstanceUseCase.ts b/src/domain/instance/usecases/ValidateInstanceUseCase.ts index 62c8a44a1..5220574c6 100644 --- a/src/domain/instance/usecases/ValidateInstanceUseCase.ts +++ b/src/domain/instance/usecases/ValidateInstanceUseCase.ts @@ -1,15 +1,18 @@ import i18n from "../../../locales"; import { debug } from "../../../utils/debug"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource, isJSONDataSource } from "../entities/DataSource"; -export class ValidateInstanceUseCase extends DefaultUseCase implements UseCase { +export class ValidateInstanceUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory) {} + public async execute(instance: DataSource): Promise> { if (isJSONDataSource(instance)) return Either.success(undefined); try { - const version = await this.instanceRepository(instance).getVersion(); + const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); if (version) { return Either.success(undefined); diff --git a/src/domain/mapping/usecases/GenericMappingUseCase.ts b/src/domain/mapping/usecases/GenericMappingUseCase.ts index 91321d438..4a1288e57 100644 --- a/src/domain/mapping/usecases/GenericMappingUseCase.ts +++ b/src/domain/mapping/usecases/GenericMappingUseCase.ts @@ -5,24 +5,20 @@ import { } from "../../../presentation/react/components/mapping-table/utils"; import { Dictionary } from "../../../types/utils"; import { NamedRef } from "../../common/entities/Ref"; -import { DefaultUseCase } from "../../common/entities/UseCase"; + import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { MetadataMapping, MetadataMappingDictionary } from "../entities/MetadataMapping"; -export abstract class GenericMappingUseCase extends DefaultUseCase { - constructor(repositoryFactory: RepositoryFactory, protected localInstance: Instance) { - super(repositoryFactory); - } +export abstract class GenericMappingUseCase { + constructor(private repositoryFactory: RepositoryFactory, protected localInstance: Instance) {} protected async getMetadata(instance: DataSource, ids: string[]) { - return this.metadataRepository(instance).getMetadataByIds>( - ids, - fields, - true - ); + return this.repositoryFactory + .metadataRepository(instance) + .getMetadataByIds>(ids, fields, true); } protected createMetadataDictionary(metadata: MetadataPackage) { @@ -132,7 +128,9 @@ export abstract class GenericMappingUseCase extends DefaultUseCase { const programStages = this.getProgramStages(metadata[0]); const programStageDataElements = this.getProgramStageDataElements(metadata[0]); - const defaultValues = await this.metadataRepository(instance).getDefaultIds(); + const defaultValues = await this.repositoryFactory + .metadataRepository(instance) + .getDefaultIds(); return _.union(categoryOptions, options, programStages, programStageDataElements) .map(({ id }) => id) @@ -158,9 +156,9 @@ export abstract class GenericMappingUseCase extends DefaultUseCase { const selectedItem = originMetadata[selectedItemId]; if (!selectedItem) return []; - const destinationMetadata = await this.metadataRepository( - destinationInstance - ).lookupSimilar(selectedItem); + const destinationMetadata = await this.repositoryFactory + .metadataRepository(destinationInstance) + .lookupSimilar(selectedItem); const objects = _(destinationMetadata) .values() diff --git a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts index 159fc75a1..cc6ea9382 100644 --- a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts @@ -1,21 +1,19 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -export class GetResponsiblesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetResponsiblesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( ids: string[], instance = this.localInstance ): Promise { - const items = await this.storageRepository(instance).listObjectsInCollection< - MetadataResponsible - >(Namespace.RESPONSIBLES); + const items = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.RESPONSIBLES); return items.filter(({ id }) => ids.includes(id)); } diff --git a/src/domain/metadata/usecases/ImportMetadataUseCase.ts b/src/domain/metadata/usecases/ImportMetadataUseCase.ts index 525ab31b0..a5c6b12e9 100644 --- a/src/domain/metadata/usecases/ImportMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ImportMetadataUseCase.ts @@ -1,18 +1,16 @@ -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; import { MetadataPackage } from "../entities/MetadataEntities"; -export class ImportMetadataUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ImportMetadataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( payload: MetadataPackage, instance = this.localInstance ): Promise { - return this.metadataRepository(instance).save(payload); + return this.repositoryFactory.metadataRepository(instance).save(payload); } } diff --git a/src/domain/metadata/usecases/ListAllMetadataUseCase.ts b/src/domain/metadata/usecases/ListAllMetadataUseCase.ts index 8bdc91c97..54cab7dab 100644 --- a/src/domain/metadata/usecases/ListAllMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ListAllMetadataUseCase.ts @@ -1,19 +1,17 @@ -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { MetadataEntity } from "../entities/MetadataEntities"; import { ListMetadataParams } from "../repositories/MetadataRepository"; -export class ListAllMetadataUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListAllMetadataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( params: ListMetadataParams, instance: DataSource = this.localInstance ): Promise { - return this.metadataRepository(instance).listAllMetadata(params); + return this.repositoryFactory.metadataRepository(instance).listAllMetadata(params); } } diff --git a/src/domain/metadata/usecases/ListMetadataUseCase.ts b/src/domain/metadata/usecases/ListMetadataUseCase.ts index d4dfb42ca..de497c6ca 100644 --- a/src/domain/metadata/usecases/ListMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ListMetadataUseCase.ts @@ -1,18 +1,16 @@ -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { ListMetadataParams, ListMetadataResponse } from "../repositories/MetadataRepository"; -export class ListMetadataUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListMetadataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( params: ListMetadataParams, instance: DataSource = this.localInstance ): Promise { - return this.metadataRepository(instance).listMetadata(params); + return this.repositoryFactory.metadataRepository(instance).listMetadata(params); } } diff --git a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts index d3b9067c6..ff74bcfcb 100644 --- a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts @@ -1,19 +1,17 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -export class ListResponsiblesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListResponsiblesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(instance = this.localInstance): Promise { - const items = await this.storageRepository(instance).listObjectsInCollection< - MetadataResponsible - >(Namespace.RESPONSIBLES); + const items = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.RESPONSIBLES); const names = await this.getDisplayNames( instance, @@ -24,10 +22,12 @@ export class ListResponsiblesUseCase extends DefaultUseCase implements UseCase { } private async getDisplayNames(instance: Instance, ids: string[]) { - const metadata = await this.metadataRepository(instance).getMetadataByIds<{ - id: string; - displayName: string; - }>(ids, "id,displayName"); + const metadata = await this.repositoryFactory + .metadataRepository(instance) + .getMetadataByIds<{ + id: string; + displayName: string; + }>(ids, "id,displayName"); return _(metadata) .values() diff --git a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts index 153e26a3e..b2ceba954 100644 --- a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts @@ -1,30 +1,26 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { ReceivedPullRequestNotification } from "../../notifications/entities/PullRequestNotification"; import { MetadataResponsible } from "../entities/MetadataResponsible"; -export class SetResponsiblesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class SetResponsiblesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(responsible: MetadataResponsible): Promise { const { id, users, userGroups } = responsible; if (users.length === 0 && userGroups.length === 0) { - await this.storageRepository(this.localInstance).removeObjectInCollection( - Namespace.RESPONSIBLES, - id - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .removeObjectInCollection(Namespace.RESPONSIBLES, id); } else { - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.RESPONSIBLES, - responsible - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.RESPONSIBLES, responsible); } await this.updatePendingPullRequests(responsible); @@ -35,9 +31,9 @@ export class SetResponsiblesUseCase extends DefaultUseCase implements UseCase { users, userGroups, }: MetadataResponsible): Promise { - const notifications = await this.storageRepository( - this.localInstance - ).listObjectsInCollection(Namespace.NOTIFICATIONS); + const notifications = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.NOTIFICATIONS); const relatedPullRequests = notifications.filter( ({ type, selectedIds }) => type === "received-pull-request" && selectedIds.includes(id) @@ -51,10 +47,9 @@ export class SetResponsiblesUseCase extends DefaultUseCase implements UseCase { userGroups: _.uniqBy([...notification.userGroups, ...userGroups], "id"), }; - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - newNotification - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); }); } } diff --git a/src/domain/modules/usecases/DeleteModuleUseCase.ts b/src/domain/modules/usecases/DeleteModuleUseCase.ts index 80bdfdb41..d9a7d73c9 100644 --- a/src/domain/modules/usecases/DeleteModuleUseCase.ts +++ b/src/domain/modules/usecases/DeleteModuleUseCase.ts @@ -1,18 +1,18 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BasePackage, Package } from "../../packages/entities/Package"; -export class DeleteModuleUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class DeleteModuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { try { - await this.storageRepository(instance).removeObjectInCollection(Namespace.MODULES, id); + await this.repositoryFactory + .storageRepository(instance) + .removeObjectInCollection(Namespace.MODULES, id); await this.deletePackagesFromModule(id, instance); } catch (error) { return false; @@ -22,9 +22,9 @@ export class DeleteModuleUseCase extends DefaultUseCase implements UseCase { } private async deletePackagesFromModule(id: string, instance: Instance): Promise { - const packages = await this.storageRepository(instance).listObjectsInCollection( - Namespace.PACKAGES - ); + const packages = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.PACKAGES); const newPackages = packages .filter(({ module }) => module.id === id) @@ -34,7 +34,9 @@ export class DeleteModuleUseCase extends DefaultUseCase implements UseCase { })); await promiseMap(newPackages, async (item: BasePackage) => { - await this.storageRepository(instance).saveObjectInCollection(Namespace.PACKAGES, item); + await this.repositoryFactory + .storageRepository(instance) + .saveObjectInCollection(Namespace.PACKAGES, item); }); } } diff --git a/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts b/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts index f9ca51a29..8039f8ba2 100644 --- a/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts +++ b/src/domain/modules/usecases/DownloadModuleSnapshotUseCase.ts @@ -1,19 +1,17 @@ import _ from "lodash"; import moment from "moment"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Package } from "../../packages/entities/Package"; import { Module } from "../entities/Module"; -export class DownloadModuleSnapshotUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class DownloadModuleSnapshotUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(module: Module, contents: MetadataPackage) { - const user = await this.instanceRepository(this.localInstance).getUser(); + const user = await this.repositoryFactory.instanceRepository(this.localInstance).getUser(); const item = Package.build({ module, lastUpdatedBy: user, @@ -24,6 +22,6 @@ export class DownloadModuleSnapshotUseCase extends DefaultUseCase implements Use const date = moment().format("YYYYMMDDHHmm"); const name = `snapshot-${ruleName}-${module.type}-${date}.json`; const payload = { package: item, ...contents }; - return this.downloadRepository().downloadFile(name, payload); + return this.repositoryFactory.downloadRepository().downloadFile(name, payload); } } diff --git a/src/domain/modules/usecases/GetModuleUseCase.ts b/src/domain/modules/usecases/GetModuleUseCase.ts index 93e06b1be..9a6dd7886 100644 --- a/src/domain/modules/usecases/GetModuleUseCase.ts +++ b/src/domain/modules/usecases/GetModuleUseCase.ts @@ -1,20 +1,17 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; -export class GetModuleUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetModuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { - const module = await this.storageRepository(instance).getObjectInCollection( - Namespace.MODULES, - id - ); + const module = await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.MODULES, id); switch (module?.type) { case "metadata": diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index d7f89558e..cde6cc56d 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -1,26 +1,28 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataModule } from "../entities/MetadataModule"; import { BaseModule, Module } from "../entities/Module"; -export class ListModulesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListModulesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( bypassSharingSettings = false, instance = this.localInstance ): Promise { - const userGroups = await this.instanceRepository(this.localInstance).getUserGroups(); - const { id: userId } = await this.instanceRepository(this.localInstance).getUser(); + const userGroups = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUserGroups(); + const { id: userId } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); const data = ( - await this.storageRepository(instance).listObjectsInCollection( - Namespace.MODULES - ) + await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.MODULES) ).filter(module => !module.autogenerated); return data diff --git a/src/domain/modules/usecases/SaveModuleUseCase.ts b/src/domain/modules/usecases/SaveModuleUseCase.ts index f5bf3ac3a..155a2b319 100644 --- a/src/domain/modules/usecases/SaveModuleUseCase.ts +++ b/src/domain/modules/usecases/SaveModuleUseCase.ts @@ -1,23 +1,25 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { Module } from "../entities/Module"; -export class SaveModuleUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class SaveModuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(module: Module): Promise { const validations = module.validate(); if (validations.length === 0) { - const user = await this.instanceRepository(this.localInstance).getUser(); + const user = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); const newModule = module.update({ - instance: this.instanceRepository(this.localInstance).getBaseUrl(), + instance: this.repositoryFactory + .instanceRepository(this.localInstance) + .getBaseUrl(), lastUpdated: new Date(), lastUpdatedBy: user, user: module.user.id ? module.user : user, @@ -34,10 +36,9 @@ export class SaveModuleUseCase extends DefaultUseCase implements UseCase { ), }); - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.MODULES, - newModule - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.MODULES, newModule); } return validations; diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index 14109ded3..271e31c98 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -1,6 +1,6 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { AppNotification } from "../entities/Notification"; @@ -16,14 +16,12 @@ export type CancelPullRequestError = | "REMOTE_NOT_FOUND" | "REMOTE_INVALID"; -export class CancelPullRequestUseCase extends DefaultUseCase implements UseCase { +export class CancelPullRequestUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute(id: string): Promise> { const notification = await this.getNotification(this.localInstance, id); @@ -40,10 +38,9 @@ export class CancelPullRequestUseCase extends DefaultUseCase implements UseCase status: "CANCELLED", }; - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - newNotification - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); const remoteInstance = await this.getInstanceById(notification.instance.id); if (!remoteInstance) return Either.error("INSTANCE_NOT_FOUND"); @@ -66,10 +63,9 @@ export class CancelPullRequestUseCase extends DefaultUseCase implements UseCase payload: {}, }; - await this.storageRepository(remoteInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - newRemoteNotification - ); + await this.repositoryFactory + .storageRepository(remoteInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, newRemoteNotification); return Either.success(undefined); } @@ -78,16 +74,15 @@ export class CancelPullRequestUseCase extends DefaultUseCase implements UseCase instance: Instance, id: string ): Promise { - return await this.storageRepository(instance).getObjectInCollection( - Namespace.NOTIFICATIONS, - id - ); + return await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.NOTIFICATIONS, id); } private async getInstanceById(id: string): Promise { - const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< - InstanceData - >(Namespace.INSTANCES); + const objects = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.INSTANCES); const data = objects.find(data => data.id === id); if (!data) return undefined; diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index 2cf86b7e3..b28d96596 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; @@ -21,14 +21,12 @@ export type ImportPullRequestError = | "ALREADY_IMPORTED" | "NOT_APPROVED"; -export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase { +export class ImportPullRequestUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute( notificationId: string @@ -52,23 +50,31 @@ export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase if (remoteNotification.status === "PENDING" || remoteNotification.status === "REJECTED") return Either.error("NOT_APPROVED"); - const result = await this.metadataRepository(this.localInstance).save( - remoteNotification.payload - ); + const result = await this.repositoryFactory + .metadataRepository(this.localInstance) + .save(remoteNotification.payload); const status: PullRequestStatus = result.status === "SUCCESS" ? "IMPORTED" : "IMPORTED_WITH_ERRORS"; const payload = status === "IMPORTED" ? {} : remoteNotification.payload; - await this.storageRepository( - this.localInstance - ).saveObjectInCollection(Namespace.NOTIFICATIONS, { ...notification, read: true, status }); - - await this.storageRepository(remoteInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - { ...remoteNotification, read: false, status, payload } - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, { + ...notification, + read: true, + status, + }); + + await this.repositoryFactory + .storageRepository(remoteInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, { + ...remoteNotification, + read: false, + status, + payload, + }); await this.sendMessage( remoteInstance, @@ -80,9 +86,9 @@ export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase } private async getInstanceById(id: string): Promise { - const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< - InstanceData - >(Namespace.INSTANCES); + const objects = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.INSTANCES); const data = objects.find(data => data.id === id); if (!data) return undefined; @@ -94,10 +100,9 @@ export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase instance: Instance, id: string ): Promise { - return await this.storageRepository(instance).getObjectInCollection( - Namespace.NOTIFICATIONS, - id - ); + return await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.NOTIFICATIONS, id); } private async sendMessage( @@ -126,7 +131,7 @@ export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase `More details at: ${instance.url}/api/apps/MetaData-Synchronization/index.html#/notifications/${id}`, ]; - await this.instanceRepository(instance).sendMessage({ + await this.repositoryFactory.instanceRepository(instance).sendMessage({ subject: `[MDSync] ${title}: ${subject}`, text: message.join("\n\n"), users: users.map(({ id }) => ({ id })), @@ -135,9 +140,9 @@ export class ImportPullRequestUseCase extends DefaultUseCase implements UseCase } private async getResponsibleNames(instance: Instance, ids: string[]) { - const responsibles = await this.storageRepository(instance).listObjectsInCollection< - MetadataResponsible - >(Namespace.RESPONSIBLES); + const responsibles = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.RESPONSIBLES); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 0750784af..59d69f007 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -1,22 +1,22 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { AppNotification } from "../entities/Notification"; -export class ListNotificationsUseCase extends DefaultUseCase implements UseCase { +export class ListNotificationsUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute(): Promise { - const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); + const { id, userGroups } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); const notifications = await this.getInstanceNotifications(); const sentPullRequestNotifications = notifications.filter( @@ -43,15 +43,15 @@ export class ListNotificationsUseCase extends DefaultUseCase implements UseCase } private async getInstanceNotifications(): Promise { - return this.storageRepository(this.localInstance).listObjectsInCollection( - Namespace.NOTIFICATIONS - ); + return this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.NOTIFICATIONS); } private async getInstanceById(id: string): Promise { - const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< - InstanceData - >(Namespace.INSTANCES); + const objects = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.INSTANCES); const data = objects.find(data => data.id === id); if (!data) return undefined; @@ -68,9 +68,12 @@ export class ListNotificationsUseCase extends DefaultUseCase implements UseCase const instance = await this.getInstanceById(notification.instance.id); if (!instance) return undefined; - const remoteNotification = await this.storageRepository(instance).getObjectInCollection< - AppNotification - >(Namespace.NOTIFICATIONS, notification.remoteNotification); + const remoteNotification = await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection( + Namespace.NOTIFICATIONS, + notification.remoteNotification + ); if ( !remoteNotification || @@ -86,10 +89,9 @@ export class ListNotificationsUseCase extends DefaultUseCase implements UseCase status: remoteNotification.status, }; - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - newNotification - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); return newNotification; } diff --git a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts index a11ff6e03..25b5128d5 100644 --- a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts @@ -1,19 +1,17 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { promiseMap } from "../../../utils/common"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { AppNotification } from "../entities/Notification"; -export class MarkReadNotificationsUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class MarkReadNotificationsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(ids: string[], read: boolean): Promise { - const notifications = await this.storageRepository( - this.localInstance - ).listObjectsInCollection(Namespace.NOTIFICATIONS); + const notifications = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.NOTIFICATIONS); if (!notifications) return; const targetNotifications = notifications.filter(({ id }) => ids.includes(id)); @@ -22,18 +20,19 @@ export class MarkReadNotificationsUseCase extends DefaultUseCase implements UseC const hasPermissions = await this.hasPermissions(notification); if (!hasPermissions) return; - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - { + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, { ...notification, read, - } - ); + }); }); } private async hasPermissions(notification: AppNotification) { - const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); + const { id, userGroups } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); if ( notification.owner.id !== id && diff --git a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts index 059712952..6fc10edcd 100644 --- a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts +++ b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; @@ -12,18 +12,16 @@ import { export type UpdatePullRequestStatusError = "NOT_FOUND" | "PERMISSIONS" | "INVALID"; -export class UpdatePullRequestStatusUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class UpdatePullRequestStatusUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( id: string, status: PullRequestStatus ): Promise> { - const notification = await this.storageRepository(this.localInstance).getObjectInCollection< - ReceivedPullRequestNotification - >(Namespace.NOTIFICATIONS, id); + const notification = await this.repositoryFactory + .storageRepository(this.localInstance) + .getObjectInCollection(Namespace.NOTIFICATIONS, id); if (!notification) { return Either.error("NOT_FOUND"); @@ -40,17 +38,18 @@ export class UpdatePullRequestStatusUseCase extends DefaultUseCase implements Us status, }; - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - newNotification - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); return Either.success(undefined); } private async hasPermissions(ids: string[]) { const responsibles = await this.getResponsibles(this.localInstance, ids); - const { id, userGroups } = await this.instanceRepository(this.localInstance).getUser(); + const { id, userGroups } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); if ( !responsibles.users?.find(user => user.id === id) && @@ -63,9 +62,9 @@ export class UpdatePullRequestStatusUseCase extends DefaultUseCase implements Us } private async getResponsibles(instance: Instance, ids: string[]) { - const responsibles = await this.storageRepository(instance).listObjectsInCollection< - MetadataResponsible - >(Namespace.RESPONSIBLES); + const responsibles = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.RESPONSIBLES); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts index b913dfb01..79218429a 100644 --- a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts @@ -1,22 +1,20 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { ImportedPackageData } from "../entities/ImportedPackage"; type ListImportedPackageError = "UNEXPECTED_ERROR"; -export class ListImportedPackagesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListImportedPackagesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(): Promise> { try { - const items = await this.storageRepository(this.localInstance).listObjectsInCollection< - ImportedPackageData - >(Namespace.IMPORTEDPACKAGES); + const items = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.IMPORTEDPACKAGES); return Either.success(items); } catch (error) { diff --git a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts index b2ee74218..157c6768e 100644 --- a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts @@ -1,25 +1,22 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { ImportedPackage } from "../entities/ImportedPackage"; type SavePackageError = "UNEXPECTED_ERROR"; -export class SaveImportedPackagesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class SaveImportedPackagesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( importedPackages: ImportedPackage[] ): Promise> { try { - await this.storageRepository(this.localInstance).saveObjectsInCollection( - Namespace.IMPORTEDPACKAGES, - importedPackages - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectsInCollection(Namespace.IMPORTEDPACKAGES, importedPackages); return Either.success(undefined); } catch (error) { diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index 5e60e9e91..1f85e00bb 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -3,7 +3,7 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { metadataTransformations } from "../../../data/transformations/PackageTransformations"; import { CompositionRoot } from "../../../presentation/CompositionRoot"; import { getMajorVersion } from "../../../utils/d2-utils"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; @@ -11,14 +11,12 @@ import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { Module } from "../../modules/entities/Module"; import { Package } from "../entities/Package"; -export class CreatePackageUseCase extends DefaultUseCase implements UseCase { +export class CreatePackageUseCase implements UseCase { constructor( private compositionRoot: CompositionRoot, - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance - ) { - super(repositoryFactory); - } + ) {} public async execute( originInstance: string, @@ -37,18 +35,18 @@ export class CreatePackageUseCase extends DefaultUseCase implements UseCase { targetInstances: [], }).buildPayload(); - const versionedPayload = this.transformationRepository().mapPackageTo( - apiVersion, - basePayload, - metadataTransformations - ); + const versionedPayload = this.repositoryFactory + .transformationRepository() + .mapPackageTo(apiVersion, basePayload, metadataTransformations); const payload = sourcePackage.update({ contents: versionedPayload, dhisVersion }); const validations = payload.validate(); if (validations.length === 0) { - const user = await this.instanceRepository(this.localInstance).getUser(); + const user = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); const newPackage = payload.update({ id: generateUid(), lastUpdated: new Date(), @@ -56,16 +54,14 @@ export class CreatePackageUseCase extends DefaultUseCase implements UseCase { user: payload.user.id ? payload.user : user, }); - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.PACKAGES, - newPackage - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.PACKAGES, newPackage); const newModule = module.update({ lastPackageVersion: newPackage.version }); - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.MODULES, - newModule - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.MODULES, newModule); } return validations; diff --git a/src/domain/packages/usecases/DeletePackageUseCase.ts b/src/domain/packages/usecases/DeletePackageUseCase.ts index 7c1064319..0f83bf55f 100644 --- a/src/domain/packages/usecases/DeletePackageUseCase.ts +++ b/src/domain/packages/usecases/DeletePackageUseCase.ts @@ -1,28 +1,27 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BasePackage } from "../entities/Package"; -export class DeletePackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class DeletePackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { try { - const item = await this.storageRepository(instance).getObjectInCollection( - Namespace.PACKAGES, - id - ); + const item = await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.PACKAGES, id); if (!item) return false; - await this.storageRepository(instance).saveObjectInCollection(Namespace.PACKAGES, { - ...item, - deleted: true, - contents: {}, - }); + await this.repositoryFactory + .storageRepository(instance) + .saveObjectInCollection(Namespace.PACKAGES, { + ...item, + deleted: true, + contents: {}, + }); } catch (error) { return false; } diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index 4b5149c96..33ab4e993 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -1,5 +1,5 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; @@ -12,14 +12,12 @@ import { BasePackage } from "./../entities/Package"; type DiffPackageUseCaseError = "PACKAGE_NOT_FOUND" | "MODULE_NOT_FOUND" | "NETWORK_ERROR"; -export class DiffPackageUseCase extends DefaultUseCase implements UseCase { +export class DiffPackageUseCase implements UseCase { constructor( private compositionRoot: CompositionRoot, - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance - ) { - super(repositoryFactory); - } + ) {} public async execute( packageIdBase: string | undefined, @@ -39,9 +37,9 @@ export class DiffPackageUseCase extends DefaultUseCase implements UseCase { contentsBase = packageBase.contents; } else { // No package B specified, use local contents - const moduleDataMerge = await this.storageRepository(instance).getObjectInCollection< - BaseModule - >(Namespace.MODULES, packageMerge.module.id); + const moduleDataMerge = await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.MODULES, packageMerge.module.id); if (!moduleDataMerge) return Either.error("MODULE_NOT_FOUND"); const moduleMerge = MetadataModule.build(moduleDataMerge); @@ -67,24 +65,25 @@ export class DiffPackageUseCase extends DefaultUseCase implements UseCase { } private async getDataStorePackage(id: string, instance: Instance) { - return this.storageRepository(instance).getObjectInCollection( - Namespace.PACKAGES, - id - ); + return this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.PACKAGES, id); } private async getStorePackage(storeId: string, url: string) { - const store = await this.storeRepository(this.localInstance).getById(storeId); + const store = await this.repositoryFactory + .storeRepository(this.localInstance) + .getById(storeId); if (!store) return undefined; - const { encoding, content } = await this.gitRepository().request<{ + const { encoding, content } = await this.repositoryFactory.gitRepository().request<{ encoding: string; content: string; }>(store, url); - const validation = this.gitRepository().readFileContents< - MetadataPackage & { package: BasePackage } - >(encoding, content); + const validation = this.repositoryFactory + .gitRepository() + .readFileContents(encoding, content); if (!validation.value.data) return undefined; const { package: basePackage, ...contents } = validation.value.data; diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index 198de0508..6cf21cf53 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -1,16 +1,14 @@ import _ from "lodash"; import moment from "moment"; import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { BasePackage } from "../entities/Package"; -export class DownloadPackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class DownloadPackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(storeId: string | undefined, id: string, instance = this.localInstance) { const element = storeId @@ -23,28 +21,29 @@ export class DownloadPackageUseCase extends DefaultUseCase implements UseCase { const date = moment().format("YYYYMMDDHHmm"); const name = `package-${ruleName}-${date}.json`; const payload = { package: item, ...contents }; - this.downloadRepository().downloadFile(name, payload); + this.repositoryFactory.downloadRepository().downloadFile(name, payload); } private async getDataStorePackage(id: string, instance: Instance) { - return this.storageRepository(instance).getObjectInCollection( - Namespace.PACKAGES, - id - ); + return this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.PACKAGES, id); } private async getStorePackage(storeId: string, url: string) { - const store = await this.storeRepository(this.localInstance).getById(storeId); + const store = await this.repositoryFactory + .storeRepository(this.localInstance) + .getById(storeId); if (!store) return undefined; - const { encoding, content } = await this.gitRepository().request<{ + const { encoding, content } = await this.repositoryFactory.gitRepository().request<{ encoding: string; content: string; }>(store, url); - const validation = this.gitRepository().readFileContents< - MetadataPackage & { package: BasePackage } - >(encoding, content); + const validation = this.repositoryFactory + .gitRepository() + .readFileContents(encoding, content); if (!validation.value.data) return undefined; const { package: basePackage, ...contents } = validation.value.data; diff --git a/src/domain/packages/usecases/GetPackageUseCase.ts b/src/domain/packages/usecases/GetPackageUseCase.ts index 55a5707bd..1245e46c1 100644 --- a/src/domain/packages/usecases/GetPackageUseCase.ts +++ b/src/domain/packages/usecases/GetPackageUseCase.ts @@ -1,23 +1,20 @@ import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BasePackage, Package } from "../entities/Package"; -export class GetPackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetPackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( id: string, instance = this.localInstance ): Promise> { - const data = await this.storageRepository(instance).getObjectInCollection( - Namespace.PACKAGES, - id - ); + const data = await this.repositoryFactory + .storageRepository(instance) + .getObjectInCollection(Namespace.PACKAGES, id); if (data) return Either.success(Package.build(data)); else return Either.error("NOT_FOUND"); diff --git a/src/domain/packages/usecases/GetStorePackageUseCase.ts b/src/domain/packages/usecases/GetStorePackageUseCase.ts index f52c093a0..b58cc8a5d 100644 --- a/src/domain/packages/usecases/GetStorePackageUseCase.ts +++ b/src/domain/packages/usecases/GetStorePackageUseCase.ts @@ -1,31 +1,31 @@ import _ from "lodash"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; import { BasePackage, Package } from "../entities/Package"; -export class GetStorePackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class GetStorePackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( storeId: string, packageId: string ): Promise> { - const store = await this.storeRepository(this.localInstance).getById(storeId); + const store = await this.repositoryFactory + .storeRepository(this.localInstance) + .getById(storeId); if (!store) return Either.error("NOT_FOUND"); - const { encoding, content } = await this.gitRepository().request<{ + const { encoding, content } = await this.repositoryFactory.gitRepository().request<{ encoding: string; content: string; }>(store, packageId); - const readFileResult = this.gitRepository().readFileContents< - MetadataPackage & { package: BasePackage } - >(encoding, content); + const readFileResult = this.repositoryFactory + .gitRepository() + .readFileContents(encoding, content); if (readFileResult.isError()) return Either.error("NOT_FOUND"); diff --git a/src/domain/packages/usecases/ImportPackageUseCase.ts b/src/domain/packages/usecases/ImportPackageUseCase.ts index 0a0fecf5f..d9b06e0c1 100644 --- a/src/domain/packages/usecases/ImportPackageUseCase.ts +++ b/src/domain/packages/usecases/ImportPackageUseCase.ts @@ -1,5 +1,5 @@ import { debug } from "../../../utils/debug"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; @@ -8,10 +8,8 @@ import { MappingMapper } from "../../mapping/helpers/MappingMapper"; import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; import { Package } from "../entities/Package"; -export class ImportPackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ImportPackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( item: Package, @@ -19,12 +17,12 @@ export class ImportPackageUseCase extends DefaultUseCase implements UseCase { originInstance: DataSource, destinationInstance: DataSource = this.localInstance ): Promise { - const originCategoryOptionCombos = await this.metadataRepository( - originInstance - ).getCategoryOptionCombos(); - const destinationCategoryOptionCombos = await this.metadataRepository( - destinationInstance - ).getCategoryOptionCombos(); + const originCategoryOptionCombos = await this.repositoryFactory + .metadataRepository(originInstance) + .getCategoryOptionCombos(); + const destinationCategoryOptionCombos = await this.repositoryFactory + .metadataRepository(destinationInstance) + .getCategoryOptionCombos(); const mapper = new MappingMapper( mapping, @@ -33,7 +31,9 @@ export class ImportPackageUseCase extends DefaultUseCase implements UseCase { ); const payload = mapper.applyMapping(item.contents); - const result = await this.metadataRepository(destinationInstance).save(payload); + const result = await this.repositoryFactory + .metadataRepository(destinationInstance) + .save(payload); debug("Import package", { originInstance, diff --git a/src/domain/packages/usecases/ListPackagesUseCase.ts b/src/domain/packages/usecases/ListPackagesUseCase.ts index a9ad03e0f..2c4b68248 100644 --- a/src/domain/packages/usecases/ListPackagesUseCase.ts +++ b/src/domain/packages/usecases/ListPackagesUseCase.ts @@ -1,25 +1,27 @@ import { Namespace } from "../../../data/storage/Namespaces"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataModule } from "../../modules/entities/MetadataModule"; import { BasePackage, Package } from "../entities/Package"; -export class ListPackagesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListPackagesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( bypassSharingSettings = false, instance = this.localInstance ): Promise { - const userGroups = await this.instanceRepository(this.localInstance).getUserGroups(); - const { id: userId } = await this.instanceRepository(this.localInstance).getUser(); + const userGroups = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUserGroups(); + const { id: userId } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); - const items = await this.storageRepository(instance).listObjectsInCollection( - Namespace.PACKAGES - ); + const items = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.PACKAGES); return items .filter(({ deleted }) => !deleted) diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index 48796ece8..40eaf5670 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import moment from "moment"; import { promiseMap } from "../../../utils/common"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataModule } from "../../modules/entities/MetadataModule"; @@ -14,17 +14,19 @@ import { moduleFile } from "../repositories/GitHubRepository"; export type ListStorePackagesError = GitHubError | "STORE_NOT_FOUND"; -export class ListStorePackagesUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class ListStorePackagesUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(storeId: string): Promise> { - const store = await this.storeRepository(this.localInstance).getById(storeId); + const store = await this.repositoryFactory + .storeRepository(this.localInstance) + .getById(storeId); if (!store) return Either.error("STORE_NOT_FOUND"); - const userGroups = await this.instanceRepository(this.localInstance).getUserGroups(); - const validation = await this.gitRepository().listBranches(store); + const userGroups = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUserGroups(); + const validation = await this.repositoryFactory.gitRepository().listBranches(store); if (validation.isError()) return Either.error(validation.value.error); const branches = validation.value.data?.flatMap(({ name }) => name) ?? []; @@ -46,7 +48,7 @@ export class ListStorePackagesUseCase extends DefaultUseCase implements UseCase store: Store, userGroup: string ): Promise> { - const validation = await this.gitRepository().listFiles(store, userGroup); + const validation = await this.repositoryFactory.gitRepository().listFiles(store, userGroup); if (validation.isError()) return Either.error(validation.value.error); @@ -80,12 +82,14 @@ export class ListStorePackagesUseCase extends DefaultUseCase implements UseCase if (!moduleFileUrl) return unknownModule; - const { encoding, content } = await this.gitRepository().request<{ + const { encoding, content } = await this.repositoryFactory.gitRepository().request<{ encoding: string; content: string; }>(store, moduleFileUrl); - const readFileResult = this.gitRepository().readFileContents(encoding, content); + const readFileResult = this.repositoryFactory + .gitRepository() + .readFileContents(encoding, content); return readFileResult.match({ success: module => module, diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index 79b920bf7..81db3c051 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -1,7 +1,7 @@ import moment from "moment"; import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { BaseModule } from "../../modules/entities/Module"; @@ -16,21 +16,21 @@ export type PublishStorePackageError = | "PACKAGE_NOT_FOUND" | "ALREADY_PUBLISHED"; -export class PublishStorePackageUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class PublishStorePackageUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute( packageId: string, force = false ): Promise> { - const defaultStore = await this.storeRepository(this.localInstance).getDefault(); + const defaultStore = await this.repositoryFactory + .storeRepository(this.localInstance) + .getDefault(); if (!defaultStore) return Either.error("DEFAULT_STORE_NOT_FOUND"); - const storedPackage = await this.storageRepository( - this.localInstance - ).getObjectInCollection(Namespace.PACKAGES, packageId); + const storedPackage = await this.repositoryFactory + .storageRepository(this.localInstance) + .getObjectInCollection(Namespace.PACKAGES, packageId); if (!storedPackage) return Either.error("PACKAGE_NOT_FOUND"); const { contents, ...item } = storedPackage; @@ -40,25 +40,21 @@ export class PublishStorePackageUseCase extends DefaultUseCase implements UseCas const path = `${item.module.name}/${fileName}.json`; const branch = item.module.department.name.replace(/\s/g, "-"); - const existingFileCheck = await this.gitRepository().readFile(defaultStore, branch, path); + const existingFileCheck = await this.repositoryFactory + .gitRepository() + .readFile(defaultStore, branch, path); if (existingFileCheck.isSuccess()) return Either.error("ALREADY_PUBLISHED"); - const validation = await this.gitRepository().writeFile( - defaultStore, - branch, - path, - JSON.stringify(payload, null, 4) - ); + const validation = await this.repositoryFactory + .gitRepository() + .writeFile(defaultStore, branch, path, JSON.stringify(payload, null, 4)); if (validation.isError()) { if (force && validation.value.error === "BRANCH_NOT_FOUND") { - await this.gitRepository().createBranch(defaultStore, branch); - const validation = await this.gitRepository().writeFile( - defaultStore, - branch, - path, - JSON.stringify(payload, null, 4) - ); + await this.repositoryFactory.gitRepository().createBranch(defaultStore, branch); + const validation = await this.repositoryFactory + .gitRepository() + .writeFile(defaultStore, branch, path, JSON.stringify(payload, null, 4)); if (validation.isError()) return Either.error(validation.value.error); } else { @@ -82,12 +78,9 @@ export class PublishStorePackageUseCase extends DefaultUseCase implements UseCas path: string, moduleRef: Pick ) { - const validation = await this.gitRepository().writeFile( - store, - branch, - path, - JSON.stringify(moduleRef, null, 4) - ); + const validation = await this.repositoryFactory + .gitRepository() + .writeFile(store, branch, path, JSON.stringify(moduleRef, null, 4)); if (validation.isError()) { return console.warn("An error creating the module file has ocurred"); diff --git a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts index a18687d79..b3f5fc8bb 100644 --- a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts +++ b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts @@ -1,7 +1,7 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { NamedRef } from "../../common/entities/Ref"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; import { MetadataPackage } from "../../metadata/entities/MetadataEntities"; @@ -23,10 +23,8 @@ interface CreatePullRequestParams { notificationUsers: Pick; } -export class CreatePullRequestUseCase extends DefaultUseCase implements UseCase { - constructor(repositoryFactory: RepositoryFactory, private localInstance: Instance) { - super(repositoryFactory); - } +export class CreatePullRequestUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute({ instance, @@ -63,21 +61,21 @@ export class CreatePullRequestUseCase extends DefaultUseCase implements UseCase remoteNotification: receivedPullRequest.id, }); - await this.storageRepository(instance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - receivedPullRequest - ); + await this.repositoryFactory + .storageRepository(instance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, receivedPullRequest); - await this.storageRepository(this.localInstance).saveObjectInCollection( - Namespace.NOTIFICATIONS, - sentPullRequest - ); + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.NOTIFICATIONS, sentPullRequest); await this.sendMessage(instance, receivedPullRequest); } private async getOwner(): Promise { - const { id, name } = await this.instanceRepository(this.localInstance).getUser(); + const { id, name } = await this.repositoryFactory + .instanceRepository(this.localInstance) + .getUser(); return { id, name }; } @@ -106,7 +104,7 @@ export class CreatePullRequestUseCase extends DefaultUseCase implements UseCase `More details at: ${instance.url}/api/apps/MetaData-Synchronization/index.html#/notifications/${id}`, ]; - await this.instanceRepository(instance).sendMessage({ + await this.repositoryFactory.instanceRepository(instance).sendMessage({ subject: `[MDSync] Received Pull Request: ${subject}`, text: message.join("\n\n"), users: users.map(({ id }) => ({ id })), @@ -115,9 +113,9 @@ export class CreatePullRequestUseCase extends DefaultUseCase implements UseCase } private async getResponsibleNames(instance: Instance, ids: string[]) { - const responsibles = await this.storageRepository(instance).listObjectsInCollection< - MetadataResponsible - >(Namespace.RESPONSIBLES); + const responsibles = await this.repositoryFactory + .storageRepository(instance) + .listObjectsInCollection(Namespace.RESPONSIBLES); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 2e8290ae5..5d6a6d527 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -11,7 +11,7 @@ import { getD2APiFromInstance } from "../../../utils/d2-utils"; import { debug } from "../../../utils/debug"; import { AggregatedPackage } from "../../aggregated/entities/AggregatedPackage"; import { AggregatedSyncUseCase } from "../../aggregated/usecases/AggregatedSyncUseCase"; -import { DefaultUseCase } from "../../common/entities/UseCase"; + import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { EventsPackage } from "../../events/entities/EventsPackage"; import { EventsSyncUseCase } from "../../events/usecases/EventsSyncUseCase"; @@ -35,7 +35,7 @@ export type SyncronizationClass = | typeof DeletedMetadataSyncUseCase; export type SyncronizationPayload = MetadataPackage | AggregatedPackage | EventsPackage; -export abstract class GenericSyncUseCase extends DefaultUseCase { +export abstract class GenericSyncUseCase { public abstract readonly type: SynchronizationType; public readonly fields: string = "id,name"; protected readonly api: D2Api; @@ -46,7 +46,6 @@ export abstract class GenericSyncUseCase extends DefaultUseCase { protected readonly localInstance: Instance, protected readonly encryptionKey: string ) { - super(repositoryFactory); this.api = getD2APiFromInstance(localInstance); } @@ -74,30 +73,30 @@ export abstract class GenericSyncUseCase extends DefaultUseCase { @cache() protected async getInstanceRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.instanceRepository(remoteInstance ?? defaultInstance); + return this.repositoryFactory.instanceRepository(remoteInstance ?? defaultInstance); } @cache() protected getTransformationRepository() { - return this.transformationRepository(); + return this.repositoryFactory.transformationRepository(); } @cache() protected async getMetadataRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.metadataRepository(remoteInstance ?? defaultInstance); + return this.repositoryFactory.metadataRepository(remoteInstance ?? defaultInstance); } @cache() protected async getAggregatedRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.aggregatedRepository(remoteInstance ?? defaultInstance); + return this.repositoryFactory.aggregatedRepository(remoteInstance ?? defaultInstance); } @cache() protected async getEventsRepository(remoteInstance?: Instance) { const defaultInstance = await this.getOriginInstance(); - return this.eventsRepository(remoteInstance ?? defaultInstance); + return this.repositoryFactory.eventsRepository(remoteInstance ?? defaultInstance); } @cache() @@ -162,16 +161,16 @@ export abstract class GenericSyncUseCase extends DefaultUseCase { private async getInstanceById(id: string): Promise { if (id === "LOCAL") return this.localInstance; - const data = await this.storageRepository(this.localInstance).getObjectInCollection< - InstanceData - >(Namespace.INSTANCES, id); + const data = await this.repositoryFactory + .storageRepository(this.localInstance) + .getObjectInCollection(Namespace.INSTANCES, id); if (!data) return undefined; const instance = Instance.build(data).decryptPassword(this.encryptionKey); try { - const version = await this.instanceRepository(instance).getVersion(); + const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); return instance.update({ version }); } catch (error) { return instance; diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index a6a389af9..8d275af5b 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -2,7 +2,7 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import { SynchronizationBuilder } from "../../../types/synchronization"; import { Either } from "../../common/entities/Either"; -import { DefaultUseCase, UseCase } from "../../common/entities/UseCase"; +import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; @@ -10,14 +10,12 @@ import { SynchronizationType } from "../entities/SynchronizationType"; export type PrepareSyncError = "PULL_REQUEST" | "PULL_REQUEST_RESPONSIBLE" | "INSTANCE_NOT_FOUND"; -export class PrepareSyncUseCase extends DefaultUseCase implements UseCase { +export class PrepareSyncUseCase implements UseCase { constructor( - repositoryFactory: RepositoryFactory, + private repositoryFactory: RepositoryFactory, private localInstance: Instance, private encryptionKey: string - ) { - super(repositoryFactory); - } + ) {} public async execute( type: SynchronizationType, @@ -65,7 +63,7 @@ export class PrepareSyncUseCase extends DefaultUseCase implements UseCase { } private async getCurrentUser() { - return this.instanceRepository(this.localInstance).getUser(); + return this.repositoryFactory.instanceRepository(this.localInstance).getUser(); } private async getResponsiblesForInstance( @@ -74,9 +72,9 @@ export class PrepareSyncUseCase extends DefaultUseCase implements UseCase { const instance = await this.getInstanceById(instanceId); if (instance.isError() || !instance.value.data) return Either.error("INSTANCE_NOT_FOUND"); - const responsibles = await this.storageRepository( - instance.value.data - ).listObjectsInCollection(Namespace.RESPONSIBLES); + const responsibles = await this.repositoryFactory + .storageRepository(instance.value.data) + .listObjectsInCollection(Namespace.RESPONSIBLES); return Either.success(responsibles); } @@ -84,15 +82,15 @@ export class PrepareSyncUseCase extends DefaultUseCase implements UseCase { private async getInstanceById(id: string): Promise> { if (id === "LOCAL") return Either.success(this.localInstance); - const objects = await this.storageRepository(this.localInstance).listObjectsInCollection< - InstanceData - >(Namespace.INSTANCES); + const objects = await this.repositoryFactory + .storageRepository(this.localInstance) + .listObjectsInCollection(Namespace.INSTANCES); const data = objects.find(data => data.id === id); if (!data) return Either.error("INSTANCE_NOT_FOUND"); const instance = Instance.build(data).decryptPassword(this.encryptionKey); - const version = await this.instanceRepository(instance).getVersion(); + const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); return Either.success(instance.update({ version })); } diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index df3822485..4f417bb1a 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -6,10 +6,11 @@ import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository" import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; import { StorageDataStoreClient } from "../data/storage/StorageDataStoreClient"; +import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; -import { RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; +import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; import { Instance } from "../domain/instance/entities/Instance"; @@ -50,27 +51,25 @@ import { ListImportedPackagesUseCase } from "../domain/package-import/usecases/L import { SaveImportedPackagesUseCase } from "../domain/package-import/usecases/SaveImportedPackagesUseCase"; import { CreatePackageUseCase } from "../domain/packages/usecases/CreatePackageUseCase"; import { DeletePackageUseCase } from "../domain/packages/usecases/DeletePackageUseCase"; -import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; import { DiffPackageUseCase } from "../domain/packages/usecases/DiffPackageUseCase"; import { DownloadPackageUseCase } from "../domain/packages/usecases/DownloadPackageUseCase"; import { GetPackageUseCase } from "../domain/packages/usecases/GetPackageUseCase"; import { GetStorePackageUseCase } from "../domain/packages/usecases/GetStorePackageUseCase"; -import { GetStoreUseCase } from "../domain/stores/usecases/GetStoreUseCase"; import { ImportPackageUseCase } from "../domain/packages/usecases/ImportPackageUseCase"; import { ListPackagesUseCase } from "../domain/packages/usecases/ListPackagesUseCase"; import { ListStorePackagesUseCase } from "../domain/packages/usecases/ListStorePackagesUseCase"; -import { ListStoresUseCase } from "../domain/stores/usecases/ListStoresUseCase"; import { PublishStorePackageUseCase } from "../domain/packages/usecases/PublishStorePackageUseCase"; +import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; +import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; +import { GetStoreUseCase } from "../domain/stores/usecases/GetStoreUseCase"; +import { ListStoresUseCase } from "../domain/stores/usecases/ListStoresUseCase"; import { SaveStoreUseCase } from "../domain/stores/usecases/SaveStoreUseCase"; import { SetStoreAsDefaultUseCase } from "../domain/stores/usecases/SetStoreAsDefaultUseCase"; import { ValidateStoreUseCase } from "../domain/stores/usecases/ValidateStoreUseCase"; -import { Repositories } from "../domain/Repositories"; -import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/CreatePullRequestUseCase"; import { PrepareSyncUseCase } from "../domain/synchronization/usecases/PrepareSyncUseCase"; import { SynchronizationBuilder } from "../types/synchronization"; import { cache } from "../utils/cache"; -import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; export class CompositionRoot { private repositoryFactory: RepositoryFactory; From 60eb8ba945a78224777b76e2b08b038312c57485 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 07:16:21 +0100 Subject: [PATCH 011/163] Inject encryption key --- src/domain/common/factories/RepositoryFactory.ts | 4 +++- src/domain/mapping/usecases/GenericMappingUseCase.ts | 1 - src/presentation/CompositionRoot.ts | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 4bf797046..34ac0227b 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -26,6 +26,8 @@ import { type ClassType = new (...args: any[]) => any; export class RepositoryFactory { + constructor(private encryptionKey: string) {} + private repositories: Map = new Map(); // TODO: TS 4.1 `${RepositoryKeys}-${string}` public bind(repository: RepositoryKeys, implementation: ClassType, tag = "default") { @@ -68,7 +70,7 @@ export class RepositoryFactory { public instanceRepository(instance: Instance) { return this.get(Repositories.InstanceRepository, [ instance, - "", + this.encryptionKey, ]); } diff --git a/src/domain/mapping/usecases/GenericMappingUseCase.ts b/src/domain/mapping/usecases/GenericMappingUseCase.ts index 4a1288e57..712d0c8b6 100644 --- a/src/domain/mapping/usecases/GenericMappingUseCase.ts +++ b/src/domain/mapping/usecases/GenericMappingUseCase.ts @@ -5,7 +5,6 @@ import { } from "../../../presentation/react/components/mapping-table/utils"; import { Dictionary } from "../../../types/utils"; import { NamedRef } from "../../common/entities/Ref"; - import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 4f417bb1a..1edee6776 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -75,7 +75,7 @@ export class CompositionRoot { private repositoryFactory: RepositoryFactory; constructor(public readonly localInstance: Instance, private encryptionKey: string) { - this.repositoryFactory = new RepositoryFactory(); + this.repositoryFactory = new RepositoryFactory(encryptionKey); this.repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); this.repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); this.repositoryFactory.bind(Repositories.DownloadRepository, DownloadWebRepository); From b33043895886cb56324c3f9f676a7e65819fc2b9 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 07:33:31 +0100 Subject: [PATCH 012/163] Prettify --- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 5d6a6d527..fb463fc55 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -11,7 +11,6 @@ import { getD2APiFromInstance } from "../../../utils/d2-utils"; import { debug } from "../../../utils/debug"; import { AggregatedPackage } from "../../aggregated/entities/AggregatedPackage"; import { AggregatedSyncUseCase } from "../../aggregated/usecases/AggregatedSyncUseCase"; - import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { EventsPackage } from "../../events/entities/EventsPackage"; import { EventsSyncUseCase } from "../../events/usecases/EventsSyncUseCase"; From f55c5233012262da494e924ce8d9ca36d0c88b68 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 07:55:41 +0100 Subject: [PATCH 013/163] Move back default assignment to use case --- src/data/stores/StoreD2ApiRepository.ts | 12 ++---------- .../packages/usecases/PublishStorePackageUseCase.ts | 1 + src/domain/stores/usecases/SaveStoreUseCase.ts | 10 +++++++++- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index bc12b356e..87e2ada41 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,10 +1,9 @@ import { Instance } from "../../domain/instance/entities/Instance"; -import { Namespace } from "../storage/Namespaces"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; +import { Namespace } from "../storage/Namespaces"; import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; -import { generateUid } from "d2/uid"; export class StoreD2ApiRepository implements StoreRepository { private storageClient: StorageClient; @@ -36,14 +35,7 @@ export class StoreD2ApiRepository implements StoreRepository { } public async save(store: Store): Promise { - const currentStores = await this.list(); - const isFirstStore = !store.id && currentStores.length === 0; - - await this.storageClient.saveObjectInCollection(Namespace.STORES, { - ...store, - id: store.id || generateUid(), - default: isFirstStore || store.default, - }); + await this.storageClient.saveObjectInCollection(Namespace.STORES, store); } public async getDefault(): Promise { diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index 81db3c051..dcc5ff71e 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -26,6 +26,7 @@ export class PublishStorePackageUseCase implements UseCase { const defaultStore = await this.repositoryFactory .storeRepository(this.localInstance) .getDefault(); + if (!defaultStore) return Either.error("DEFAULT_STORE_NOT_FOUND"); const storedPackage = await this.repositoryFactory diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index c609d2f7a..815aa64b7 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -1,3 +1,4 @@ +import { generateUid } from "d2/uid"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { GitHubError } from "../../packages/entities/Errors"; @@ -17,7 +18,14 @@ export class SaveStoreUseCase implements UseCase { if (validation.isError()) return Either.error(validation.value.error ?? "UNKNOWN"); } - await this.storeRepository.save(store); + const currentStores = await this.storeRepository.list(); + const isFirstStore = !store.id && currentStores.length === 0; + + await this.storeRepository.save({ + ...store, + id: store.id || generateUid(), + default: isFirstStore || store.default, + }); return Either.success(store); } From 2e15d4e61cbf7466bbe6713f1b4d7859c667b234 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 08:07:57 +0100 Subject: [PATCH 014/163] Update tests accordingly --- .../__tests__/integration/sync-aggregated.spec.ts | 8 +++++--- .../metadata/__tests__/integration/sync-events.spec.ts | 8 +++++--- .../metadata/__tests__/integration/sync-metadata.spec.ts | 8 +++++--- .../__tests__/integration/transformations-api-30.spec.ts | 8 +++++--- .../__tests__/integration/transformations-api-32.spec.ts | 8 +++++--- 5 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts index 0cad5dbde..4ebb881ac 100644 --- a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts @@ -2,9 +2,11 @@ import { Request, Server } from "miragejs"; import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; import { AggregatedSyncUseCase } from "../../../../domain/aggregated/usecases/AggregatedSyncUseCase"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; @@ -243,7 +245,7 @@ describe("Sync metadata", () => { }); function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-events.spec.ts b/src/data/metadata/__tests__/integration/sync-events.spec.ts index c9e475572..9dccef083 100644 --- a/src/data/metadata/__tests__/integration/sync-events.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-events.spec.ts @@ -1,10 +1,12 @@ import { Request, Server } from "miragejs"; import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { EventsSyncUseCase } from "../../../../domain/events/usecases/EventsSyncUseCase"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; @@ -275,7 +277,7 @@ describe("Sync metadata", () => { }); function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts index abbd0d3e7..ff69fe1e2 100644 --- a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts @@ -1,10 +1,12 @@ import { Request, Server } from "miragejs"; import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; @@ -142,7 +144,7 @@ describe("Sync metadata", () => { }); function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); diff --git a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts index 543805de8..537f57901 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts @@ -1,10 +1,12 @@ import { Request, Server } from "miragejs"; import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; @@ -396,7 +398,7 @@ describe("Sync metadata", () => { }); function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); diff --git a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts index 680e8073f..ce25cdfce 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts @@ -1,10 +1,12 @@ import { Request, Server } from "miragejs"; import { AnyRegistry } from "miragejs/-types"; import Schema from "miragejs/orm/schema"; -import { RepositoryFactory } from "../../../../domain/common/factories/RepositoryFactory"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { Repositories } from "../../../../domain/Repositories"; import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; @@ -274,7 +276,7 @@ describe("Sync metadata", () => { }); function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); From 92b37fa30b9597a1e0ca655fcefa6dd329bc5bd4 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 08:20:12 +0100 Subject: [PATCH 015/163] Add unique name validation --- .../instance/usecases/SaveInstanceUseCase.ts | 29 ++++++++++++++++--- .../instance-creation/GeneralInfoForm.tsx | 9 ++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index 56dd923cd..6fdad588b 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -1,8 +1,9 @@ import { Namespace } from "../../../data/storage/Namespaces"; +import i18n from "../../../locales"; import { UseCase } from "../../common/entities/UseCase"; import { ValidationError } from "../../common/entities/Validations"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; -import { Instance } from "../entities/Instance"; +import { Instance, InstanceData } from "../entities/Instance"; export class SaveInstanceUseCase implements UseCase { constructor( @@ -12,9 +13,29 @@ export class SaveInstanceUseCase implements UseCase { ) {} public async execute(instance: Instance): Promise { - const validations = instance.validate(); + // Find for other existing instance with same name + const existingInstances = await this.repositoryFactory + .storageRepository(this.localInstance) + .getObject(Namespace.INSTANCES); - if (validations.length === 0) { + const sameNameInstance = existingInstances?.find( + ({ name, id }) => id !== instance.id && name === instance.name + ); + + if (sameNameInstance) { + return [ + { + property: "name", + error: "name_exists", + description: i18n.t("An instance with this name already exists"), + }, + ]; + } + + // Validate model and save it if there're no errors + const modelValidations = instance.validate(); + + if (modelValidations.length === 0) { await this.repositoryFactory .storageRepository(this.localInstance) .saveObjectInCollection( @@ -23,6 +44,6 @@ export class SaveInstanceUseCase implements UseCase { ); } - return validations; + return modelValidations; } } diff --git a/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx b/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx index 9bc347a3a..70497b800 100644 --- a/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx +++ b/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx @@ -75,9 +75,14 @@ const GeneralInfoForm = ({ instance, onChange, cancelAction }: GeneralInfoFormPr } setIsSaving(true); - await compositionRoot.instances.save(instance); + const validationErrors = await compositionRoot.instances.save(instance); setIsSaving(false); - history.push("/instances"); + + if (validationErrors.length === 0) { + history.push("/instances"); + } else { + snackbar.error(validationErrors.map(({ description }) => description).join("\n")); + } }, [compositionRoot, errors, history, instance, snackbar]); return ( From 441f41093050adef7db93c019c860bd69680e669 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 08:54:52 +0100 Subject: [PATCH 016/163] Add migration to create local instance --- i18n/en.pot | 7 +++-- i18n/es.po | 5 +++- i18n/fr.po | 5 +++- i18n/pt.po | 5 +++- src/domain/instance/entities/DataSource.ts | 2 +- src/domain/instance/entities/Instance.ts | 14 +++++++--- src/migrations/tasks/06.this-instance.ts | 30 ++++++++++++++++++++++ src/migrations/tasks/index.ts | 2 ++ 8 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/migrations/tasks/06.this-instance.ts diff --git a/i18n/en.pot b/i18n/en.pot index a93693fad..2c4af5c41 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-25T07:21:05.637Z\n" +"PO-Revision-Date: 2020-11-25T07:21:05.637Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -20,6 +20,9 @@ msgstr "" msgid "Field {{field}} is not valid" msgstr "" +msgid "An instance with this name already exists" +msgstr "" + msgid "You need to provide a username and password combination" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6ce42123a..2f4932963 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-25T07:21:05.637Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,6 +20,9 @@ msgstr "" msgid "Field {{field}} is not valid" msgstr "" +msgid "An instance with this name already exists" +msgstr "" + msgid "You need to provide a username and password combination" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 153603391..1cb9311a1 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-25T07:21:05.637Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,6 +20,9 @@ msgstr "" msgid "Field {{field}} is not valid" msgstr "" +msgid "An instance with this name already exists" +msgstr "" + msgid "You need to provide a username and password combination" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 153603391..1cb9311a1 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-25T07:21:05.637Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,6 +20,9 @@ msgstr "" msgid "Field {{field}} is not valid" msgstr "" +msgid "An instance with this name already exists" +msgstr "" + msgid "You need to provide a username and password combination" msgstr "" diff --git a/src/domain/instance/entities/DataSource.ts b/src/domain/instance/entities/DataSource.ts index a5e9777be..5e6c30ff6 100644 --- a/src/domain/instance/entities/DataSource.ts +++ b/src/domain/instance/entities/DataSource.ts @@ -1,7 +1,7 @@ import { Instance } from "./Instance"; import { JSONDataSource } from "./JSONDataSource"; -export type DataSourceType = "dhis" | "json"; +export type DataSourceType = "local" | "dhis" | "json"; export type DataSource = Instance | JSONDataSource; diff --git a/src/domain/instance/entities/Instance.ts b/src/domain/instance/entities/Instance.ts index 1a954a0b2..c3ad7e6c0 100644 --- a/src/domain/instance/entities/Instance.ts +++ b/src/domain/instance/entities/Instance.ts @@ -6,8 +6,10 @@ import { ModelValidation, validateModel, ValidationError } from "../../common/en import { MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; export type PublicInstance = Omit; +export type InstanceType = "local" | "dhis"; export interface InstanceData { + type: InstanceType; id: string; name: string; url: string; @@ -19,13 +21,16 @@ export interface InstanceData { } export class Instance { - public type = "dhis" as const; private data: InstanceData; - constructor(data: InstanceData) { + private constructor(data: InstanceData) { this.data = data; } + public get type(): InstanceType { + return this.data.type; + } + public get id(): string { return this.data.id; } @@ -95,8 +100,9 @@ export class Instance { }); } - public static build(data: PartialBy): Instance { - return new Instance({ id: generateUid(), ...data }); + public static build(data: PartialBy): Instance { + const { type = "dhis", id = generateUid() } = data; + return new Instance({ type, id: type === "local" ? "LOCAL" : id, ...data }); } private moduleValidations = (): ModelValidation[] => [ diff --git a/src/migrations/tasks/06.this-instance.ts b/src/migrations/tasks/06.this-instance.ts new file mode 100644 index 000000000..24642c91f --- /dev/null +++ b/src/migrations/tasks/06.this-instance.ts @@ -0,0 +1,30 @@ +import _ from "lodash"; +import { Instance, InstanceData } from "../../domain/instance/entities/Instance"; +import { D2Api } from "../../types/d2-api"; + +export default async function migrate(api: D2Api): Promise { + const dataStore = api.dataStore("metadata-synchronization"); + const oldContents = await dataStore.get("instances").getData(); + if (!oldContents) return; + + const oldInstances = oldContents.map(({ name, ...rest }) => + Instance.build({ + ...rest, + name: + name === "This instance" + ? `Local Instance with user ${rest.username ?? "unknown"}` + : name, + }).toObject() + ); + + const localInstance = Instance.build({ + type: "local", + id: "LOCAL", + name: "This instance", + url: api.baseUrl, + }).toObject(); + + const instances = _.uniqBy([localInstance, ...oldInstances], "id"); + + await dataStore.save("instances", instances).getData(); +} diff --git a/src/migrations/tasks/index.ts b/src/migrations/tasks/index.ts index fb07812bf..3f160ad91 100644 --- a/src/migrations/tasks/index.ts +++ b/src/migrations/tasks/index.ts @@ -4,6 +4,7 @@ import Migration02 from "./02.rules-by-id"; import Migration03 from "./03.sync-reports"; import Migration04 from "./04.history-notifications"; import Migration05 from "./05.multiple-stores"; +import Migration06 from "./06.this-instance"; export const migrationTasks: Migration[] = [ { version: 1, name: "01.instances-by-id", fn: Migration01 }, @@ -11,4 +12,5 @@ export const migrationTasks: Migration[] = [ { version: 3, name: "03.sync-reports", fn: Migration03 }, { version: 4, name: "04.history-notifications", fn: Migration04 }, { version: 5, name: "05.multiple-stores", fn: Migration05 }, + { version: 6, name: "06.this-instance", fn: Migration06 }, ]; From 6b3d5752634868c0c59375f374b68082826f0d83 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 09:17:09 +0100 Subject: [PATCH 017/163] Update contextual actions --- src/data/metadata/MetadataD2ApiRepository.ts | 4 +-- src/domain/instance/entities/DataSource.ts | 2 +- .../instance/usecases/ListInstancesUseCase.ts | 6 +++- src/presentation/webapp/WebApp.jsx | 7 ++++- .../pages/instance-list/InstanceListPage.tsx | 28 ++++++++++++++++--- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index 7a53cc249..7f98d071b 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -3,7 +3,7 @@ import _ from "lodash"; import moment from "moment"; import { buildPeriodFromParams } from "../../domain/aggregated/utils"; import { IdentifiableRef, Ref } from "../../domain/common/entities/Ref"; -import { DataSource } from "../../domain/instance/entities/DataSource"; +import { DataSource, isDhisInstance } from "../../domain/instance/entities/DataSource"; import { Instance } from "../../domain/instance/entities/Instance"; import { DateFilter, @@ -43,7 +43,7 @@ export class MetadataD2ApiRepository implements MetadataRepository { private instance: Instance; constructor(instance: DataSource, private transformationRepository: TransformationRepository) { - if (instance.type !== "dhis") { + if (!isDhisInstance(instance)) { throw new Error("Invalid instance type for MetadataD2ApiRepository"); } diff --git a/src/domain/instance/entities/DataSource.ts b/src/domain/instance/entities/DataSource.ts index 5e6c30ff6..517e54f12 100644 --- a/src/domain/instance/entities/DataSource.ts +++ b/src/domain/instance/entities/DataSource.ts @@ -6,7 +6,7 @@ export type DataSourceType = "local" | "dhis" | "json"; export type DataSource = Instance | JSONDataSource; export const isDhisInstance = (source: DataSource): source is Instance => { - return source.type === "dhis"; + return source.type === "dhis" || source.type === "local"; }; export const isJSONDataSource = (source: DataSource): source is JSONDataSource => { diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index cdad5dd6a..fef3a4e99 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -32,7 +32,11 @@ export class ListInstancesUseCase implements UseCase { ) : objects; - return filteredData.map(data => { + const instances = filteredData.map(item => + item.type === "local" ? { ...item, url: this.localInstance.url } : item + ); + + return instances.map(data => { return Instance.build(data).decryptPassword(this.encryptionKey); }); } diff --git a/src/presentation/webapp/WebApp.jsx b/src/presentation/webapp/WebApp.jsx index be5278001..1c4bdb81d 100644 --- a/src/presentation/webapp/WebApp.jsx +++ b/src/presentation/webapp/WebApp.jsx @@ -57,7 +57,12 @@ const App = () => { const d2 = await init({ baseUrl: `${baseUrl}/api` }); const api = new D2Api({ baseUrl, backend: "fetch" }); const version = await api.getVersion(); - const instance = Instance.build({ name: "This instance", url: baseUrl, version }); + const instance = Instance.build({ + type: "local", + name: "This instance", + url: baseUrl, + version, + }); const compositionRoot = new CompositionRoot(instance, encryptionKey); diff --git a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx index d9b2e7a34..ed3b216f2 100644 --- a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx @@ -7,6 +7,7 @@ import { ConfirmationDialog, ObjectsTable, ObjectsTableDetailField, + RowConfig, TableAction, TableColumn, TableSelection, @@ -149,13 +150,22 @@ const InstanceListPage = () => { const columns: TableColumn[] = [ { name: "name" as const, text: i18n.t("Server name"), sortable: true }, { name: "url" as const, text: i18n.t("URL endpoint"), sortable: false }, - { name: "username" as const, text: i18n.t("Username"), sortable: true }, + { + name: "username" as const, + text: i18n.t("Username"), + sortable: true, + getValue: row => (row.type === "local" ? "Logged user" : row.username), + }, ]; const details: ObjectsTableDetailField[] = [ { name: "name" as const, text: i18n.t("Server name") }, { name: "url" as const, text: i18n.t("URL endpoint") }, - { name: "username" as const, text: i18n.t("Username") }, + { + name: "username" as const, + text: i18n.t("Username"), + getValue: row => (row.type === "local" ? "Logged user" : row.username), + }, { name: "description" as const, text: i18n.t("Description") }, ]; @@ -169,7 +179,7 @@ const InstanceListPage = () => { name: "edit", text: i18n.t("Edit"), multiple: false, - isActive: () => appConfigurator, + isActive: rows => appConfigurator && _.every(rows, row => row.type !== "local"), primary: true, onClick: editInstance, icon: , @@ -178,6 +188,7 @@ const InstanceListPage = () => { name: "replicate", text: i18n.t("Replicate"), multiple: false, + isActive: rows => appConfigurator && _.every(rows, row => row.type !== "local"), onClick: replicateInstance, icon: content_copy, }, @@ -185,7 +196,7 @@ const InstanceListPage = () => { name: "delete", text: i18n.t("Delete"), multiple: true, - isActive: () => appConfigurator, + isActive: rows => appConfigurator && _.every(rows, row => row.type !== "local"), onClick: deleteInstances, icon: , }, @@ -193,6 +204,7 @@ const InstanceListPage = () => { name: "testConnection", text: i18n.t("Test Connection"), multiple: false, + isActive: rows => _.every(rows, row => row.type !== "local"), onClick: testConnection, icon: , }, @@ -213,6 +225,13 @@ const InstanceListPage = () => { }, ]; + const rowConfig = React.useCallback( + (instance: Instance): RowConfig => ({ + cellStyle: instance.type === "local" ? { fontWeight: "bold" } : undefined, + }), + [] + ); + return ( { columns={columns} details={details} actions={actions} + rowConfig={rowConfig} onActionButtonClick={appConfigurator ? createInstance : undefined} onChangeSearch={changeSearch} selection={selection} From 432bdc257decce46e42eeafe2c4f280a0989c378 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 09:23:07 +0100 Subject: [PATCH 018/163] Hide duplicated items in dropdown --- .../InstanceSelectionDropdown.tsx | 27 +++++++++---------- src/presentation/widget/WidgetApp.jsx | 8 +++++- src/scheduler/cli.ts | 1 + 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx b/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx index 63ac7e1a0..909314749 100644 --- a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx +++ b/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx @@ -51,20 +51,19 @@ export const InstanceSelectionDropdown: React.FC [instances, stores, onChangeSelected] ); - const instanceItems = useMemo( - () => - _.compact([ - showInstances.local && { id: "LOCAL", name: i18n.t("This instance") }, - ...(showInstances.store - ? stores.map(store => ({ - id: store.id, - name: `${store.account} - ${store.repository} (${i18n.t("Store")})`, - })) - : []), - ...(showInstances.remote ? instances : []), - ]), - [showInstances, instances, stores] - ); + const instanceItems = useMemo(() => { + const localInstance = { id: "LOCAL", name: i18n.t("This instance") }; + const storeInstances = stores.map(store => ({ + id: store.id, + name: `${store.account} - ${store.repository} (${i18n.t("Store")})`, + })); + + return _.compact([ + showInstances.local && localInstance, + ...(showInstances.store ? storeInstances : []), + ...(showInstances.remote ? instances.filter(item => item.type === "dhis") : []), + ]); + }, [showInstances, instances, stores]); useEffect(() => { compositionRoot.instances.list().then(setInstances); diff --git a/src/presentation/widget/WidgetApp.jsx b/src/presentation/widget/WidgetApp.jsx index a39e4cf87..18f0f4195 100644 --- a/src/presentation/widget/WidgetApp.jsx +++ b/src/presentation/widget/WidgetApp.jsx @@ -37,7 +37,13 @@ const App = () => { const d2 = await init({ baseUrl: `${baseUrl}/api` }); const api = new D2Api({ baseUrl, backend: "fetch" }); - const instance = Instance.build({ name: "This instance", url: baseUrl }); + const version = await api.getVersion(); + const instance = Instance.build({ + type: "local", + name: "This instance", + url: baseUrl, + version, + }); const compositionRoot = new CompositionRoot(instance, encryptionKey); setAppContext({ d2, api, compositionRoot }); diff --git a/src/scheduler/cli.ts b/src/scheduler/cli.ts index d4a9b0e8a..7a7dfd17f 100644 --- a/src/scheduler/cli.ts +++ b/src/scheduler/cli.ts @@ -60,6 +60,7 @@ const start = async (): Promise => { const version = await api.getVersion(); const instance = Instance.build({ + type: "local", name: "This instance", url: baseUrl, username, From d40648baa92bae7a0739801801df27232b99e264 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 09:26:59 +0100 Subject: [PATCH 019/163] Remove old console.log --- .../react/components/metadata-table/MetadataTable.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/presentation/react/components/metadata-table/MetadataTable.tsx b/src/presentation/react/components/metadata-table/MetadataTable.tsx index ab3bc279c..3356149ea 100644 --- a/src/presentation/react/components/metadata-table/MetadataTable.tsx +++ b/src/presentation/react/components/metadata-table/MetadataTable.tsx @@ -427,7 +427,6 @@ const MetadataTable: React.FC = ({ .list({ ...filters, filterRows, fields, includeParents }, remoteInstance) .then(({ objects, pager }) => { const rows = model.getApiModelTransform()((objects as unknown) as MetadataType[]); - console.log(3, rows); notifyRowsChange(rows); setRows(rows); From 5cb29aaecd7bdf8bfcda3fb6f031541a39479e2a Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 09:36:44 +0100 Subject: [PATCH 020/163] Fix validations for local instance type --- src/domain/instance/entities/Instance.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/domain/instance/entities/Instance.ts b/src/domain/instance/entities/Instance.ts index c3ad7e6c0..180f38282 100644 --- a/src/domain/instance/entities/Instance.ts +++ b/src/domain/instance/entities/Instance.ts @@ -84,7 +84,10 @@ export class Instance { } public validate(filter?: string[]): ValidationError[] { - return validateModel(this, this.moduleValidations()).filter( + const validations = + this.type === "local" ? this.localInstanceValidations() : this.moduleValidations(); + + return validateModel(this, validations).filter( ({ property }) => filter?.includes(property) ?? true ); } @@ -113,6 +116,12 @@ export class Instance { { property: "password", validation: "hasText" }, ]; + private localInstanceValidations = (): ModelValidation[] => [ + { property: "name", validation: "hasText" }, + { property: "url", validation: "isUrl" }, + { property: "url", validation: "hasText" }, + ]; + public decryptPassword(encryptionKey: string): Instance { const password = this.password ? new Cryptr(encryptionKey).decrypt(this.password) : ""; return Instance.build({ ...this.data, password }); From 62d59ec7e6fede855f21269959d8e7416f294e9e Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 09:41:11 +0100 Subject: [PATCH 021/163] Remove assumptions of local instance --- src/domain/instance/usecases/GetInstanceByIdUseCase.ts | 2 -- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 2 -- src/domain/synchronization/usecases/PrepareSyncUseCase.ts | 2 -- .../react/components/sync-wizard/common/SummaryStep.jsx | 5 +---- 4 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index b1373c631..13f83eb1a 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -12,8 +12,6 @@ export class GetInstanceByIdUseCase implements UseCase { ) {} public async execute(id: string): Promise> { - if (id === "LOCAL") return Either.success(this.localInstance); - const data = await this.repositoryFactory .storageRepository(this.localInstance) .getObjectInCollection(Namespace.INSTANCES, id); diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index fb463fc55..b08b20233 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -158,8 +158,6 @@ export abstract class GenericSyncUseCase { } private async getInstanceById(id: string): Promise { - if (id === "LOCAL") return this.localInstance; - const data = await this.repositoryFactory .storageRepository(this.localInstance) .getObjectInCollection(Namespace.INSTANCES, id); diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 8d275af5b..3767b3249 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -80,8 +80,6 @@ export class PrepareSyncUseCase implements UseCase { } private async getInstanceById(id: string): Promise> { - if (id === "LOCAL") return Either.success(this.localInstance); - const objects = await this.repositoryFactory .storageRepository(this.localInstance) .listObjectsInCollection(Namespace.INSTANCES); diff --git a/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx b/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx index 12f06e361..4f694b7f2 100644 --- a/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx +++ b/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx @@ -102,10 +102,7 @@ const SaveStep = ({ syncRule, onCancel }) => { const destinationInstances = useMemo( () => _.compact( - syncRule.targetInstances.map(id => { - if (id === "LOCAL") return { value: id, text: "This instance" }; - return instanceOptions.find(e => e.value === id); - }) + syncRule.targetInstances.map(id => instanceOptions.find(e => e.value === id)) ), [instanceOptions, syncRule.targetInstances] ); From dbcd838029e7b13678d4354abdf797c6e8aad63b Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 12:08:48 +0100 Subject: [PATCH 022/163] Add basic integration test --- .../integration/local-instance-mapped.spec.ts | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts diff --git a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts new file mode 100644 index 000000000..017304e63 --- /dev/null +++ b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts @@ -0,0 +1,181 @@ +import { Request, Server } from "miragejs"; +import { AnyRegistry } from "miragejs/-types"; +import Schema from "miragejs/orm/schema"; +import { AggregatedSyncUseCase } from "../../../../domain/aggregated/usecases/AggregatedSyncUseCase"; +import { + Repositories, + RepositoryFactory, +} from "../../../../domain/common/factories/RepositoryFactory"; +import { Instance } from "../../../../domain/instance/entities/Instance"; +import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { startDhis } from "../../../../utils/dhisServer"; +import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; +import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; +import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; +import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; +import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; + +const repositoryFactory = buildRepositoryFactory(); + +describe("Sync metadata", () => { + let local: Server; + + beforeAll(() => { + jest.setTimeout(30000); + }); + + beforeEach(() => { + local = startDhis({ urlPrefix: "http://origin.test" }); + + local.get("/categoryOptionCombos", async () => ({ + categoryOptionCombos: [ + { + name: "default", + id: "default8", + categoryCombo: { id: "default7" }, + categoryOptions: [{ id: "default5" }], + }, + ], + })); + + local.get("/metadata", async (_schema, request) => { + if (request.queryParams.filter === "id:in:[dataSet1]") + return { + dataSets: [ + { + name: "Test data set", + id: "dataSet1", + dataSetElements: [ + { + dataElement: { + name: "Test data element 1", + id: "id1", + }, + dataSet: { + id: "dataSet1", + }, + }, + ], + }, + ], + }; + + if (request.queryParams.filter === "code:eq:default") + return { + categoryOptions: [{ id: "default1" }], + categories: [{ id: "default2" }], + categoryCombos: [{ id: "default3" }], + categoryOptionCombos: [{ id: "default4" }], + }; + + console.log("Unknown metadata request", request.queryParams); + }); + + local.get("/dataValueSets", async () => ({ + dataValues: [ + { + dataElement: "id1", + period: "20191231", + orgUnit: "Global", + categoryOptionCombo: "default4", + attributeOptionCombo: "default4", + value: "test-value-1", + storedBy: "test-user", + created: "2020-05-28T08:32:53.000+0000", + lastUpdated: "2020-05-28T08:32:53.000+0000", + followup: false, + }, + ], + })); + + local.get("/dataStore/metadata-synchronization/instances", async () => [ + { + id: "LOCAL", + name: "This instance", + url: "http://origin.test", + description: "", + }, + ]); + + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({ + metadataMapping: { + aggregatedDataElements: { + id1: { + mappedId: "id2", + mappedName: "foo", + code: "foo", + conflicts: false, + global: false, + mapping: {}, + }, + }, + }, + })); + + const addAggregatedToDb = async (schema: Schema, request: Request) => { + schema.db.dataValueSets.insert(JSON.parse(request.requestBody)); + + return { + responseType: "ImportSummary", + status: "WARNING", + description: "Import process completed successfully", + importCount: { imported: 0, updated: 0, ignored: 477, deleted: 0 }, + conflicts: [ + { + object: "id1", + value: "Data element not found or not accessible", + }, + ], + dataSetComplete: "false", + }; + }; + + local.db.createCollection("dataValueSets", []); + local.post("/dataValueSets", addAggregatedToDb); + }); + + afterEach(() => { + local.shutdown(); + }); + + it("Local server to local - same version", async () => { + const localInstance = Instance.build({ + url: "http://origin.test", + name: "Testing", + version: "2.30", + }); + + const builder: SynchronizationBuilder = { + originInstance: "LOCAL", + targetInstances: ["LOCAL"], + metadataIds: ["dataSet1"], + excludedIds: [], + }; + + const sync = new AggregatedSyncUseCase(builder, repositoryFactory, localInstance, ""); + + const payload = await sync.buildPayload(); + expect(payload.dataValues?.find(({ value }) => value === "test-value-1")).toBeDefined(); + + for await (const _sync of sync.execute()) { + // no-op + } + + const response = local.db.dataValueSets.find(1); + expect(response.dataValues[0].value).toEqual("test-value-1"); + expect(response.dataValues[0].dataElement).toEqual("id2"); + }); + +}); + +function buildRepositoryFactory() { + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); + repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); + repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); + repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); + repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); + return repositoryFactory; +} + +export {}; From 4f016b4a172e0ca7971ca48eb3e3ddb819ad1997 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 12:09:11 +0100 Subject: [PATCH 023/163] Add logged user pseudo-name --- .../components/sync-wizard/common/InstanceSelectionStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx index 1dc1db197..89acefb02 100644 --- a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx +++ b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx @@ -11,7 +11,7 @@ import { SyncWizardStepProps } from "../Steps"; export const buildInstanceOptions = (instances: Instance[]) => { return instances.map(instance => ({ value: instance.id, - text: `${instance.name} (${instance.url} with user ${instance.username})`, + text: `${instance.name} (${instance.url} with user ${instance.username ?? "logged"})`, })); }; From f1c00fd0ab9a2fab86e5b9666e162a13d30b93b0 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 12:10:08 +0100 Subject: [PATCH 024/163] Fix selection step UI --- .../components/sync-wizard/common/InstanceSelectionStep.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx index 89acefb02..15d3703b0 100644 --- a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx +++ b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx @@ -1,6 +1,5 @@ import { makeStyles, Typography } from "@material-ui/core"; import { MultiSelector } from "d2-ui-components"; -import _ from "lodash"; import React, { useEffect, useState } from "react"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../locales"; @@ -21,9 +20,7 @@ const InstanceSelectionStep: React.FC = ({ syncRule, onChan const [selectedOptions, setSelectedOptions] = useState(syncRule.targetInstances); const [targetInstances, setTargetInstances] = useState([]); - const instanceOptions = buildInstanceOptions(targetInstances); - const isDestinationRemote = !_.isEqual(syncRule.targetInstances, ["LOCAL"]); const includeCurrentUrlAndTypeIsEvents = (selectedinstanceIds: string[]) => { return ( @@ -55,7 +52,7 @@ const InstanceSelectionStep: React.FC = ({ syncRule, onChan return ( - {isDestinationRemote ? ( + {syncRule.originInstance === "LOCAL" ? ( Date: Wed, 25 Nov 2020 12:10:20 +0100 Subject: [PATCH 025/163] Do not allow selecting this instance --- src/presentation/webapp/pages/instance-list/InstanceListPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx index ed3b216f2..26842f5cd 100644 --- a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx @@ -228,6 +228,7 @@ const InstanceListPage = () => { const rowConfig = React.useCallback( (instance: Instance): RowConfig => ({ cellStyle: instance.type === "local" ? { fontWeight: "bold" } : undefined, + selectable: instance.type !== "local", }), [] ); From 5db711b6bb697876f514d3d1f1887411dd71f7a5 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 12:18:10 +0100 Subject: [PATCH 026/163] Disable local instance edit action --- .../__tests__/integration/local-instance-mapped.spec.ts | 1 - .../pages/instance-creation/InstanceCreationPage.tsx | 8 +++++++- .../webapp/pages/instance-list/InstanceListPage.tsx | 8 +++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts index 017304e63..6e5ae3306 100644 --- a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts +++ b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts @@ -165,7 +165,6 @@ describe("Sync metadata", () => { expect(response.dataValues[0].value).toEqual("test-value-1"); expect(response.dataValues[0].dataElement).toEqual("id2"); }); - }); function buildRepositoryFactory() { diff --git a/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx b/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx index 19f4d066a..93876bf71 100644 --- a/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx +++ b/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx @@ -72,7 +72,13 @@ const InstanceCreationPage = () => { - + {instance.type === "dhis" && ( + + )} ); }; diff --git a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx index 26842f5cd..82e721465 100644 --- a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx @@ -50,9 +50,11 @@ const InstanceListPage = () => { history.push("/instances/new"); }; - const editInstance = (ids: string[]) => { - if (ids.length !== 1) return; - if (appConfigurator) history.push(`/instances/edit/${ids[0]}`); + const editInstance = async (ids: string[]) => { + const instance = rows.find(row => row.id === ids[0]); + if (instance?.type === "dhis" && appConfigurator) { + history.push(`/instances/edit/${instance.id}`); + } }; const replicateInstance = async (ids: string[]) => { From 0f590f0858a9ca418b1bc1cd77665665ea1acca1 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 12:21:19 +0100 Subject: [PATCH 027/163] Remove url from data store --- src/domain/instance/entities/Instance.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/domain/instance/entities/Instance.ts b/src/domain/instance/entities/Instance.ts index 180f38282..13b1aebea 100644 --- a/src/domain/instance/entities/Instance.ts +++ b/src/domain/instance/entities/Instance.ts @@ -103,9 +103,18 @@ export class Instance { }); } - public static build(data: PartialBy): Instance { - const { type = "dhis", id = generateUid() } = data; - return new Instance({ type, id: type === "local" ? "LOCAL" : id, ...data }); + public static build({ + type = "dhis", + id = generateUid(), + url, + ...rest + }: PartialBy): Instance { + return new Instance({ + type, + id: type === "local" ? "LOCAL" : id, + url: type === "local" ? "" : url, + ...rest, + }); } private moduleValidations = (): ModelValidation[] => [ From 12d1447741f93aa097cceff9807cf721763f78c4 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 18:14:48 +0100 Subject: [PATCH 028/163] Update selector on destination dropdown --- .../webapp/pages/manual-sync/InstancesSelectors.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx b/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx index 7ab04a911..30eb48346 100644 --- a/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx +++ b/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx @@ -27,9 +27,7 @@ const InstancesSelectors: React.FC = ({ const classes = useStyles(); const sourceInstanceIsRemote = !!sourceInstance; - const showRemoteInstances = sourceInstanceIsRemote - ? showOnlyLocalInstances - : showOnlyRemoteInstances; + const showRemoteInstances = sourceInstanceIsRemote ? showOnlyLocalInstances : showAllInstances; const sourceSelectedInstance = sourceInstance?.id ?? "LOCAL"; const changeDestination = useCallback( @@ -66,7 +64,6 @@ const InstancesSelectors: React.FC = ({ }; const showAllInstances = { local: true, remote: true }; -const showOnlyRemoteInstances = { local: false, remote: true }; const showOnlyLocalInstances = { local: true, remote: false }; const useStyles = makeStyles({ From 7b1466da90a04f2ecf4fc27a26571fc8d666bbc9 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 18:39:47 +0100 Subject: [PATCH 029/163] Persist version on data store --- src/domain/instance/entities/Instance.ts | 25 ++++++---------- .../instance/usecases/SaveInstanceUseCase.ts | 29 +++++++++++++------ .../pages/instance-list/InstanceListPage.tsx | 1 + 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/domain/instance/entities/Instance.ts b/src/domain/instance/entities/Instance.ts index 13b1aebea..077516e2b 100644 --- a/src/domain/instance/entities/Instance.ts +++ b/src/domain/instance/entities/Instance.ts @@ -65,14 +65,16 @@ export class Instance { return this.data.metadataMapping ?? {}; } - public get version(): string { - return this.data.version ?? "2.30"; + public get version(): string | undefined { + return this.data.version; } public get apiVersion(): number { const apiVersion = _.get(this.version?.split("."), 1); - if (!apiVersion) throw new Error("Invalid api version"); - return Number(apiVersion); + // TODO: Review implications of having a default value here + // Not having this set means no connection possible on save + // For example, we should error during sync instead + return apiVersion ? Number(apiVersion) : 30; } public toObject(): InstanceData { @@ -103,18 +105,9 @@ export class Instance { }); } - public static build({ - type = "dhis", - id = generateUid(), - url, - ...rest - }: PartialBy): Instance { - return new Instance({ - type, - id: type === "local" ? "LOCAL" : id, - url: type === "local" ? "" : url, - ...rest, - }); + public static build(data: PartialBy): Instance { + const { type = "dhis", id = generateUid() } = data; + return new Instance({ type, id: type === "local" ? "LOCAL" : id, ...data }); } private moduleValidations = (): ModelValidation[] => [ diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index 6fdad588b..5c2a84743 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -34,16 +34,27 @@ export class SaveInstanceUseCase implements UseCase { // Validate model and save it if there're no errors const modelValidations = instance.validate(); + if (modelValidations.length > 0) return modelValidations; - if (modelValidations.length === 0) { - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection( - Namespace.INSTANCES, - instance.encryptPassword(this.encryptionKey).toObject() - ); - } + const instanceData = { + ...instance.encryptPassword(this.encryptionKey).toObject(), + url: instance.type === "local" ? "" : instance.url, + version: await this.getVersion(instance), + }; + + await this.repositoryFactory + .storageRepository(this.localInstance) + .saveObjectInCollection(Namespace.INSTANCES, instanceData); - return modelValidations; + return []; + } + + private async getVersion(instance: Instance): Promise { + try { + const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); + return version; + } catch (error) { + return instance.version; + } } } diff --git a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx index 82e721465..5e3a3e879 100644 --- a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx @@ -169,6 +169,7 @@ const InstanceListPage = () => { getValue: row => (row.type === "local" ? "Logged user" : row.username), }, { name: "description" as const, text: i18n.t("Description") }, + { name: "version" as const, text: i18n.t("Version") }, ]; const actions: TableAction[] = [ From d8cc961a3f04d0623ec84a3dd17d76acd0abb022 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 18:43:23 +0100 Subject: [PATCH 030/163] Join two maps --- src/domain/instance/usecases/ListInstancesUseCase.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index fef3a4e99..cbc5da6de 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -32,12 +32,11 @@ export class ListInstancesUseCase implements UseCase { ) : objects; - const instances = filteredData.map(item => - item.type === "local" ? { ...item, url: this.localInstance.url } : item + return filteredData.map(data => + Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + }).decryptPassword(this.encryptionKey) ); - - return instances.map(data => { - return Instance.build(data).decryptPassword(this.encryptionKey); - }); } } From a55e68b44caacf51b1373469f7b245fd2626e6b0 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 18:48:38 +0100 Subject: [PATCH 031/163] Update translated sentence --- i18n/en.pot | 10 ++++++++-- i18n/es.po | 8 +++++++- i18n/fr.po | 8 +++++++- i18n/pt.po | 8 +++++++- .../sync-wizard/common/InstanceSelectionStep.tsx | 4 +++- 5 files changed, 32 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 2c4af5c41..0f0739fa5 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:21:05.637Z\n" -"PO-Revision-Date: 2020-11-25T07:21:05.637Z\n" +"POT-Creation-Date: 2020-11-25T17:48:23.463Z\n" +"PO-Revision-Date: 2020-11-25T17:48:23.463Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -993,6 +993,12 @@ msgstr "" msgid "Source instance" msgstr "" +msgid "{{name}} ({{url}}) with user {{username}}" +msgstr "" + +msgid "{{name}} ({{url}}) with logged user" +msgstr "" + msgid "Destination" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 2f4932963..81306529c 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-25T07:21:05.637Z\n" +"POT-Creation-Date: 2020-11-25T17:48:23.463Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -998,6 +998,12 @@ msgstr "" msgid "Source instance" msgstr "" +msgid "{{name}} ({{url}}) with user {{username}}" +msgstr "" + +msgid "{{name}} ({{url}}) with logged user" +msgstr "" + msgid "Destination" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 1cb9311a1..d909de0ac 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-25T07:21:05.637Z\n" +"POT-Creation-Date: 2020-11-25T17:48:23.463Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -995,6 +995,12 @@ msgstr "" msgid "Source instance" msgstr "" +msgid "{{name}} ({{url}}) with user {{username}}" +msgstr "" + +msgid "{{name}} ({{url}}) with logged user" +msgstr "" + msgid "Destination" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 1cb9311a1..d909de0ac 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-25T07:21:05.637Z\n" +"POT-Creation-Date: 2020-11-25T17:48:23.463Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -995,6 +995,12 @@ msgstr "" msgid "Source instance" msgstr "" +msgid "{{name}} ({{url}}) with user {{username}}" +msgstr "" + +msgid "{{name}} ({{url}}) with logged user" +msgstr "" + msgid "Destination" msgstr "" diff --git a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx index 15d3703b0..f31038cbb 100644 --- a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx +++ b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx @@ -10,7 +10,9 @@ import { SyncWizardStepProps } from "../Steps"; export const buildInstanceOptions = (instances: Instance[]) => { return instances.map(instance => ({ value: instance.id, - text: `${instance.name} (${instance.url} with user ${instance.username ?? "logged"})`, + text: instance.username + ? i18n.t("{{name}} ({{url}}) with user {{username}}") + : i18n.t("{{name}} ({{url}}) with logged user"), })); }; From 6c15640ab37157695423b5cfc32cd94a33c52fd3 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 25 Nov 2020 22:13:30 +0100 Subject: [PATCH 032/163] Fix tests --- .../integration/local-instance-mapped.spec.ts | 3 +- .../integration/sync-aggregated.spec.ts | 9 + .../__tests__/integration/sync-events.spec.ts | 9 + .../integration/sync-metadata.spec.ts | 9 + .../__tests__/integration/helpers.ts | 16 +- .../transformations-api-30.spec.ts | 442 +++++------------- .../transformations-api-31.spec.ts | 54 +-- .../transformations-api-32.spec.ts | 9 + .../transformations-api-34.spec.ts | 365 ++++++++------- .../instance/usecases/ListInstancesUseCase.ts | 1 + 10 files changed, 372 insertions(+), 545 deletions(-) diff --git a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts index 6e5ae3306..f2130f050 100644 --- a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts +++ b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts @@ -90,10 +90,11 @@ describe("Sync metadata", () => { local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", id: "LOCAL", name: "This instance", - url: "http://origin.test", description: "", + url: "http://origin.test", }, ]); diff --git a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts index 4ebb881ac..376e4c849 100644 --- a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts @@ -131,6 +131,14 @@ describe("Sync metadata", () => { local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", + id: "LOCAL", + name: "This instance", + description: "", + url: "http://origin.test", + }, + { + type: "dhis", id: "DESTINATION", name: "Destination test", url: "http://destination.test", @@ -140,6 +148,7 @@ describe("Sync metadata", () => { }, ]); + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({})); local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({ metadataMapping: { aggregatedDataElements: { diff --git a/src/data/metadata/__tests__/integration/sync-events.spec.ts b/src/data/metadata/__tests__/integration/sync-events.spec.ts index 9dccef083..37fe21e5f 100644 --- a/src/data/metadata/__tests__/integration/sync-events.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-events.spec.ts @@ -170,6 +170,14 @@ describe("Sync metadata", () => { local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", + id: "LOCAL", + name: "This instance", + description: "", + url: "http://origin.test", + }, + { + type: "dhis", id: "DESTINATION", name: "Destination test", url: "http://destination.test", @@ -179,6 +187,7 @@ describe("Sync metadata", () => { }, ]); + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({})); local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({})); const addEventsToDb = async (schema: Schema, request: Request) => { diff --git a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts index ff69fe1e2..60b3989a1 100644 --- a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts @@ -41,6 +41,14 @@ describe("Sync metadata", () => { local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", + id: "LOCAL", + name: "This instance", + description: "", + url: "http://origin.test", + }, + { + type: "dhis", id: "DESTINATION", name: "Destination test", url: "http://destination.test", @@ -50,6 +58,7 @@ describe("Sync metadata", () => { }, ]); + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({})); local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({})); const addMetadataToDb = async (schema: Schema, request: Request) => { diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 93f568ceb..8f2681bff 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -54,6 +54,14 @@ export async function sync({ local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", + id: "LOCAL", + name: "This instance", + description: "", + url: "http://origin.test", + }, + { + type: "dhis", id: "DESTINATION", name: "Destination test", url: "http://destination.test", @@ -63,6 +71,7 @@ export async function sync({ }, ]); + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({})); local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({})); const addMetadataToDb = async (schema: Schema, request: Request) => { @@ -98,7 +107,7 @@ export async function executeMetadataSync( const repositoryFactory = buildRepositoryFactory(); const localInstance = Instance.build({ - url: local.urlPrefix, + url: "http://origin.test", name: "Testing", version: fromVersion, }); @@ -112,9 +121,8 @@ export async function executeMetadataSync( const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - for await (const { done } of useCase.execute()) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - done; + for await (const _sync of useCase.execute()) { + // no-op } expect(local.db.metadata.where({})).toHaveLength(0); diff --git a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts index 537f57901..06f72d383 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-30.spec.ts @@ -1,40 +1,14 @@ -import { Request, Server } from "miragejs"; -import { AnyRegistry } from "miragejs/-types"; -import Schema from "miragejs/orm/schema"; -import { - Repositories, - RepositoryFactory, -} from "../../../../domain/common/factories/RepositoryFactory"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; -import { startDhis } from "../../../../utils/dhisServer"; -import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; -import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; +import { sync, SyncResult } from "./helpers"; -const repositoryFactory = buildRepositoryFactory(); +let payload: SyncResult; describe("Sync metadata", () => { - let local: Server; - let remote: Server; - beforeAll(() => { jest.setTimeout(30000); }); - beforeEach(() => { - local = startDhis({ urlPrefix: "http://origin.test" }); - remote = startDhis( - { - urlPrefix: "http://destination.test", - pretender: local.pretender, - }, - { version: "2.30" } - ); - - local.get("/metadata", async () => ({ + describe("Transformations for 2.31 -> 2.30", () => { + const metadata = { programs: [ { id: "id1", @@ -64,346 +38,142 @@ describe("Sync metadata", () => { validationStrategy: "ON_UPDATE_AND_INSERT", }, ], - organisationUnits: [ - { - id: "ou_id1", - name: "Test org unit", - dimensionItemType: "ORGANISATION_UNIT", - geometry: { - coordinates: "[22.0123,-1.9012]", - type: "Point", - }, - }, - { - id: "ou_id2", - name: "Test org unit", - dimensionItemType: "ORGANISATION_UNIT", - geometry: { - coordinates: - "[[[[-12.0931,8.507],[-12.09,8.5025],[-12.0875,8.4996],[-12.0814,8.4934],[-12.0779,8.4897],[-12.0753,8.4859],[-12.0735,8.4844],[-12.0679,8.4816],[-12.0629,8.48],[-12.0612,8.4781],[-12.0606,8.4756],[-12.0605,8.4729],[-12.0605,8.4623],[-12.0607,8.4585],[-12.0612,8.4558],[-12.0634,8.4505],[-12.0638,8.448],[-12.0636,8.4445],[-12.0624,8.4396],[-12.0638,8.4336],[-12.0637,8.4297],[-12.0626,8.4274],[-12.0603,8.4258],[-12.0571,8.4255],[-12.0524,8.4276],[-12.0481,8.4281],[-12.0451,8.4274],[-12.0399,8.4236],[-12.0376,8.4228],[-12.0356,8.4232],[-12.0306,8.4255],[-12.0272,8.4275],[-12.0239,8.428],[-12.021,8.4265],[-12.0195,8.4241],[-12.0196,8.4212],[-12.0216,8.4169],[-12.0233,8.4111],[-12.0253,8.4068],[-12.0255,8.4032],[-12.0242,8.4007],[-12.0225,8.3994],[-12.0194,8.3986],[-12.0166,8.3985],[-12.0088,8.3988],[-12.005,8.3987],[-12.0017,8.398],[-11.9999,8.3964],[-11.9979,8.3938],[-11.9916,8.3869],[-11.9904,8.3834],[-11.9914,8.3807],[-11.9937,8.3772],[-11.9982,8.3731],[-12.0036,8.3677],[-12.0093,8.3641],[-12.021,8.358],[-12.0268,8.3539],[-12.0322,8.3507],[-12.0363,8.347],[-12.0399,8.3428],[-12.0442,8.333],[-12.049,8.3247],[-12.0529,8.319],[-12.0544,8.3152],[-12.0545,8.311],[-12.0527,8.3072],[-12.0499,8.3041],[-12.0465,8.3017],[-12.0403,8.2993],[-12.0371,8.2974],[-12.0348,8.2947],[-12.032,8.2896],[-12.0307,8.2841],[-12.0296,8.272],[-12.0268,8.2613],[-12.0265,8.2561],[-12.0272,8.2529],[-12.0299,8.2496],[-12.0337,8.2478],[-12.0366,8.2474],[-12.0411,8.2474],[-12.0456,8.248],[-12.0485,8.2489],[-12.0522,8.2513],[-12.0609,8.2593],[-12.0643,8.2617],[-12.0734,8.2657],[-12.0833,8.267],[-12.0874,8.2685],[-12.0911,8.2711],[-12.1011,8.2806],[-12.1059,8.2842],[-12.1136,8.2877],[-12.1225,8.2896],[-12.1262,8.2914],[-12.1294,8.2941],[-12.1322,8.2973],[-12.1356,8.3026],[-12.1407,8.3093],[-12.1441,8.3145],[-12.1458,8.3168],[-12.149,8.3197],[-12.1514,8.3213],[-12.1578,8.3238],[-12.1651,8.328],[-12.1692,8.3293],[-12.1765,8.3299],[-12.1945,8.3299],[-12.2003,8.3322],[-12.2046,8.3331],[-12.2151,8.334],[-12.2192,8.335],[-12.2257,8.3379],[-12.2321,8.3413],[-12.2414,8.3475],[-12.2467,8.3502],[-12.2504,8.3516],[-12.2547,8.3522],[-12.2606,8.3525],[-12.265,8.3523],[-12.2679,8.3518],[-12.2718,8.3502],[-12.2762,8.3467],[-12.2788,8.3436],[-12.2821,8.3384],[-12.284,8.3362],[-12.2863,8.3343],[-12.2903,8.3321],[-12.2937,8.3306],[-12.2966,8.3299],[-12.2997,8.3296],[-12.3073,8.3298],[-12.3115,8.3307],[-12.3194,8.3341],[-12.3387,8.344],[-12.3464,8.3474],[-12.3506,8.3483],[-12.3564,8.3483],[-12.3605,8.3475],[-12.3671,8.3445],[-12.3718,8.3412],[-12.3773,8.3359],[-12.387,8.3259],[-12.3899,8.3224],[-12.3943,8.3166],[-12.4044,8.3079],[-12.4087,8.3051],[-12.4121,8.3044],[-12.4158,8.3054],[-12.4183,8.3077],[-12.4213,8.3141],[-12.424,8.3172],[-12.427,8.3182],[-12.4304,8.3164],[-12.4319,8.313],[-12.4322,8.3012],[-12.4325,8.2982],[-12.4345,8.2908],[-12.4364,8.2813],[-12.439,8.2729],[-12.4475,8.2802],[-12.4526,8.2869],[-12.4581,8.2906],[-12.4626,8.2909],[-12.4702,8.2894],[-12.4842,8.2882],[-12.4949,8.2854],[-12.4995,8.2849],[-12.5043,8.2848],[-12.5105,8.2853],[-12.5147,8.2865],[-12.5211,8.2896],[-12.5271,8.2935],[-12.5309,8.2953],[-12.549,8.2983],[-12.5523,8.2977],[-12.5597,8.2941],[-12.564,8.293],[-12.5702,8.2924],[-12.57,8.2998],[-12.5696,8.3041],[-12.5671,8.314],[-12.567,8.3193],[-12.5685,8.3254],[-12.5703,8.3288],[-12.5741,8.3334],[-12.5786,8.3375],[-12.5833,8.3406],[-12.5899,8.3437],[-12.5964,8.3459],[-12.605,8.3508],[-12.6163,8.3545],[-12.6261,8.356],[-12.63,8.3572],[-12.6329,8.3593],[-12.6364,8.3635],[-12.639,8.3659],[-12.6464,8.371],[-12.6502,8.3731],[-12.6543,8.3741],[-12.6585,8.3744],[-12.6644,8.3742],[-12.6719,8.3735],[-12.668,8.3838],[-12.666,8.3876],[-12.6611,8.3934],[-12.6545,8.3996],[-12.6521,8.4014],[-12.6495,8.4028],[-12.6466,8.4037],[-12.6393,8.4053],[-12.6306,8.4096],[-12.6265,8.4132],[-12.6238,8.4164],[-12.6202,8.4214],[-12.6181,8.4234],[-12.6144,8.4254],[-12.6076,8.4272],[-12.6036,8.43],[-12.6022,8.433],[-12.6018,8.4378],[-12.6027,8.4433],[-12.6057,8.4505],[-12.6055,8.4546],[-12.6036,8.4574],[-12.5999,8.4595],[-12.589,8.4612],[-12.5809,8.464],[-12.5731,8.4643],[-12.571,8.4629],[-12.5657,8.4552],[-12.5607,8.4508],[-12.5562,8.4454],[-12.5536,8.443],[-12.5498,8.4404],[-12.5471,8.4379],[-12.545,8.4351],[-12.5417,8.4296],[-12.5397,8.4278],[-12.5355,8.4264],[-12.5315,8.4264],[-12.5284,8.4276],[-12.5264,8.4294],[-12.5241,8.4332],[-12.5221,8.4386],[-12.5184,8.4517],[-12.5149,8.4551],[-12.511,8.4563],[-12.5068,8.4564],[-12.5025,8.4557],[-12.4966,8.4537],[-12.4924,8.453],[-12.4866,8.4529],[-12.4824,8.4536],[-12.4666,8.4587],[-12.4638,8.4593],[-12.4568,8.4602],[-12.4515,8.462],[-12.4471,8.4653],[-12.4444,8.4685],[-12.4411,8.4738],[-12.4375,8.4782],[-12.4303,8.4854],[-12.4223,8.4916],[-12.4201,8.4944],[-12.4188,8.4973],[-12.4049,8.4969],[-12.4005,8.4961],[-12.3959,8.4947],[-12.3904,8.4945],[-12.3756,8.498],[-12.372,8.4999],[-12.3689,8.5027],[-12.3662,8.5059],[-12.3621,8.5121],[-12.3593,8.5152],[-12.3559,8.5176],[-12.352,8.519],[-12.341,8.5211],[-12.3364,8.5241],[-12.3328,8.5283],[-12.3275,8.5383],[-12.3247,8.5406],[-12.3206,8.5418],[-12.3103,8.5426],[-12.3062,8.5435],[-12.2962,8.5469],[-12.2877,8.5519],[-12.2812,8.5542],[-12.2696,8.5588],[-12.265,8.5572],[-12.2621,8.5537],[-12.2598,8.552],[-12.2559,8.5501],[-12.2485,8.5446],[-12.2454,8.5432],[-12.241,8.5408],[-12.2378,8.5394],[-12.2335,8.5371],[-12.2303,8.5357],[-12.2259,8.5335],[-12.2199,8.5321],[-12.2146,8.5298],[-12.2086,8.5284],[-12.2033,8.5261],[-12.1965,8.5244],[-12.1929,8.5226],[-12.1876,8.5216],[-12.1799,8.5215],[-12.1749,8.5209],[-12.1685,8.5185],[-12.1646,8.5179],[-12.1574,8.5178],[-12.1544,8.5175],[-12.1515,8.5169],[-12.147,8.515],[-12.1441,8.5145],[-12.1374,8.514],[-12.1336,8.5135],[-12.1282,8.5113],[-12.1223,8.5098],[-12.116,8.5073],[-12.1122,8.5067],[-12.1071,8.5066],[-12.098,8.5067],[-12.0931,8.507]]]]", - type: "Polygon", - }, - }, - ], - })); - - remote.get("/metadata", async () => ({})); - - local.get("/dataStore/metadata-synchronization/instances", async () => [ - { - id: "DESTINATION", - name: "Destination test", - url: "http://destination.test", - username: "test", - password: "", - description: "", - }, - ]); - - local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({})); - - const addMetadataToDb = async (schema: Schema, request: Request) => { - schema.db.metadata.insert(JSON.parse(request.requestBody)); - - return { - status: "OK", - stats: { created: 1, updated: 0, deleted: 0, ignored: 0, total: 1 }, - typeReports: [], - }; }; - local.db.createCollection("metadata", []); - local.post("/metadata", addMetadataToDb); - - remote.db.createCollection("metadata", []); - remote.post("/metadata", addMetadataToDb); - }); - - afterEach(() => { - local.shutdown(); - remote.shutdown(); - }); - - it("Local server to remote - program featureType POINT to captureCoordinates - API 31 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.31", + beforeAll(async () => { + payload = await sync({ + from: "2.31", + to: "2.30", + metadata, + models: ["programs", "programStages"], + }); }); - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["id1"], - excludedIds: [], - }; - - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); + it("Local server to remote - program featureType POINT to captureCoordinates - API 31 to API 30", async () => { + const program = payload.programs["id1"]; + expect(program).toBeDefined(); + expect(program.name).toEqual("Test tracker program"); - const payload = await useCase.buildPayload(); - expect(payload.programs?.find(({ id }) => id === "id1")).toBeDefined(); + // Assert new properties have the correct values + expect(program.featureType).toEqual("POINT"); - for await (const _sync of useCase.execute()) { - // no-op - } - - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.programs[0].id).toEqual("id1"); - expect(response.programs[0].name).toEqual("Test tracker program"); - - // Assert new properties have the correct values - expect(response.programs[0].featureType).toEqual("POINT"); - - // Assert old properties are not anymore - expect(response.programs[0].captureCoordinates).toEqual(true); - - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); - }); - - it("Local server to remote - program featureType POLYGON to captureCoordinates - API 31 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.31", + // Assert old properties are not anymore + expect(program.captureCoordinates).toEqual(true); }); - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["id1", "id2", "id3"], - excludedIds: [], - }; + it("Local server to remote - program featureType POLYGON to captureCoordinates - API 31 to API 30", async () => { + const program = payload.programs["id2"]; + expect(program).toBeDefined(); + expect(program.name).toEqual("Test tracker program"); - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); + // Assert new properties have the correct values + expect(program.captureCoordinates).toEqual(true); - const payload = await useCase.buildPayload(); - expect(payload.programs?.find(({ id }) => id === "id2")).toBeDefined(); - - for await (const _sync of useCase.execute()) { - // no-op - } - - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.programs[1].id).toEqual("id2"); - expect(response.programs[1].name).toEqual("Test tracker program"); - - // Assert new properties have the correct values - expect(response.programs[1].captureCoordinates).toEqual(true); - - // Assert old properties still here - expect(response.programs[1].featureType).toEqual("POLYGON"); - - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); - }); - - it("Local server to remote - program featureType POLYGON to captureCoordinates - API 31 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.31", + // Assert old properties still here + expect(program.featureType).toEqual("POLYGON"); }); - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["id3"], - excludedIds: [], - }; - - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - - const payload = await useCase.buildPayload(); - expect(payload.programs?.find(({ id }) => id === "id3")).toBeDefined(); - - for await (const _sync of useCase.execute()) { - // no-op - } - - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.programs[2].id).toEqual("id3"); - expect(response.programs[2].name).toEqual("Test tracker program"); - - // Assert new properties have the correct values - expect(response.programs[2].captureCoordinates).toEqual(false); + it("Local server to remote - program featureType POLYGON to captureCoordinates - API 31 to API 30", async () => { + const program = payload.programs["id3"]; + expect(program).toBeDefined(); + expect(program.name).toEqual("Test tracker program"); - // Assert old properties still here - expect(response.programs[2].featureType).toEqual("NONE"); + // Assert new properties have the correct values + expect(program.captureCoordinates).toEqual(false); - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); - }); - - it("Local server to remote - programStage validationStrategy -> ON_COMPLETE to validCompleteOnly true - API 31 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.31", + // Assert old properties still here + expect(program.featureType).toEqual("NONE"); }); - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["ps_id1"], - excludedIds: [], - }; - - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - - const payload = await useCase.buildPayload(); - expect(payload.programStages?.find(({ id }) => id === "ps_id1")).toBeDefined(); - - for await (const _sync of useCase.execute()) { - // no-op - } - - // Assert object has been created on remote - const response = remote.db.metadata.find(1); + it("Local server to remote - programStage validationStrategy -> ON_COMPLETE to validCompleteOnly true - API 31 to API 30", async () => { + const programStage = payload.programStages["ps_id1"]; + expect(programStage).toBeDefined(); + expect(programStage.name).toEqual("Test programStage"); - expect(response.programStages[0].id).toEqual("ps_id1"); - expect(response.programStages[0].name).toEqual("Test programStage"); - // Assert old properties still here - expect(response.programStages[0].validationStrategy).toEqual("ON_COMPLETE"); + // Assert old properties still here + expect(programStage.validationStrategy).toEqual("ON_COMPLETE"); - // Assert new properties have the correct values - expect(response.programStages[0].validCompleteOnly).toEqual(true); - - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); - }); - - it("Local server to remote - programStage validationStrategy -> ON_UPDATE_AND_INSERT to validCompleteOnly false - API 31 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.31", + // Assert new properties have the correct values + expect(programStage.validCompleteOnly).toEqual(true); }); - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["ps_id2"], - excludedIds: [], - }; - - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - - const payload = await useCase.buildPayload(); - expect(payload.programStages?.find(({ id }) => id === "ps_id2")).toBeDefined(); - - for await (const _sync of useCase.execute()) { - // no-op - } + it("Local server to remote - programStage validationStrategy -> ON_UPDATE_AND_INSERT to validCompleteOnly false - API 31 to API 30", async () => { + const programStage = payload.programStages["ps_id2"]; + expect(programStage).toBeDefined(); + expect(programStage.name).toEqual("Test programStage"); - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.programStages[1].id).toEqual("ps_id2"); - expect(response.programStages[1].name).toEqual("Test programStage"); + // Assert old properties still here + expect(programStage.validationStrategy).toEqual("ON_UPDATE_AND_INSERT"); - // Assert old properties still here - expect(response.programStages[1].validationStrategy).toEqual("ON_UPDATE_AND_INSERT"); - - // Assert new properties have the correct values - expect(response.programStages[1].validCompleteOnly).toEqual(false); - - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); - }); - - it("Local server to remote - organisationUnits featureType type POINT to geometry - API 32 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.32", + // Assert new properties have the correct values + expect(programStage.validCompleteOnly).toEqual(false); }); - - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["ou_id1"], - excludedIds: [], - }; - - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - - const payload = await useCase.buildPayload(); - expect(payload.organisationUnits?.find(({ id }) => id === "ou_id1")).toBeDefined(); - - for await (const _sync of useCase.execute()) { - // no-op - } - - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.organisationUnits[0].id).toEqual("ou_id1"); - expect(response.organisationUnits[0].name).toEqual("Test org unit"); - expect(response.organisationUnits[0].dimensionItemType).toEqual("ORGANISATION_UNIT"); - - // Assert new properties have the correct values - expect(response.organisationUnits[0].featureType).toEqual("POINT"); - expect(response.organisationUnits[0].coordinates).toEqual("[22.0123,-1.9012]"); - - // Assert old properties are not anymore - expect(response.organisationUnits[0].geometry).toBeUndefined(); - - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); }); - it("Local server to remote - organisationUnits featureType type POLYGON to geometry - API 32 to API 30", async () => { - const localInstance = Instance.build({ - url: "http://origin.test", - name: "Testing", - version: "2.32", - }); - - const builder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: ["DESTINATION"], - metadataIds: ["ou_id2"], - excludedIds: [], + describe("Transformations for 2.32 -> 2.30", () => { + const metadata = { + organisationUnits: [ + { + id: "ou_id1", + name: "Test org unit", + dimensionItemType: "ORGANISATION_UNIT", + geometry: { + coordinates: "[22.0123,-1.9012]", + type: "Point", + }, + }, + { + id: "ou_id2", + name: "Test org unit", + dimensionItemType: "ORGANISATION_UNIT", + geometry: { + coordinates: + "[[[[-12.0931,8.507],[-12.09,8.5025],[-12.0875,8.4996],[-12.0814,8.4934],[-12.0779,8.4897],[-12.0753,8.4859],[-12.0735,8.4844],[-12.0679,8.4816],[-12.0629,8.48],[-12.0612,8.4781],[-12.0606,8.4756],[-12.0605,8.4729],[-12.0605,8.4623],[-12.0607,8.4585],[-12.0612,8.4558],[-12.0634,8.4505],[-12.0638,8.448],[-12.0636,8.4445],[-12.0624,8.4396],[-12.0638,8.4336],[-12.0637,8.4297],[-12.0626,8.4274],[-12.0603,8.4258],[-12.0571,8.4255],[-12.0524,8.4276],[-12.0481,8.4281],[-12.0451,8.4274],[-12.0399,8.4236],[-12.0376,8.4228],[-12.0356,8.4232],[-12.0306,8.4255],[-12.0272,8.4275],[-12.0239,8.428],[-12.021,8.4265],[-12.0195,8.4241],[-12.0196,8.4212],[-12.0216,8.4169],[-12.0233,8.4111],[-12.0253,8.4068],[-12.0255,8.4032],[-12.0242,8.4007],[-12.0225,8.3994],[-12.0194,8.3986],[-12.0166,8.3985],[-12.0088,8.3988],[-12.005,8.3987],[-12.0017,8.398],[-11.9999,8.3964],[-11.9979,8.3938],[-11.9916,8.3869],[-11.9904,8.3834],[-11.9914,8.3807],[-11.9937,8.3772],[-11.9982,8.3731],[-12.0036,8.3677],[-12.0093,8.3641],[-12.021,8.358],[-12.0268,8.3539],[-12.0322,8.3507],[-12.0363,8.347],[-12.0399,8.3428],[-12.0442,8.333],[-12.049,8.3247],[-12.0529,8.319],[-12.0544,8.3152],[-12.0545,8.311],[-12.0527,8.3072],[-12.0499,8.3041],[-12.0465,8.3017],[-12.0403,8.2993],[-12.0371,8.2974],[-12.0348,8.2947],[-12.032,8.2896],[-12.0307,8.2841],[-12.0296,8.272],[-12.0268,8.2613],[-12.0265,8.2561],[-12.0272,8.2529],[-12.0299,8.2496],[-12.0337,8.2478],[-12.0366,8.2474],[-12.0411,8.2474],[-12.0456,8.248],[-12.0485,8.2489],[-12.0522,8.2513],[-12.0609,8.2593],[-12.0643,8.2617],[-12.0734,8.2657],[-12.0833,8.267],[-12.0874,8.2685],[-12.0911,8.2711],[-12.1011,8.2806],[-12.1059,8.2842],[-12.1136,8.2877],[-12.1225,8.2896],[-12.1262,8.2914],[-12.1294,8.2941],[-12.1322,8.2973],[-12.1356,8.3026],[-12.1407,8.3093],[-12.1441,8.3145],[-12.1458,8.3168],[-12.149,8.3197],[-12.1514,8.3213],[-12.1578,8.3238],[-12.1651,8.328],[-12.1692,8.3293],[-12.1765,8.3299],[-12.1945,8.3299],[-12.2003,8.3322],[-12.2046,8.3331],[-12.2151,8.334],[-12.2192,8.335],[-12.2257,8.3379],[-12.2321,8.3413],[-12.2414,8.3475],[-12.2467,8.3502],[-12.2504,8.3516],[-12.2547,8.3522],[-12.2606,8.3525],[-12.265,8.3523],[-12.2679,8.3518],[-12.2718,8.3502],[-12.2762,8.3467],[-12.2788,8.3436],[-12.2821,8.3384],[-12.284,8.3362],[-12.2863,8.3343],[-12.2903,8.3321],[-12.2937,8.3306],[-12.2966,8.3299],[-12.2997,8.3296],[-12.3073,8.3298],[-12.3115,8.3307],[-12.3194,8.3341],[-12.3387,8.344],[-12.3464,8.3474],[-12.3506,8.3483],[-12.3564,8.3483],[-12.3605,8.3475],[-12.3671,8.3445],[-12.3718,8.3412],[-12.3773,8.3359],[-12.387,8.3259],[-12.3899,8.3224],[-12.3943,8.3166],[-12.4044,8.3079],[-12.4087,8.3051],[-12.4121,8.3044],[-12.4158,8.3054],[-12.4183,8.3077],[-12.4213,8.3141],[-12.424,8.3172],[-12.427,8.3182],[-12.4304,8.3164],[-12.4319,8.313],[-12.4322,8.3012],[-12.4325,8.2982],[-12.4345,8.2908],[-12.4364,8.2813],[-12.439,8.2729],[-12.4475,8.2802],[-12.4526,8.2869],[-12.4581,8.2906],[-12.4626,8.2909],[-12.4702,8.2894],[-12.4842,8.2882],[-12.4949,8.2854],[-12.4995,8.2849],[-12.5043,8.2848],[-12.5105,8.2853],[-12.5147,8.2865],[-12.5211,8.2896],[-12.5271,8.2935],[-12.5309,8.2953],[-12.549,8.2983],[-12.5523,8.2977],[-12.5597,8.2941],[-12.564,8.293],[-12.5702,8.2924],[-12.57,8.2998],[-12.5696,8.3041],[-12.5671,8.314],[-12.567,8.3193],[-12.5685,8.3254],[-12.5703,8.3288],[-12.5741,8.3334],[-12.5786,8.3375],[-12.5833,8.3406],[-12.5899,8.3437],[-12.5964,8.3459],[-12.605,8.3508],[-12.6163,8.3545],[-12.6261,8.356],[-12.63,8.3572],[-12.6329,8.3593],[-12.6364,8.3635],[-12.639,8.3659],[-12.6464,8.371],[-12.6502,8.3731],[-12.6543,8.3741],[-12.6585,8.3744],[-12.6644,8.3742],[-12.6719,8.3735],[-12.668,8.3838],[-12.666,8.3876],[-12.6611,8.3934],[-12.6545,8.3996],[-12.6521,8.4014],[-12.6495,8.4028],[-12.6466,8.4037],[-12.6393,8.4053],[-12.6306,8.4096],[-12.6265,8.4132],[-12.6238,8.4164],[-12.6202,8.4214],[-12.6181,8.4234],[-12.6144,8.4254],[-12.6076,8.4272],[-12.6036,8.43],[-12.6022,8.433],[-12.6018,8.4378],[-12.6027,8.4433],[-12.6057,8.4505],[-12.6055,8.4546],[-12.6036,8.4574],[-12.5999,8.4595],[-12.589,8.4612],[-12.5809,8.464],[-12.5731,8.4643],[-12.571,8.4629],[-12.5657,8.4552],[-12.5607,8.4508],[-12.5562,8.4454],[-12.5536,8.443],[-12.5498,8.4404],[-12.5471,8.4379],[-12.545,8.4351],[-12.5417,8.4296],[-12.5397,8.4278],[-12.5355,8.4264],[-12.5315,8.4264],[-12.5284,8.4276],[-12.5264,8.4294],[-12.5241,8.4332],[-12.5221,8.4386],[-12.5184,8.4517],[-12.5149,8.4551],[-12.511,8.4563],[-12.5068,8.4564],[-12.5025,8.4557],[-12.4966,8.4537],[-12.4924,8.453],[-12.4866,8.4529],[-12.4824,8.4536],[-12.4666,8.4587],[-12.4638,8.4593],[-12.4568,8.4602],[-12.4515,8.462],[-12.4471,8.4653],[-12.4444,8.4685],[-12.4411,8.4738],[-12.4375,8.4782],[-12.4303,8.4854],[-12.4223,8.4916],[-12.4201,8.4944],[-12.4188,8.4973],[-12.4049,8.4969],[-12.4005,8.4961],[-12.3959,8.4947],[-12.3904,8.4945],[-12.3756,8.498],[-12.372,8.4999],[-12.3689,8.5027],[-12.3662,8.5059],[-12.3621,8.5121],[-12.3593,8.5152],[-12.3559,8.5176],[-12.352,8.519],[-12.341,8.5211],[-12.3364,8.5241],[-12.3328,8.5283],[-12.3275,8.5383],[-12.3247,8.5406],[-12.3206,8.5418],[-12.3103,8.5426],[-12.3062,8.5435],[-12.2962,8.5469],[-12.2877,8.5519],[-12.2812,8.5542],[-12.2696,8.5588],[-12.265,8.5572],[-12.2621,8.5537],[-12.2598,8.552],[-12.2559,8.5501],[-12.2485,8.5446],[-12.2454,8.5432],[-12.241,8.5408],[-12.2378,8.5394],[-12.2335,8.5371],[-12.2303,8.5357],[-12.2259,8.5335],[-12.2199,8.5321],[-12.2146,8.5298],[-12.2086,8.5284],[-12.2033,8.5261],[-12.1965,8.5244],[-12.1929,8.5226],[-12.1876,8.5216],[-12.1799,8.5215],[-12.1749,8.5209],[-12.1685,8.5185],[-12.1646,8.5179],[-12.1574,8.5178],[-12.1544,8.5175],[-12.1515,8.5169],[-12.147,8.515],[-12.1441,8.5145],[-12.1374,8.514],[-12.1336,8.5135],[-12.1282,8.5113],[-12.1223,8.5098],[-12.116,8.5073],[-12.1122,8.5067],[-12.1071,8.5066],[-12.098,8.5067],[-12.0931,8.507]]]]", + type: "Polygon", + }, + }, + ], }; - const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); + beforeAll(async () => { + payload = await sync({ + from: "2.32", + to: "2.30", + metadata, + models: ["organisationUnits"], + }); + }); - const payload = await useCase.buildPayload(); - expect(payload.organisationUnits?.find(({ id }) => id === "ou_id2")).toBeDefined(); + it("Local server to remote - organisationUnits featureType type POINT to geometry - API 32 to API 30", async () => { + const orgUnit = payload.organisationUnits["ou_id1"]; + expect(orgUnit).toBeDefined(); + expect(orgUnit.name).toEqual("Test org unit"); + expect(orgUnit.dimensionItemType).toEqual("ORGANISATION_UNIT"); - for await (const _sync of useCase.execute()) { - // no-op - } + // Assert new properties have the correct values + expect(orgUnit.featureType).toEqual("POINT"); + expect(orgUnit.coordinates).toEqual("[22.0123,-1.9012]"); - // Assert object has been created on remote - const response = remote.db.metadata.find(1); - expect(response.organisationUnits[1].id).toEqual("ou_id2"); - expect(response.organisationUnits[1].name).toEqual("Test org unit"); - expect(response.organisationUnits[1].dimensionItemType).toEqual("ORGANISATION_UNIT"); + // Assert old properties are not anymore + expect(orgUnit.geometry).toBeUndefined(); + }); - // Assert new properties have the correct values - expect(response.organisationUnits[1].featureType).toEqual("POLYGON"); - expect(response.organisationUnits[1].coordinates).toEqual( - "[[[[-12.0931,8.507],[-12.09,8.5025],[-12.0875,8.4996],[-12.0814,8.4934],[-12.0779,8.4897],[-12.0753,8.4859],[-12.0735,8.4844],[-12.0679,8.4816],[-12.0629,8.48],[-12.0612,8.4781],[-12.0606,8.4756],[-12.0605,8.4729],[-12.0605,8.4623],[-12.0607,8.4585],[-12.0612,8.4558],[-12.0634,8.4505],[-12.0638,8.448],[-12.0636,8.4445],[-12.0624,8.4396],[-12.0638,8.4336],[-12.0637,8.4297],[-12.0626,8.4274],[-12.0603,8.4258],[-12.0571,8.4255],[-12.0524,8.4276],[-12.0481,8.4281],[-12.0451,8.4274],[-12.0399,8.4236],[-12.0376,8.4228],[-12.0356,8.4232],[-12.0306,8.4255],[-12.0272,8.4275],[-12.0239,8.428],[-12.021,8.4265],[-12.0195,8.4241],[-12.0196,8.4212],[-12.0216,8.4169],[-12.0233,8.4111],[-12.0253,8.4068],[-12.0255,8.4032],[-12.0242,8.4007],[-12.0225,8.3994],[-12.0194,8.3986],[-12.0166,8.3985],[-12.0088,8.3988],[-12.005,8.3987],[-12.0017,8.398],[-11.9999,8.3964],[-11.9979,8.3938],[-11.9916,8.3869],[-11.9904,8.3834],[-11.9914,8.3807],[-11.9937,8.3772],[-11.9982,8.3731],[-12.0036,8.3677],[-12.0093,8.3641],[-12.021,8.358],[-12.0268,8.3539],[-12.0322,8.3507],[-12.0363,8.347],[-12.0399,8.3428],[-12.0442,8.333],[-12.049,8.3247],[-12.0529,8.319],[-12.0544,8.3152],[-12.0545,8.311],[-12.0527,8.3072],[-12.0499,8.3041],[-12.0465,8.3017],[-12.0403,8.2993],[-12.0371,8.2974],[-12.0348,8.2947],[-12.032,8.2896],[-12.0307,8.2841],[-12.0296,8.272],[-12.0268,8.2613],[-12.0265,8.2561],[-12.0272,8.2529],[-12.0299,8.2496],[-12.0337,8.2478],[-12.0366,8.2474],[-12.0411,8.2474],[-12.0456,8.248],[-12.0485,8.2489],[-12.0522,8.2513],[-12.0609,8.2593],[-12.0643,8.2617],[-12.0734,8.2657],[-12.0833,8.267],[-12.0874,8.2685],[-12.0911,8.2711],[-12.1011,8.2806],[-12.1059,8.2842],[-12.1136,8.2877],[-12.1225,8.2896],[-12.1262,8.2914],[-12.1294,8.2941],[-12.1322,8.2973],[-12.1356,8.3026],[-12.1407,8.3093],[-12.1441,8.3145],[-12.1458,8.3168],[-12.149,8.3197],[-12.1514,8.3213],[-12.1578,8.3238],[-12.1651,8.328],[-12.1692,8.3293],[-12.1765,8.3299],[-12.1945,8.3299],[-12.2003,8.3322],[-12.2046,8.3331],[-12.2151,8.334],[-12.2192,8.335],[-12.2257,8.3379],[-12.2321,8.3413],[-12.2414,8.3475],[-12.2467,8.3502],[-12.2504,8.3516],[-12.2547,8.3522],[-12.2606,8.3525],[-12.265,8.3523],[-12.2679,8.3518],[-12.2718,8.3502],[-12.2762,8.3467],[-12.2788,8.3436],[-12.2821,8.3384],[-12.284,8.3362],[-12.2863,8.3343],[-12.2903,8.3321],[-12.2937,8.3306],[-12.2966,8.3299],[-12.2997,8.3296],[-12.3073,8.3298],[-12.3115,8.3307],[-12.3194,8.3341],[-12.3387,8.344],[-12.3464,8.3474],[-12.3506,8.3483],[-12.3564,8.3483],[-12.3605,8.3475],[-12.3671,8.3445],[-12.3718,8.3412],[-12.3773,8.3359],[-12.387,8.3259],[-12.3899,8.3224],[-12.3943,8.3166],[-12.4044,8.3079],[-12.4087,8.3051],[-12.4121,8.3044],[-12.4158,8.3054],[-12.4183,8.3077],[-12.4213,8.3141],[-12.424,8.3172],[-12.427,8.3182],[-12.4304,8.3164],[-12.4319,8.313],[-12.4322,8.3012],[-12.4325,8.2982],[-12.4345,8.2908],[-12.4364,8.2813],[-12.439,8.2729],[-12.4475,8.2802],[-12.4526,8.2869],[-12.4581,8.2906],[-12.4626,8.2909],[-12.4702,8.2894],[-12.4842,8.2882],[-12.4949,8.2854],[-12.4995,8.2849],[-12.5043,8.2848],[-12.5105,8.2853],[-12.5147,8.2865],[-12.5211,8.2896],[-12.5271,8.2935],[-12.5309,8.2953],[-12.549,8.2983],[-12.5523,8.2977],[-12.5597,8.2941],[-12.564,8.293],[-12.5702,8.2924],[-12.57,8.2998],[-12.5696,8.3041],[-12.5671,8.314],[-12.567,8.3193],[-12.5685,8.3254],[-12.5703,8.3288],[-12.5741,8.3334],[-12.5786,8.3375],[-12.5833,8.3406],[-12.5899,8.3437],[-12.5964,8.3459],[-12.605,8.3508],[-12.6163,8.3545],[-12.6261,8.356],[-12.63,8.3572],[-12.6329,8.3593],[-12.6364,8.3635],[-12.639,8.3659],[-12.6464,8.371],[-12.6502,8.3731],[-12.6543,8.3741],[-12.6585,8.3744],[-12.6644,8.3742],[-12.6719,8.3735],[-12.668,8.3838],[-12.666,8.3876],[-12.6611,8.3934],[-12.6545,8.3996],[-12.6521,8.4014],[-12.6495,8.4028],[-12.6466,8.4037],[-12.6393,8.4053],[-12.6306,8.4096],[-12.6265,8.4132],[-12.6238,8.4164],[-12.6202,8.4214],[-12.6181,8.4234],[-12.6144,8.4254],[-12.6076,8.4272],[-12.6036,8.43],[-12.6022,8.433],[-12.6018,8.4378],[-12.6027,8.4433],[-12.6057,8.4505],[-12.6055,8.4546],[-12.6036,8.4574],[-12.5999,8.4595],[-12.589,8.4612],[-12.5809,8.464],[-12.5731,8.4643],[-12.571,8.4629],[-12.5657,8.4552],[-12.5607,8.4508],[-12.5562,8.4454],[-12.5536,8.443],[-12.5498,8.4404],[-12.5471,8.4379],[-12.545,8.4351],[-12.5417,8.4296],[-12.5397,8.4278],[-12.5355,8.4264],[-12.5315,8.4264],[-12.5284,8.4276],[-12.5264,8.4294],[-12.5241,8.4332],[-12.5221,8.4386],[-12.5184,8.4517],[-12.5149,8.4551],[-12.511,8.4563],[-12.5068,8.4564],[-12.5025,8.4557],[-12.4966,8.4537],[-12.4924,8.453],[-12.4866,8.4529],[-12.4824,8.4536],[-12.4666,8.4587],[-12.4638,8.4593],[-12.4568,8.4602],[-12.4515,8.462],[-12.4471,8.4653],[-12.4444,8.4685],[-12.4411,8.4738],[-12.4375,8.4782],[-12.4303,8.4854],[-12.4223,8.4916],[-12.4201,8.4944],[-12.4188,8.4973],[-12.4049,8.4969],[-12.4005,8.4961],[-12.3959,8.4947],[-12.3904,8.4945],[-12.3756,8.498],[-12.372,8.4999],[-12.3689,8.5027],[-12.3662,8.5059],[-12.3621,8.5121],[-12.3593,8.5152],[-12.3559,8.5176],[-12.352,8.519],[-12.341,8.5211],[-12.3364,8.5241],[-12.3328,8.5283],[-12.3275,8.5383],[-12.3247,8.5406],[-12.3206,8.5418],[-12.3103,8.5426],[-12.3062,8.5435],[-12.2962,8.5469],[-12.2877,8.5519],[-12.2812,8.5542],[-12.2696,8.5588],[-12.265,8.5572],[-12.2621,8.5537],[-12.2598,8.552],[-12.2559,8.5501],[-12.2485,8.5446],[-12.2454,8.5432],[-12.241,8.5408],[-12.2378,8.5394],[-12.2335,8.5371],[-12.2303,8.5357],[-12.2259,8.5335],[-12.2199,8.5321],[-12.2146,8.5298],[-12.2086,8.5284],[-12.2033,8.5261],[-12.1965,8.5244],[-12.1929,8.5226],[-12.1876,8.5216],[-12.1799,8.5215],[-12.1749,8.5209],[-12.1685,8.5185],[-12.1646,8.5179],[-12.1574,8.5178],[-12.1544,8.5175],[-12.1515,8.5169],[-12.147,8.515],[-12.1441,8.5145],[-12.1374,8.514],[-12.1336,8.5135],[-12.1282,8.5113],[-12.1223,8.5098],[-12.116,8.5073],[-12.1122,8.5067],[-12.1071,8.5066],[-12.098,8.5067],[-12.0931,8.507]]]]" - ); + it("Local server to remote - organisationUnits featureType type POLYGON to geometry - API 32 to API 30", async () => { + const orgUnit = payload.organisationUnits["ou_id2"]; + expect(orgUnit).toBeDefined(); + expect(orgUnit.name).toEqual("Test org unit"); + expect(orgUnit.dimensionItemType).toEqual("ORGANISATION_UNIT"); - // Assert old properties are not anymore - expect(response.organisationUnits[1].geometry).toBeUndefined(); + // Assert new properties have the correct values + expect(orgUnit.featureType).toEqual("POLYGON"); + expect(orgUnit.coordinates).toEqual( + "[[[[-12.0931,8.507],[-12.09,8.5025],[-12.0875,8.4996],[-12.0814,8.4934],[-12.0779,8.4897],[-12.0753,8.4859],[-12.0735,8.4844],[-12.0679,8.4816],[-12.0629,8.48],[-12.0612,8.4781],[-12.0606,8.4756],[-12.0605,8.4729],[-12.0605,8.4623],[-12.0607,8.4585],[-12.0612,8.4558],[-12.0634,8.4505],[-12.0638,8.448],[-12.0636,8.4445],[-12.0624,8.4396],[-12.0638,8.4336],[-12.0637,8.4297],[-12.0626,8.4274],[-12.0603,8.4258],[-12.0571,8.4255],[-12.0524,8.4276],[-12.0481,8.4281],[-12.0451,8.4274],[-12.0399,8.4236],[-12.0376,8.4228],[-12.0356,8.4232],[-12.0306,8.4255],[-12.0272,8.4275],[-12.0239,8.428],[-12.021,8.4265],[-12.0195,8.4241],[-12.0196,8.4212],[-12.0216,8.4169],[-12.0233,8.4111],[-12.0253,8.4068],[-12.0255,8.4032],[-12.0242,8.4007],[-12.0225,8.3994],[-12.0194,8.3986],[-12.0166,8.3985],[-12.0088,8.3988],[-12.005,8.3987],[-12.0017,8.398],[-11.9999,8.3964],[-11.9979,8.3938],[-11.9916,8.3869],[-11.9904,8.3834],[-11.9914,8.3807],[-11.9937,8.3772],[-11.9982,8.3731],[-12.0036,8.3677],[-12.0093,8.3641],[-12.021,8.358],[-12.0268,8.3539],[-12.0322,8.3507],[-12.0363,8.347],[-12.0399,8.3428],[-12.0442,8.333],[-12.049,8.3247],[-12.0529,8.319],[-12.0544,8.3152],[-12.0545,8.311],[-12.0527,8.3072],[-12.0499,8.3041],[-12.0465,8.3017],[-12.0403,8.2993],[-12.0371,8.2974],[-12.0348,8.2947],[-12.032,8.2896],[-12.0307,8.2841],[-12.0296,8.272],[-12.0268,8.2613],[-12.0265,8.2561],[-12.0272,8.2529],[-12.0299,8.2496],[-12.0337,8.2478],[-12.0366,8.2474],[-12.0411,8.2474],[-12.0456,8.248],[-12.0485,8.2489],[-12.0522,8.2513],[-12.0609,8.2593],[-12.0643,8.2617],[-12.0734,8.2657],[-12.0833,8.267],[-12.0874,8.2685],[-12.0911,8.2711],[-12.1011,8.2806],[-12.1059,8.2842],[-12.1136,8.2877],[-12.1225,8.2896],[-12.1262,8.2914],[-12.1294,8.2941],[-12.1322,8.2973],[-12.1356,8.3026],[-12.1407,8.3093],[-12.1441,8.3145],[-12.1458,8.3168],[-12.149,8.3197],[-12.1514,8.3213],[-12.1578,8.3238],[-12.1651,8.328],[-12.1692,8.3293],[-12.1765,8.3299],[-12.1945,8.3299],[-12.2003,8.3322],[-12.2046,8.3331],[-12.2151,8.334],[-12.2192,8.335],[-12.2257,8.3379],[-12.2321,8.3413],[-12.2414,8.3475],[-12.2467,8.3502],[-12.2504,8.3516],[-12.2547,8.3522],[-12.2606,8.3525],[-12.265,8.3523],[-12.2679,8.3518],[-12.2718,8.3502],[-12.2762,8.3467],[-12.2788,8.3436],[-12.2821,8.3384],[-12.284,8.3362],[-12.2863,8.3343],[-12.2903,8.3321],[-12.2937,8.3306],[-12.2966,8.3299],[-12.2997,8.3296],[-12.3073,8.3298],[-12.3115,8.3307],[-12.3194,8.3341],[-12.3387,8.344],[-12.3464,8.3474],[-12.3506,8.3483],[-12.3564,8.3483],[-12.3605,8.3475],[-12.3671,8.3445],[-12.3718,8.3412],[-12.3773,8.3359],[-12.387,8.3259],[-12.3899,8.3224],[-12.3943,8.3166],[-12.4044,8.3079],[-12.4087,8.3051],[-12.4121,8.3044],[-12.4158,8.3054],[-12.4183,8.3077],[-12.4213,8.3141],[-12.424,8.3172],[-12.427,8.3182],[-12.4304,8.3164],[-12.4319,8.313],[-12.4322,8.3012],[-12.4325,8.2982],[-12.4345,8.2908],[-12.4364,8.2813],[-12.439,8.2729],[-12.4475,8.2802],[-12.4526,8.2869],[-12.4581,8.2906],[-12.4626,8.2909],[-12.4702,8.2894],[-12.4842,8.2882],[-12.4949,8.2854],[-12.4995,8.2849],[-12.5043,8.2848],[-12.5105,8.2853],[-12.5147,8.2865],[-12.5211,8.2896],[-12.5271,8.2935],[-12.5309,8.2953],[-12.549,8.2983],[-12.5523,8.2977],[-12.5597,8.2941],[-12.564,8.293],[-12.5702,8.2924],[-12.57,8.2998],[-12.5696,8.3041],[-12.5671,8.314],[-12.567,8.3193],[-12.5685,8.3254],[-12.5703,8.3288],[-12.5741,8.3334],[-12.5786,8.3375],[-12.5833,8.3406],[-12.5899,8.3437],[-12.5964,8.3459],[-12.605,8.3508],[-12.6163,8.3545],[-12.6261,8.356],[-12.63,8.3572],[-12.6329,8.3593],[-12.6364,8.3635],[-12.639,8.3659],[-12.6464,8.371],[-12.6502,8.3731],[-12.6543,8.3741],[-12.6585,8.3744],[-12.6644,8.3742],[-12.6719,8.3735],[-12.668,8.3838],[-12.666,8.3876],[-12.6611,8.3934],[-12.6545,8.3996],[-12.6521,8.4014],[-12.6495,8.4028],[-12.6466,8.4037],[-12.6393,8.4053],[-12.6306,8.4096],[-12.6265,8.4132],[-12.6238,8.4164],[-12.6202,8.4214],[-12.6181,8.4234],[-12.6144,8.4254],[-12.6076,8.4272],[-12.6036,8.43],[-12.6022,8.433],[-12.6018,8.4378],[-12.6027,8.4433],[-12.6057,8.4505],[-12.6055,8.4546],[-12.6036,8.4574],[-12.5999,8.4595],[-12.589,8.4612],[-12.5809,8.464],[-12.5731,8.4643],[-12.571,8.4629],[-12.5657,8.4552],[-12.5607,8.4508],[-12.5562,8.4454],[-12.5536,8.443],[-12.5498,8.4404],[-12.5471,8.4379],[-12.545,8.4351],[-12.5417,8.4296],[-12.5397,8.4278],[-12.5355,8.4264],[-12.5315,8.4264],[-12.5284,8.4276],[-12.5264,8.4294],[-12.5241,8.4332],[-12.5221,8.4386],[-12.5184,8.4517],[-12.5149,8.4551],[-12.511,8.4563],[-12.5068,8.4564],[-12.5025,8.4557],[-12.4966,8.4537],[-12.4924,8.453],[-12.4866,8.4529],[-12.4824,8.4536],[-12.4666,8.4587],[-12.4638,8.4593],[-12.4568,8.4602],[-12.4515,8.462],[-12.4471,8.4653],[-12.4444,8.4685],[-12.4411,8.4738],[-12.4375,8.4782],[-12.4303,8.4854],[-12.4223,8.4916],[-12.4201,8.4944],[-12.4188,8.4973],[-12.4049,8.4969],[-12.4005,8.4961],[-12.3959,8.4947],[-12.3904,8.4945],[-12.3756,8.498],[-12.372,8.4999],[-12.3689,8.5027],[-12.3662,8.5059],[-12.3621,8.5121],[-12.3593,8.5152],[-12.3559,8.5176],[-12.352,8.519],[-12.341,8.5211],[-12.3364,8.5241],[-12.3328,8.5283],[-12.3275,8.5383],[-12.3247,8.5406],[-12.3206,8.5418],[-12.3103,8.5426],[-12.3062,8.5435],[-12.2962,8.5469],[-12.2877,8.5519],[-12.2812,8.5542],[-12.2696,8.5588],[-12.265,8.5572],[-12.2621,8.5537],[-12.2598,8.552],[-12.2559,8.5501],[-12.2485,8.5446],[-12.2454,8.5432],[-12.241,8.5408],[-12.2378,8.5394],[-12.2335,8.5371],[-12.2303,8.5357],[-12.2259,8.5335],[-12.2199,8.5321],[-12.2146,8.5298],[-12.2086,8.5284],[-12.2033,8.5261],[-12.1965,8.5244],[-12.1929,8.5226],[-12.1876,8.5216],[-12.1799,8.5215],[-12.1749,8.5209],[-12.1685,8.5185],[-12.1646,8.5179],[-12.1574,8.5178],[-12.1544,8.5175],[-12.1515,8.5169],[-12.147,8.515],[-12.1441,8.5145],[-12.1374,8.514],[-12.1336,8.5135],[-12.1282,8.5113],[-12.1223,8.5098],[-12.116,8.5073],[-12.1122,8.5067],[-12.1071,8.5066],[-12.098,8.5067],[-12.0931,8.507]]]]" + ); - // Assert we have not updated local metadata - expect(local.db.metadata.find(1)).toBeNull(); + // Assert old properties are not anymore + expect(orgUnit.geometry).toBeUndefined(); + }); }); }); -function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); - repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); - repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); - repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); - return repositoryFactory; -} - export {}; diff --git a/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts index 8c0cf9c94..7c92e4b35 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts @@ -1,35 +1,35 @@ import { sync, SyncResult } from "./helpers"; -const metadata = { - programs: [ - { - id: "id1", - name: "Test tracker program", - captureCoordinates: true, - }, - { - id: "id2", - name: "Test tracker program", - captureCoordinates: false, - }, - ], - programStages: [ - { - id: "ps_id1", - name: "Test programStage", - validCompleteOnly: false, - }, - { - id: "ps_id2", - name: "Test programStage", - validCompleteOnly: true, - }, - ], -}; - let payload: SyncResult; describe("Transformations for 2.30 -> 2.31", () => { + const metadata = { + programs: [ + { + id: "id1", + name: "Test tracker program", + captureCoordinates: true, + }, + { + id: "id2", + name: "Test tracker program", + captureCoordinates: false, + }, + ], + programStages: [ + { + id: "ps_id1", + name: "Test programStage", + validCompleteOnly: false, + }, + { + id: "ps_id2", + name: "Test programStage", + validCompleteOnly: true, + }, + ], + }; + beforeAll(async () => { payload = await sync({ from: "2.30", diff --git a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts index ce25cdfce..d157c2582 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts @@ -70,6 +70,14 @@ describe("Sync metadata", () => { local.get("/dataStore/metadata-synchronization/instances", async () => [ { + type: "local", + id: "LOCAL", + name: "This instance", + description: "", + url: "http://origin.test", + }, + { + type: "dhis", id: "DESTINATION", name: "Destination test", url: "http://destination.test", @@ -79,6 +87,7 @@ describe("Sync metadata", () => { }, ]); + local.get("/dataStore/metadata-synchronization/instances-LOCAL", async () => ({})); local.get("/dataStore/metadata-synchronization/instances-DESTINATION", async () => ({})); const addMetadataToDb = async (schema: Schema, request: Request) => { diff --git a/src/data/transformations/__tests__/integration/transformations-api-34.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-34.spec.ts index a37519d15..19a0a9441 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-34.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-34.spec.ts @@ -3,196 +3,207 @@ import { sync } from "./helpers"; import visualizations30 from "./data/visualizations-30.json"; import visualizations34 from "./data/visualizations-34.json"; -describe("Transformation 2.30 -> 2.34", () => { - it("Transforms report params", async () => { - const metadata = { - reports: [ - { - id: "id1", - name: "Test Severity Report", - type: "HTML", - cacheStrategy: "RESPECT_SYSTEM_SETTING", - reportParams: { - paramGrandParentOrganisationUnit: false, - paramReportingPeriod: true, - paramOrganisationUnit: true, - paramParentOrganisationUnit: false, - }, - }, - ], - }; - - const { reports } = await sync({ from: "2.30", to: "2.34", metadata, models: ["reports"] }); - const report = reports["id1"]; - expect(report).toBeDefined(); - - expect(report.type).toEqual("HTML"); - - const { reportParams } = report; - expect(reportParams).toBeDefined(); - - // Assert new properties have the correct values - expect(reportParams.grandParentOrganisationUnit).toEqual(false); - expect(reportParams.reportingPeriod).toEqual(true); - expect(reportParams.organisationUnit).toEqual(true); - expect(reportParams.parentOrganisationUnit).toEqual(false); - - // Assert old properties are not anymore - expect(reportParams.paramGrandParentOrganisationUnit).toBeUndefined(); - expect(reportParams.paramReportingPeriod).toBeUndefined(); - expect(reportParams.paramOrganisationUnit).toBeUndefined(); - expect(reportParams.paramParentOrganisationUnit).toBeUndefined(); +describe("API 34", () => { + beforeAll(() => { + jest.setTimeout(30000); }); - it("Transforms dashboard items", async () => { - const metadata = { - dashboards: [ - { - id: "dashboard1", - dashboardItems: [ - { - id: "item1", - type: "CHART", - chart: { id: "v7g3iMUFcsD" }, + describe("Transformation 2.30 -> 2.34", () => { + it("Transforms report params", async () => { + const metadata = { + reports: [ + { + id: "id1", + name: "Test Severity Report", + type: "HTML", + cacheStrategy: "RESPECT_SYSTEM_SETTING", + reportParams: { + paramGrandParentOrganisationUnit: false, + paramReportingPeriod: true, + paramOrganisationUnit: true, + paramParentOrganisationUnit: false, }, - ], - }, - ], - }; - - const { dashboards } = await sync({ - from: "2.30", - to: "2.34", - metadata, - models: ["dashboards"], + }, + ], + }; + + const { reports } = await sync({ + from: "2.30", + to: "2.34", + metadata, + models: ["reports"], + }); + const report = reports["id1"]; + expect(report).toBeDefined(); + + expect(report.type).toEqual("HTML"); + + const { reportParams } = report; + expect(reportParams).toBeDefined(); + + // Assert new properties have the correct values + expect(reportParams.grandParentOrganisationUnit).toEqual(false); + expect(reportParams.reportingPeriod).toEqual(true); + expect(reportParams.organisationUnit).toEqual(true); + expect(reportParams.parentOrganisationUnit).toEqual(false); + + // Assert old properties are not anymore + expect(reportParams.paramGrandParentOrganisationUnit).toBeUndefined(); + expect(reportParams.paramReportingPeriod).toBeUndefined(); + expect(reportParams.paramOrganisationUnit).toBeUndefined(); + expect(reportParams.paramParentOrganisationUnit).toBeUndefined(); }); - const dashboard = dashboards["dashboard1"]; - expect(dashboard).toBeDefined(); - const [item] = dashboard?.dashboardItems; - expect(item).toBeDefined(); - - expect(item.type).toEqual("VISUALIZATION"); - expect(item.visualization, "to keep referencing the chart").toEqual({ - id: "v7g3iMUFcsD", + it("Transforms dashboard items", async () => { + const metadata = { + dashboards: [ + { + id: "dashboard1", + dashboardItems: [ + { + id: "item1", + type: "CHART", + chart: { id: "v7g3iMUFcsD" }, + }, + ], + }, + ], + }; + + const { dashboards } = await sync({ + from: "2.30", + to: "2.34", + metadata, + models: ["dashboards"], + }); + const dashboard = dashboards["dashboard1"]; + expect(dashboard).toBeDefined(); + + const [item] = dashboard?.dashboardItems; + expect(item).toBeDefined(); + + expect(item.type).toEqual("VISUALIZATION"); + expect(item.visualization, "to keep referencing the chart").toEqual({ + id: "v7g3iMUFcsD", + }); + expect(item.chart, "is no longer set").toBeUndefined(); }); - expect(item.chart, "is no longer set").toBeUndefined(); - }); - it("Transforms charts and report tables to visualizations", async () => { - const payload = await sync({ - from: "2.30", - to: "2.34", - metadata: visualizations30, - models: ["visualizations"], + it("Transforms charts and report tables to visualizations", async () => { + const payload = await sync({ + from: "2.30", + to: "2.34", + metadata: visualizations30, + models: ["visualizations"], + }); + + expect( + payload.visualizations["LW0O27b7TdD"], + "Chart to be transformed into a visualization" + ).toMatchObject(visualizations34.visualizations[0]); + + expect( + payload.visualizations["qfMh2IjOxvw"], + "Report table to be transformed into a visualization" + ).toMatchObject(visualizations34.visualizations[1]); }); - - expect( - payload.visualizations["LW0O27b7TdD"], - "Chart to be transformed into a visualization" - ).toMatchObject(visualizations34.visualizations[0]); - - expect( - payload.visualizations["qfMh2IjOxvw"], - "Report table to be transformed into a visualization" - ).toMatchObject(visualizations34.visualizations[1]); }); -}); - -describe("Transformation 2.34 -> 2.30", () => { - it("Transforms dashboard items", async () => { - const metadata = { - dashboards: [ - { - id: "dashboard1", - dashboardItems: [ - { - id: "item1", - type: "VISUALIZATION", - visualization: { id: "chart1" }, - }, - { - id: "item2", - type: "VISUALIZATION", - visualization: { id: "reportTable1" }, - }, - { - id: "item3", - type: "MAP", - map: { id: "map1" }, - }, - ], - }, - ], - visualizations: [ - { - id: "chart1", - type: "LINE", - }, - { - id: "reportTable1", - type: "PIVOT_TABLE", - }, - ], - }; - - const { dashboards } = await sync({ - from: "2.34", - to: "2.30", - metadata, - models: ["dashboards"], - }); - - const dashboard = dashboards["dashboard1"]; - expect(dashboard).toBeDefined(); - expect(dashboard?.dashboardItems).toHaveLength(3); - const [chartItem, reportTableItem, mapItem] = dashboard?.dashboardItems; - // Chart item - expect(chartItem).toBeDefined(); - - expect(chartItem.type).toEqual("CHART"); - expect(chartItem.chart, "to reference the chart").toEqual({ - id: "chart1", - }); - expect(chartItem.visualization, "is no longer set").toBeUndefined(); - - // Report table item - expect(reportTableItem).toBeDefined(); - - expect(reportTableItem.type).toEqual("REPORT_TABLE"); - expect(reportTableItem.reportTable, "to reference the report table").toEqual({ - id: "reportTable1", - }); - expect(reportTableItem.visualization, "is no longer set").toBeUndefined(); - - // Other item - expect(mapItem).toBeDefined(); - - expect(mapItem.type).toEqual("MAP"); - expect(mapItem.map, "to reference the map").toEqual({ - id: "map1", + describe("Transformation 2.34 -> 2.30", () => { + it("Transforms dashboard items", async () => { + const metadata = { + dashboards: [ + { + id: "dashboard1", + dashboardItems: [ + { + id: "item1", + type: "VISUALIZATION", + visualization: { id: "chart1" }, + }, + { + id: "item2", + type: "VISUALIZATION", + visualization: { id: "reportTable1" }, + }, + { + id: "item3", + type: "MAP", + map: { id: "map1" }, + }, + ], + }, + ], + visualizations: [ + { + id: "chart1", + type: "LINE", + }, + { + id: "reportTable1", + type: "PIVOT_TABLE", + }, + ], + }; + + const { dashboards } = await sync({ + from: "2.34", + to: "2.30", + metadata, + models: ["dashboards"], + }); + + const dashboard = dashboards["dashboard1"]; + expect(dashboard).toBeDefined(); + expect(dashboard?.dashboardItems).toHaveLength(3); + const [chartItem, reportTableItem, mapItem] = dashboard?.dashboardItems; + + // Chart item + expect(chartItem).toBeDefined(); + + expect(chartItem.type).toEqual("CHART"); + expect(chartItem.chart, "to reference the chart").toEqual({ + id: "chart1", + }); + expect(chartItem.visualization, "is no longer set").toBeUndefined(); + + // Report table item + expect(reportTableItem).toBeDefined(); + + expect(reportTableItem.type).toEqual("REPORT_TABLE"); + expect(reportTableItem.reportTable, "to reference the report table").toEqual({ + id: "reportTable1", + }); + expect(reportTableItem.visualization, "is no longer set").toBeUndefined(); + + // Other item + expect(mapItem).toBeDefined(); + + expect(mapItem.type).toEqual("MAP"); + expect(mapItem.map, "to reference the map").toEqual({ + id: "map1", + }); + expect(mapItem.visualization).toBeUndefined(); }); - expect(mapItem.visualization).toBeUndefined(); - }); - it("Transforms visualizations into charts and report tables", async () => { - const payload = await sync({ - from: "2.34", - to: "2.30", - metadata: visualizations34, - models: ["charts", "reportTables"], + it("Transforms visualizations into charts and report tables", async () => { + const payload = await sync({ + from: "2.34", + to: "2.30", + metadata: visualizations34, + models: ["charts", "reportTables"], + }); + + expect( + payload.charts["LW0O27b7TdD"], + "Chart to be transformed into a visualization" + ).toMatchObject(visualizations30.charts[0]); + + expect( + payload.reportTables["qfMh2IjOxvw"], + "Report table to be transformed into a visualization" + ).toMatchObject(visualizations30.reportTables[0]); }); - - expect( - payload.charts["LW0O27b7TdD"], - "Chart to be transformed into a visualization" - ).toMatchObject(visualizations30.charts[0]); - - expect( - payload.reportTables["qfMh2IjOxvw"], - "Report table to be transformed into a visualization" - ).toMatchObject(visualizations30.reportTables[0]); }); }); diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index cbc5da6de..41c552123 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -36,6 +36,7 @@ export class ListInstancesUseCase implements UseCase { Instance.build({ ...data, url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, }).decryptPassword(this.encryptionKey) ); } From 45571f4510dc285e26910ba430604f6be90b2912 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 27 Nov 2020 13:04:16 +0100 Subject: [PATCH 033/163] Add missing import --- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index c4f846aaa..122f8a5b6 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -11,7 +11,7 @@ import { getD2APiFromInstance } from "../../../utils/d2-utils"; import { debug } from "../../../utils/debug"; import { AggregatedPackage } from "../../aggregated/entities/AggregatedPackage"; import { AggregatedSyncUseCase } from "../../aggregated/usecases/AggregatedSyncUseCase"; -import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Repositories, RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { EventsPackage } from "../../events/entities/EventsPackage"; import { EventsSyncUseCase } from "../../events/usecases/EventsSyncUseCase"; import { FileRepositoryConstructor } from "../../file/FileRepository"; From 1b9bcc9fec841d03d96c30afcab3f9596fcc1144 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 1 Dec 2020 10:52:28 +0100 Subject: [PATCH 034/163] Add execution rights to git hook --- .githooks/dep-check | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .githooks/dep-check diff --git a/.githooks/dep-check b/.githooks/dep-check old mode 100644 new mode 100755 From 1fccec46e8cc44b253094fdd871d7f52d1414c36 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 1 Dec 2020 10:55:31 +0100 Subject: [PATCH 035/163] Remove old type --- src/models/syncReport.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/models/syncReport.ts b/src/models/syncReport.ts index f2cc56a09..07e4283ff 100644 --- a/src/models/syncReport.ts +++ b/src/models/syncReport.ts @@ -10,6 +10,7 @@ import { SynchronizationResult } from "../domain/synchronization/entities/Synchr import { SynchronizationType } from "../domain/synchronization/entities/SynchronizationType"; import { D2Api } from "../types/d2-api"; import { SyncReportTableFilters } from "../types/d2-ui-components"; +import { PartialBy } from "../types/utils"; import { deleteData, deleteDataStore, @@ -22,13 +23,11 @@ import { const dataStoreKey = Namespace.HISTORY; -type Optional = Omit & { [P in Extract]?: T[P] }; - export default class SyncReport { private results: SynchronizationResult[] | null; public readonly syncReport: SynchronizationReport; - constructor(syncReport: Optional) { + constructor(syncReport: PartialBy) { this.results = null; this.syncReport = { id: generateUid(), @@ -62,7 +61,7 @@ export default class SyncReport { }); } - public static build(syncReport?: Optional): SyncReport { + public static build(syncReport?: PartialBy): SyncReport { return syncReport ? new SyncReport(syncReport) : this.create(); } From d455e16b2a4be316969c8511590df1bc849bc094 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 2 Dec 2020 10:39:49 +0100 Subject: [PATCH 036/163] Fix tests --- src/data/transformations/__tests__/integration/helpers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 8f2681bff..500c9b9e9 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -16,7 +16,7 @@ import { TransformationD2ApiRepository } from "../../../transformations/Transfor import { SynchronizationBuilder } from "./../../../../types/synchronization"; export function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); @@ -121,9 +121,11 @@ export async function executeMetadataSync( const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - for await (const _sync of useCase.execute()) { - // no-op + let done = false; + for await (const sync of useCase.execute()) { + done = !!sync.done; } + expect(done).toBeTruthy(); expect(local.db.metadata.where({})).toHaveLength(0); From b6b24c60e110a57172d1faa1d02a67f4ff8401a7 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 2 Dec 2020 10:44:30 +0100 Subject: [PATCH 037/163] Add save use-case --- .../aggregated/AggregatedD2ApiRepository.ts | 2 +- src/data/events/EventsD2ApiRepository.ts | 2 +- src/data/metadata/MetadataD2ApiRepository.ts | 2 +- src/data/metadata/MetadataJSONRepository.ts | 2 +- src/data/reports/ReportsD2ApiRepository.ts | 45 ++++++ .../repositories/AggregatedRepository.ts | 2 +- .../common/factories/RepositoryFactory.ts | 7 + .../events/repositories/EventsRepository.ts | 2 +- .../events/usecases/EventsSyncUseCase.ts | 2 +- .../repositories/MetadataRepository.ts | 2 +- .../usecases/ImportMetadataUseCase.ts | 2 +- .../metadata/usecases/MetadataSyncUseCase.ts | 2 +- .../usecases/ImportPullRequestUseCase.ts | 2 +- .../packages/usecases/ImportPackageUseCase.ts | 2 +- .../reports/entities/SynchronizationReport.ts | 120 ++++++++++++++ .../entities/SynchronizationResult.ts | 2 +- .../reports/repositories/ReportsRepository.ts | 13 ++ .../reports/usecases/SaveSyncReportUseCase.ts | 12 ++ .../entities/SynchronizationReport.ts | 27 ---- .../usecases/GenericSyncUseCase.ts | 11 +- src/migrations/tasks/03.sync-reports.ts | 4 +- .../tasks/04.history-notifications.ts | 4 +- src/models/syncReport.md | 57 +++++++ src/models/syncReport.ts | 146 ------------------ src/presentation/CompositionRoot.ts | 8 + .../module-list-table/ModuleListTable.tsx | 8 +- .../ModulePackageListTable.tsx | 10 +- .../PackageImportDialog.tsx | 10 +- .../package-list-table/PackageListTable.tsx | 9 +- .../components/packages-diff-dialog/utils.tsx | 13 +- .../components/sync-summary/SyncSummary.tsx | 20 ++- .../webapp/pages/history/HistoryPage.tsx | 28 ++-- .../pages/manual-sync/ManualSyncPage.tsx | 15 +- .../ModulePackageListPage.tsx | 8 +- .../NotificationsListPage.tsx | 17 +- .../sync-rules-list/SyncRulesListPage.tsx | 17 +- src/scheduler/scheduler.ts | 2 +- src/types/synchronization.ts | 6 +- 38 files changed, 371 insertions(+), 272 deletions(-) create mode 100644 src/data/reports/ReportsD2ApiRepository.ts create mode 100644 src/domain/reports/entities/SynchronizationReport.ts rename src/domain/{synchronization => reports}/entities/SynchronizationResult.ts (90%) create mode 100644 src/domain/reports/repositories/ReportsRepository.ts create mode 100644 src/domain/reports/usecases/SaveSyncReportUseCase.ts delete mode 100644 src/domain/synchronization/entities/SynchronizationReport.ts create mode 100644 src/models/syncReport.md delete mode 100644 src/models/syncReport.ts diff --git a/src/data/aggregated/AggregatedD2ApiRepository.ts b/src/data/aggregated/AggregatedD2ApiRepository.ts index 5c7f527cc..29ae78cab 100644 --- a/src/data/aggregated/AggregatedD2ApiRepository.ts +++ b/src/data/aggregated/AggregatedD2ApiRepository.ts @@ -8,7 +8,7 @@ import { buildPeriodFromParams } from "../../domain/aggregated/utils"; import { Instance } from "../../domain/instance/entities/Instance"; import { MetadataMappingDictionary } from "../../domain/mapping/entities/MetadataMapping"; import { CategoryOptionCombo } from "../../domain/metadata/entities/MetadataEntities"; -import { SynchronizationResult } from "../../domain/synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../domain/reports/entities/SynchronizationResult"; import { cleanOrgUnitPaths } from "../../domain/synchronization/utils"; import { DataImportParams } from "../../types/d2"; import { D2Api, DataValueSetsPostResponse } from "../../types/d2-api"; diff --git a/src/data/events/EventsD2ApiRepository.ts b/src/data/events/EventsD2ApiRepository.ts index 1f4de85f7..455a2a917 100644 --- a/src/data/events/EventsD2ApiRepository.ts +++ b/src/data/events/EventsD2ApiRepository.ts @@ -7,7 +7,7 @@ import { Instance } from "../../domain/instance/entities/Instance"; import { SynchronizationResult, SynchronizationStats, -} from "../../domain/synchronization/entities/SynchronizationResult"; +} from "../../domain/reports/entities/SynchronizationResult"; import { cleanObjectDefault, cleanOrgUnitPaths } from "../../domain/synchronization/utils"; import { DataImportParams } from "../../types/d2"; import { D2Api, Pager } from "../../types/d2-api"; diff --git a/src/data/metadata/MetadataD2ApiRepository.ts b/src/data/metadata/MetadataD2ApiRepository.ts index 7f98d071b..d3ec4f052 100644 --- a/src/data/metadata/MetadataD2ApiRepository.ts +++ b/src/data/metadata/MetadataD2ApiRepository.ts @@ -25,7 +25,7 @@ import { } from "../../domain/metadata/repositories/MetadataRepository"; import { MetadataImportParams } from "../../domain/metadata/types"; import { getClassName } from "../../domain/metadata/utils"; -import { SynchronizationResult } from "../../domain/synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../domain/reports/entities/SynchronizationResult"; import { cleanOrgUnitPaths } from "../../domain/synchronization/utils"; import { TransformationRepository } from "../../domain/transformations/repositories/TransformationRepository"; import { modelFactory } from "../../models/dhis/factory"; diff --git a/src/data/metadata/MetadataJSONRepository.ts b/src/data/metadata/MetadataJSONRepository.ts index dcb7320c9..2f98bfeb1 100644 --- a/src/data/metadata/MetadataJSONRepository.ts +++ b/src/data/metadata/MetadataJSONRepository.ts @@ -14,7 +14,7 @@ import { MetadataRepository, } from "../../domain/metadata/repositories/MetadataRepository"; import { MetadataImportParams } from "../../domain/metadata/types"; -import { SynchronizationResult } from "../../domain/synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../domain/reports/entities/SynchronizationResult"; import { TransformationRepository } from "../../domain/transformations/repositories/TransformationRepository"; import { Dictionary } from "../../types/utils"; diff --git a/src/data/reports/ReportsD2ApiRepository.ts b/src/data/reports/ReportsD2ApiRepository.ts new file mode 100644 index 000000000..3c21df5c0 --- /dev/null +++ b/src/data/reports/ReportsD2ApiRepository.ts @@ -0,0 +1,45 @@ +import { Instance } from "../../domain/instance/entities/Instance"; +import { + SynchronizationReport, + SynchronizationReportData, +} from "../../domain/reports/entities/SynchronizationReport"; +import { ReportsRepository } from "../../domain/reports/repositories/ReportsRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; +import { Namespace } from "../storage/Namespaces"; +import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; + +export class ReportsD2ApiRepository implements ReportsRepository { + private storageClient: StorageClient; + + constructor(instance: Instance) { + this.storageClient = new StorageDataStoreClient(instance); + } + + public async getById(id: string): Promise { + const data = await this.storageClient.getObjectInCollection( + Namespace.HISTORY, + id + ); + + return data ? SynchronizationReport.build(data) : undefined; + } + + public async list(): Promise { + const stores = await this.storageClient.listObjectsInCollection( + Namespace.HISTORY + ); + + return stores.map(data => SynchronizationReport.build(data)); + } + + public async save(report: SynchronizationReport): Promise { + await this.storageClient.saveObjectInCollection( + Namespace.HISTORY, + report.toObject() + ); + } + + public async delete(id: string): Promise { + await this.storageClient.removeObjectInCollection(Namespace.HISTORY, id); + } +} diff --git a/src/domain/aggregated/repositories/AggregatedRepository.ts b/src/domain/aggregated/repositories/AggregatedRepository.ts index 90ffac072..67ab0d749 100644 --- a/src/domain/aggregated/repositories/AggregatedRepository.ts +++ b/src/domain/aggregated/repositories/AggregatedRepository.ts @@ -2,7 +2,7 @@ import { DataImportParams } from "../../../types/d2"; import { Instance } from "../../instance/entities/Instance"; import { MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; import { CategoryOptionCombo } from "../../metadata/entities/MetadataEntities"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { AggregatedPackage } from "../entities/AggregatedPackage"; import { MappedCategoryOption } from "../entities/MappedCategoryOption"; import { DataSynchronizationParams } from "../types"; diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 49945bc76..25b91f179 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -15,6 +15,7 @@ import { MetadataRepositoryConstructor, } from "../../metadata/repositories/MetadataRepository"; import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; +import { ReportsRepositoryConstructor } from "../../reports/repositories/ReportsRepository"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; @@ -104,6 +105,11 @@ export class RepositoryFactory { public eventsRepository(instance: Instance): EventsRepository { return this.get(Repositories.EventsRepository, [instance]); } + + @cache() + public reportsRepository(instance: Instance) { + return this.get(Repositories.ReportsRepository, [instance]); + } } type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; @@ -119,4 +125,5 @@ export const Repositories = { MetadataRepository: "metadataRepository", TransformationRepository: "transformationsRepository", FileRepository: "fileRepository", + ReportsRepository: "reportsRepository", } as const; diff --git a/src/domain/events/repositories/EventsRepository.ts b/src/domain/events/repositories/EventsRepository.ts index 3ad1d1325..00d3f6cb1 100644 --- a/src/domain/events/repositories/EventsRepository.ts +++ b/src/domain/events/repositories/EventsRepository.ts @@ -1,7 +1,7 @@ import { DataImportParams } from "../../../types/d2"; import { DataSynchronizationParams } from "../../aggregated/types"; import { Instance } from "../../instance/entities/Instance"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { ProgramEvent } from "../entities/ProgramEvent"; export interface EventsRepositoryConstructor { diff --git a/src/domain/events/usecases/EventsSyncUseCase.ts b/src/domain/events/usecases/EventsSyncUseCase.ts index 41f095439..ebd5997f6 100644 --- a/src/domain/events/usecases/EventsSyncUseCase.ts +++ b/src/domain/events/usecases/EventsSyncUseCase.ts @@ -14,7 +14,7 @@ import { AggregatedSyncUseCase } from "../../aggregated/usecases/AggregatedSyncU import { Instance } from "../../instance/entities/Instance"; import { MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; import { CategoryOptionCombo } from "../../metadata/entities/MetadataEntities"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { GenericSyncUseCase, SyncronizationPayload, diff --git a/src/domain/metadata/repositories/MetadataRepository.ts b/src/domain/metadata/repositories/MetadataRepository.ts index ae99ef0d0..3090bba90 100644 --- a/src/domain/metadata/repositories/MetadataRepository.ts +++ b/src/domain/metadata/repositories/MetadataRepository.ts @@ -2,7 +2,7 @@ import { FilterSingleOperatorBase } from "d2-api/api/common"; import { IdentifiableRef, Ref } from "../../common/entities/Ref"; import { Id } from "../../common/entities/Schemas"; import { DataSource } from "../../instance/entities/DataSource"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { TransformationRepository } from "../../transformations/repositories/TransformationRepository"; import { FilterRule } from "../entities/FilterRule"; import { diff --git a/src/domain/metadata/usecases/ImportMetadataUseCase.ts b/src/domain/metadata/usecases/ImportMetadataUseCase.ts index a5c6b12e9..c37b0bfe7 100644 --- a/src/domain/metadata/usecases/ImportMetadataUseCase.ts +++ b/src/domain/metadata/usecases/ImportMetadataUseCase.ts @@ -1,7 +1,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { MetadataPackage } from "../entities/MetadataEntities"; export class ImportMetadataUseCase implements UseCase { diff --git a/src/domain/metadata/usecases/MetadataSyncUseCase.ts b/src/domain/metadata/usecases/MetadataSyncUseCase.ts index 60d6b370b..7bd9f330f 100644 --- a/src/domain/metadata/usecases/MetadataSyncUseCase.ts +++ b/src/domain/metadata/usecases/MetadataSyncUseCase.ts @@ -7,7 +7,7 @@ import { debug } from "../../../utils/debug"; import { Ref } from "../../common/entities/Ref"; import { Instance } from "../../instance/entities/Instance"; import { MappingMapper } from "../../mapping/helpers/MappingMapper"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { GenericSyncUseCase } from "../../synchronization/usecases/GenericSyncUseCase"; import { MetadataEntities, MetadataPackage, Document } from "../entities/MetadataEntities"; import { buildNestedRules, cleanObject, cleanReferences, getAllReferences } from "../utils"; diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index b28d96596..2caace6a2 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -5,7 +5,7 @@ import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { AppNotification } from "../entities/Notification"; import { PullRequestStatus, diff --git a/src/domain/packages/usecases/ImportPackageUseCase.ts b/src/domain/packages/usecases/ImportPackageUseCase.ts index d9b06e0c1..71d45d7bf 100644 --- a/src/domain/packages/usecases/ImportPackageUseCase.ts +++ b/src/domain/packages/usecases/ImportPackageUseCase.ts @@ -5,7 +5,7 @@ import { DataSource } from "../../instance/entities/DataSource"; import { Instance } from "../../instance/entities/Instance"; import { MetadataMappingDictionary } from "../../mapping/entities/MetadataMapping"; import { MappingMapper } from "../../mapping/helpers/MappingMapper"; -import { SynchronizationResult } from "../../synchronization/entities/SynchronizationResult"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; import { Package } from "../entities/Package"; export class ImportPackageUseCase implements UseCase { diff --git a/src/domain/reports/entities/SynchronizationReport.ts b/src/domain/reports/entities/SynchronizationReport.ts new file mode 100644 index 000000000..6eef307f8 --- /dev/null +++ b/src/domain/reports/entities/SynchronizationReport.ts @@ -0,0 +1,120 @@ +import { generateUid } from "d2/uid"; +import _ from "lodash"; +import { PartialBy } from "../../../types/utils"; +import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; +import { SynchronizationResult } from "./SynchronizationResult"; + +export class SynchronizationReport implements SynchronizationReportData { + // TODO: Review functional + private results: SynchronizationResult[] | null; + public readonly id: string; + public readonly date?: Date | undefined; + public readonly user: string; + // TODO: Review functional + public status: SynchronizationReportStatus; + // TODO: Review functional + public types: string[]; + public readonly syncRule?: string | undefined; + public readonly packageImport?: boolean | undefined; + public readonly deletedSyncRuleLabel?: string | undefined; + public readonly type: SynchronizationType; + public readonly dataStats?: AggregatedDataStats[] | EventsDataStats[] | undefined; + + private constructor(syncReport: SynchronizationReportData) { + this.results = null; + this.id = syncReport.id; + this.date = syncReport.date; + this.user = syncReport.user; + this.status = syncReport.status; + this.types = syncReport.types; + this.syncRule = syncReport.syncRule; + this.deletedSyncRuleLabel = syncReport.deletedSyncRuleLabel; + this.type = syncReport.type; + this.dataStats = syncReport.dataStats; + this.packageImport = syncReport.packageImport; + } + + public static create( + type: SynchronizationType = "metadata", + user = "", + packageImport?: boolean + ): SynchronizationReport { + return new SynchronizationReport({ + id: generateUid(), + user, + status: "READY" as SynchronizationReportStatus, + types: [], + type, + packageImport, + }); + } + + public static build( + syncReport?: PartialBy + ): SynchronizationReport { + return syncReport + ? new SynchronizationReport({ id: generateUid(), ...syncReport }) + : this.create(); + } + + public setStatus(status: SynchronizationReportStatus): void { + this.status = status; + } + + public setTypes(types: string[]): void { + this.types = types; + } + + public addSyncResult(...result: SynchronizationResult[]): void { + this.results = _.unionBy( + [...result], + this.results, + ({ instance, type, originPackage }) => `${instance.id}-${type}-${originPackage?.id}` + ); + } + + public hasErrors(): boolean { + return _.some(this.results, result => ["ERROR", "NETWORK ERROR"].includes(result.status)); + } + + public toObject(): SynchronizationReportData { + return { + id: this.id, + date: this.date, + user: this.user, + status: this.status, + types: this.types, + syncRule: this.syncRule, + packageImport: this.packageImport, + deletedSyncRuleLabel: this.deletedSyncRuleLabel, + type: this.type, + dataStats: this.dataStats, + }; + } +} + +export interface SynchronizationReportData { + id: string; + date?: Date; + user: string; + status: SynchronizationReportStatus; + types: string[]; + syncRule?: string; + packageImport?: boolean; + deletedSyncRuleLabel?: string; + type: SynchronizationType; + dataStats?: AggregatedDataStats[] | EventsDataStats[]; +} + +export type SynchronizationReportStatus = "READY" | "RUNNING" | "FAILURE" | "DONE"; + +export interface AggregatedDataStats { + dataElement: string; + count: number; +} + +export interface EventsDataStats { + program: string; + count: number; + orgUnits: string[]; +} diff --git a/src/domain/synchronization/entities/SynchronizationResult.ts b/src/domain/reports/entities/SynchronizationResult.ts similarity index 90% rename from src/domain/synchronization/entities/SynchronizationResult.ts rename to src/domain/reports/entities/SynchronizationResult.ts index 7b163b918..1bb612e14 100644 --- a/src/domain/synchronization/entities/SynchronizationResult.ts +++ b/src/domain/reports/entities/SynchronizationResult.ts @@ -1,7 +1,7 @@ import { NamedRef } from "../../common/entities/Ref"; import { PublicInstance } from "../../instance/entities/Instance"; import { Store } from "../../stores/entities/Store"; -import { SynchronizationType } from "./SynchronizationType"; +import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; export type SynchronizationStatus = "PENDING" | "SUCCESS" | "WARNING" | "ERROR" | "NETWORK ERROR"; diff --git a/src/domain/reports/repositories/ReportsRepository.ts b/src/domain/reports/repositories/ReportsRepository.ts new file mode 100644 index 000000000..34366bf01 --- /dev/null +++ b/src/domain/reports/repositories/ReportsRepository.ts @@ -0,0 +1,13 @@ +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationReport } from "../entities/SynchronizationReport"; + +export interface ReportsRepositoryConstructor { + new (instance: Instance): ReportsRepository; +} + +export interface ReportsRepository { + getById(id: string): Promise; + list(): Promise; + save(report: SynchronizationReport): Promise; + delete(id: string): Promise; +} diff --git a/src/domain/reports/usecases/SaveSyncReportUseCase.ts b/src/domain/reports/usecases/SaveSyncReportUseCase.ts new file mode 100644 index 000000000..1ab05a192 --- /dev/null +++ b/src/domain/reports/usecases/SaveSyncReportUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationReport } from "../entities/SynchronizationReport"; + +export class SaveSyncReportUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(report: SynchronizationReport): Promise { + await this.repositoryFactory.reportsRepository(this.localInstance).save(report); + } +} diff --git a/src/domain/synchronization/entities/SynchronizationReport.ts b/src/domain/synchronization/entities/SynchronizationReport.ts deleted file mode 100644 index 39812bd80..000000000 --- a/src/domain/synchronization/entities/SynchronizationReport.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { SynchronizationType } from "./SynchronizationType"; - -export interface SynchronizationReport { - id: string; - date?: Date; - user: string; - status: SynchronizationReportStatus; - types: string[]; - syncRule?: string; - packageImport?: boolean; - deletedSyncRuleLabel?: string; - type: SynchronizationType; - dataStats?: AggregatedDataStats[] | EventsDataStats[]; -} - -export type SynchronizationReportStatus = "READY" | "RUNNING" | "FAILURE" | "DONE"; - -export interface AggregatedDataStats { - dataElement: string; - count: number; -} - -export interface EventsDataStats { - program: string; - count: number; - orgUnits: string[]; -} diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 2c8ada76c..89146e2cf 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -2,7 +2,6 @@ import { D2Api } from "d2-api/2.30"; import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import i18n from "../../../locales"; -import SyncReport from "../../../models/syncReport"; import SyncRule from "../../../models/syncRule"; import { SynchronizationBuilder } from "../../../types/synchronization"; import { cache } from "../../../utils/cache"; @@ -23,9 +22,13 @@ import { MetadataSyncUseCase } from "../../metadata/usecases/MetadataSyncUseCase import { AggregatedDataStats, EventsDataStats, + SynchronizationReport, SynchronizationReportStatus, -} from "../entities/SynchronizationReport"; -import { SynchronizationResult, SynchronizationStatus } from "../entities/SynchronizationResult"; +} from "../../reports/entities/SynchronizationReport"; +import { + SynchronizationResult, + SynchronizationStatus, +} from "../../reports/entities/SynchronizationResult"; import { SynchronizationType } from "../entities/SynchronizationType"; export type SyncronizationClass = @@ -156,7 +159,7 @@ export abstract class GenericSyncUseCase { .get({ fields: { userCredentials: { username: true } } }) .getData(); - return SyncReport.build({ + return SynchronizationReport.build({ user: currentUser.userCredentials.username ?? "Unknown", types: _.keys(metadataPackage), status: "RUNNING" as SynchronizationReportStatus, diff --git a/src/migrations/tasks/03.sync-reports.ts b/src/migrations/tasks/03.sync-reports.ts index 418b04fc5..2b56575d6 100644 --- a/src/migrations/tasks/03.sync-reports.ts +++ b/src/migrations/tasks/03.sync-reports.ts @@ -1,4 +1,4 @@ -import { SynchronizationReport } from "../../domain/synchronization/entities/SynchronizationReport"; +import { SynchronizationReportData } from "../../domain/reports/entities/SynchronizationReport"; import { getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; import { Debug } from "../types"; @@ -88,7 +88,7 @@ export default async function migrate(api: D2Api, debug: Debug): Promise { .filter(key => key.startsWith("notifications-")) .map(key => key.replace("notifications-", "")); - const notifications = await getDataStore(api, "notifications", []); + const notifications = await getDataStore(api, "notifications", []); for (const notification of notificationKeys) { const { type = "metadata" } = notifications.find(({ id }) => id === notification) ?? {}; diff --git a/src/migrations/tasks/04.history-notifications.ts b/src/migrations/tasks/04.history-notifications.ts index 14f95e4bd..2d7535944 100644 --- a/src/migrations/tasks/04.history-notifications.ts +++ b/src/migrations/tasks/04.history-notifications.ts @@ -1,4 +1,4 @@ -import { SynchronizationReport } from "../../domain/synchronization/entities/SynchronizationReport"; +import { SynchronizationReportData } from "../../domain/reports/entities/SynchronizationReport"; import { deleteDataStore, getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; import { promiseMap } from "../../utils/common"; @@ -9,7 +9,7 @@ export default async function migrate(api: D2Api): Promise { const notificationKeys = dataStoreKeys.filter(key => key.startsWith("notifications")); await promiseMap(notificationKeys, async key => { - const contents = await getDataStore(api, key, []); + const contents = await getDataStore(api, key, []); const newKey = key.replace("notifications", "history"); await saveDataStore(api, newKey, contents); await deleteDataStore(api, key); diff --git a/src/models/syncReport.md b/src/models/syncReport.md new file mode 100644 index 000000000..9955fc4db --- /dev/null +++ b/src/models/syncReport.md @@ -0,0 +1,57 @@ +//const dataStoreKey = Namespace.HISTORY; + +/** + public static async get(api: D2Api, id: string): Promise { + const data = await getDataById(api, dataStoreKey, id); + return data ? this.build(data) : null; + } + + public static async list( + api: D2Api, + filters: SyncReportTableFilters, + state?: TableInitialState, + paging = true + ): Promise<{ rows: SynchronizationReport[]; pager: Partial }> { + const { statusFilter, syncRuleFilter, type } = filters; + const { pagination, sorting } = state || {}; + const { page = 1, pageSize = 25 } = pagination || {}; + + const data = await getPaginatedData(api, dataStoreKey, filters, { + paging: false, + sorting: [sorting?.field ?? "id", sorting?.order ?? "asc"], + }); + + const filteredObjects = _(data.objects) + .filter(e => (statusFilter ? e.status === statusFilter : true)) + .filter(e => (syncRuleFilter ? e.syncRule === syncRuleFilter : true)) + .filter(({ type: elementType = "metadata" }) => elementType === type) + .value(); + + const total = filteredObjects.length; + const firstItem = paging ? (page - 1) * pageSize : 0; + const lastItem = paging ? firstItem + pageSize : total; + const rows = _.slice(filteredObjects, firstItem, lastItem); + + return { rows, pager: { ...pagination, page, pageSize, total } }; + } + + public async save(api: D2Api): Promise { + const exists = !!this.syncReport.id; + const element = exists ? this.syncReport : { ...this.syncReport, id: generateUid() }; + + if (exists) await this.remove(api); + await saveDataStore(api, `${dataStoreKey}-${element.id}`, this.results); + await saveData(api, dataStoreKey, element); + } + + public async remove(api: D2Api): Promise { + await deleteDataStore(api, `${dataStoreKey}-${this.syncReport.id}`); + await deleteData(api, dataStoreKey, this.syncReport); + } + + public async loadSyncResults(api: D2Api): Promise { + const { id } = this.syncReport; + return id ? getDataStore(api, `${dataStoreKey}-${id}`, []) : []; + } + +**/ \ No newline at end of file diff --git a/src/models/syncReport.ts b/src/models/syncReport.ts deleted file mode 100644 index 07e4283ff..000000000 --- a/src/models/syncReport.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { TableInitialState, TablePagination } from "d2-ui-components"; -import { generateUid } from "d2/uid"; -import _ from "lodash"; -import { Namespace } from "../data/storage/Namespaces"; -import { - SynchronizationReport, - SynchronizationReportStatus, -} from "../domain/synchronization/entities/SynchronizationReport"; -import { SynchronizationResult } from "../domain/synchronization/entities/SynchronizationResult"; -import { SynchronizationType } from "../domain/synchronization/entities/SynchronizationType"; -import { D2Api } from "../types/d2-api"; -import { SyncReportTableFilters } from "../types/d2-ui-components"; -import { PartialBy } from "../types/utils"; -import { - deleteData, - deleteDataStore, - getDataById, - getDataStore, - getPaginatedData, - saveData, - saveDataStore, -} from "./dataStore"; - -const dataStoreKey = Namespace.HISTORY; - -export default class SyncReport { - private results: SynchronizationResult[] | null; - public readonly syncReport: SynchronizationReport; - - constructor(syncReport: PartialBy) { - this.results = null; - this.syncReport = { - id: generateUid(), - date: new Date(), - ..._.pick(syncReport, [ - "id", - "date", - "user", - "status", - "types", - "syncRule", - "deletedSyncRuleLabel", - "type", - "dataStats", - "packageImport", - ]), - }; - } - - public static create( - type: SynchronizationType = "metadata", - user = "", - packageImport?: boolean - ): SyncReport { - return new SyncReport({ - user, - status: "READY" as SynchronizationReportStatus, - types: [], - type, - packageImport, - }); - } - - public static build(syncReport?: PartialBy): SyncReport { - return syncReport ? new SyncReport(syncReport) : this.create(); - } - - public static async get(api: D2Api, id: string): Promise { - const data = await getDataById(api, dataStoreKey, id); - return data ? this.build(data) : null; - } - - public static async list( - api: D2Api, - filters: SyncReportTableFilters, - state?: TableInitialState, - paging = true - ): Promise<{ rows: SynchronizationReport[]; pager: Partial }> { - const { statusFilter, syncRuleFilter, type } = filters; - const { pagination, sorting } = state || {}; - const { page = 1, pageSize = 25 } = pagination || {}; - - const data = await getPaginatedData(api, dataStoreKey, filters, { - paging: false, - sorting: [sorting?.field ?? "id", sorting?.order ?? "asc"], - }); - - const filteredObjects = _(data.objects) - .filter(e => (statusFilter ? e.status === statusFilter : true)) - .filter(e => (syncRuleFilter ? e.syncRule === syncRuleFilter : true)) - .filter(({ type: elementType = "metadata" }) => elementType === type) - .value(); - - const total = filteredObjects.length; - const firstItem = paging ? (page - 1) * pageSize : 0; - const lastItem = paging ? firstItem + pageSize : total; - const rows = _.slice(filteredObjects, firstItem, lastItem); - - return { rows, pager: { ...pagination, page, pageSize, total } }; - } - - public async save(api: D2Api): Promise { - const exists = !!this.syncReport.id; - const element = exists ? this.syncReport : { ...this.syncReport, id: generateUid() }; - - if (exists) await this.remove(api); - await saveDataStore(api, `${dataStoreKey}-${element.id}`, this.results); - await saveData(api, dataStoreKey, element); - } - - public async remove(api: D2Api): Promise { - await deleteDataStore(api, `${dataStoreKey}-${this.syncReport.id}`); - await deleteData(api, dataStoreKey, this.syncReport); - } - - public setStatus(status: SynchronizationReportStatus): void { - this.syncReport.status = status; - } - - public setTypes(types: string[]): void { - this.syncReport.types = types; - } - - public addSyncResult(...result: SynchronizationResult[]): void { - this.results = _.unionBy( - [...result], - this.results, - ({ instance, type, originPackage }) => `${instance.id}-${type}-${originPackage?.id}` - ); - } - - public async loadSyncResults(api: D2Api): Promise { - const { id } = this.syncReport; - return id ? getDataStore(api, `${dataStoreKey}-${id}`, []) : []; - } - - public hasErrors(): boolean { - return _.some(this.results, result => - _(["ERROR", "NETWORK ERROR"]).includes(result.status) - ); - } - - public get id(): string | undefined { - return this.syncReport.id; - } -} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 72aa0410d..a3e36fedd 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -61,6 +61,7 @@ import { ImportPackageUseCase } from "../domain/packages/usecases/ImportPackageU import { ListPackagesUseCase } from "../domain/packages/usecases/ListPackagesUseCase"; import { ListStorePackagesUseCase } from "../domain/packages/usecases/ListStorePackagesUseCase"; import { PublishStorePackageUseCase } from "../domain/packages/usecases/PublishStorePackageUseCase"; +import { SaveSyncReportUseCase } from "../domain/reports/usecases/SaveSyncReportUseCase"; import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; import { GetStoreUseCase } from "../domain/stores/usecases/GetStoreUseCase"; @@ -299,6 +300,13 @@ export class CompositionRoot { buildMapping: new BuildMappingUseCase(this.repositoryFactory, this.localInstance), }); } + + @cache() + public get reports() { + return getExecute({ + save: new SaveSyncReportUseCase(this.repositoryFactory, this.localInstance), + }); + } } function getExecute, Key extends keyof UseCases>( diff --git a/src/presentation/react/components/module-list-table/ModuleListTable.tsx b/src/presentation/react/components/module-list-table/ModuleListTable.tsx index 263219885..bbac29127 100644 --- a/src/presentation/react/components/module-list-table/ModuleListTable.tsx +++ b/src/presentation/react/components/module-list-table/ModuleListTable.tsx @@ -22,14 +22,14 @@ import { Package } from "../../../../domain/packages/entities/Package"; import i18n from "../../../../locales"; import { promiseMap } from "../../../../utils/common"; import { getUserInfo, isGlobalAdmin, UserInfo } from "../../../../utils/permissions"; +import { ModulePackageListPageProps } from "../../../webapp/pages/module-package-list/ModulePackageListPage"; +import { useAppContext } from "../../contexts/AppContext"; import Dropdown from "../dropdown/Dropdown"; import { PullRequestCreation, PullRequestCreationDialog, } from "../pull-request-creation-dialog/PullRequestCreationDialog"; import { SharingDialog } from "../sharing-dialog/SharingDialog"; -import { ModulePackageListPageProps } from "../../../webapp/pages/module-package-list/ModulePackageListPage"; -import { useAppContext } from "../../contexts/AppContext"; import { NewPackageDialog } from "./NewPackageDialog"; import { getValidationsByVersionFeedback } from "./utils"; @@ -168,7 +168,7 @@ export const ModulesListTable: React.FC = ({ const synchronize = async () => { for await (const { message, syncReport, done } of sync.execute()) { if (message) loading.show(true, message); - if (syncReport) await syncReport.save(api); + if (syncReport) await compositionRoot.reports.save(syncReport); if (done) { openSyncSummary(syncReport); return; @@ -219,7 +219,7 @@ export const ModulesListTable: React.FC = ({ loading.reset(); } }, - [compositionRoot, openSyncSummary, remoteInstance, loading, rows, snackbar, api] + [compositionRoot, openSyncSummary, remoteInstance, loading, rows, snackbar] ); const replicateModule = useCallback( diff --git a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx b/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx index 9b2ee092c..2ad225388 100644 --- a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx +++ b/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx @@ -1,18 +1,18 @@ import { PaginationOptions } from "d2-ui-components"; import React, { ReactNode, useCallback, useMemo, useState } from "react"; import { Instance } from "../../../../domain/instance/entities/Instance"; +import { Store } from "../../../../domain/stores/entities/Store"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { ModulesListTable } from "../module-list-table/ModuleListTable"; -import { PackagesListTable } from "../package-list-table/PackageListTable"; import Dropdown from "../dropdown/Dropdown"; import { InstanceSelectionConfig, InstanceSelectionDropdown, InstanceSelectionOption, } from "../instance-selection-dropdown/InstanceSelectionDropdown"; +import { ModulesListTable } from "../module-list-table/ModuleListTable"; +import { PackagesListTable } from "../package-list-table/PackageListTable"; import { useViewSelector, ViewSelectorConfig } from "./useViewSelector"; -import { Store } from "../../../../domain/stores/entities/Store"; export interface ModulePackageListTableProps { onCreate?(): void; @@ -21,7 +21,7 @@ export interface ModulePackageListTableProps { presentation: PresentationOption; showSelector: ViewSelectorConfig; showInstances: InstanceSelectionConfig; - openSyncSummary?: (syncReport: SyncReport) => void; + openSyncSummary?: (syncReport: SynchronizationReport) => void; onInstanceChange?: (instance?: Instance | Store) => void; actionButtonLabel?: ReactNode; } diff --git a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx index f4c08047c..605b61281 100644 --- a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx +++ b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx @@ -13,8 +13,8 @@ import { } from "../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; import { Package } from "../../../../domain/packages/entities/Package"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import { useAppContext } from "../../contexts/AppContext"; import { PackageImportWizard } from "../package-import-wizard/PackageImportWizard"; @@ -23,7 +23,7 @@ interface PackageImportDialogProps { instance: PackageSource; selectedPackagesId?: string[]; onClose: () => void; - openSyncSummary?: (result: SyncReport) => void; + openSyncSummary?: (result: SynchronizationReport) => void; disablePackageSelection?: boolean; } @@ -100,7 +100,7 @@ const PackageImportDialog: React.FC = ({ .get({ fields: { id: true, userCredentials: { username: true } } }) .getData(); - const report = SyncReport.create( + const report = SynchronizationReport.create( "metadata", currentUser.userCredentials.username ?? "Unknown", true @@ -152,7 +152,7 @@ const PackageImportDialog: React.FC = ({ ); report.setTypes( - _.uniq([...report.syncReport.types, ..._.keys(originPackage.contents)]) + _.uniq([...report.types, ..._.keys(originPackage.contents)]) ); report.setStatus( @@ -188,7 +188,7 @@ const PackageImportDialog: React.FC = ({ loading.show(true, i18n.t("Saving imported packages")); - await report.save(api); + await compositionRoot.reports.save(report); await saveImportedPackages( importedPackages, diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index cf21ce60c..ec7115c56 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -27,8 +27,8 @@ import { } from "../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; import { ListPackage, Package } from "../../../../domain/packages/entities/Package"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; import { ModulePackageListPageProps } from "../../../webapp/pages/module-package-list/ModulePackageListPage"; import { useAppContext } from "../../contexts/AppContext"; @@ -364,7 +364,7 @@ export const PackagesListTable: React.FC = ({ originDataSource ); - const report = SyncReport.create( + const report = SynchronizationReport.create( "metadata", currentUser.userCredentials.username ?? "Unknown", true @@ -383,7 +383,8 @@ export const PackagesListTable: React.FC = ({ originPackage: originPackage.toRef(), origin: remoteInstance?.toPublicObject(), }); - await report.save(api); + + await compositionRoot.reports.save(report); if (result.status === "SUCCESS") { const author = { @@ -717,7 +718,7 @@ export const PackagesListTable: React.FC = ({ return groupPackagesByModuleAndVersion(packageItems); }, [moduleFilter, rows, dhis2VersionFilter, installStatusFilter]); - const handleOpenSyncSummaryFromDialog = (syncReport: SyncReport) => { + const handleOpenSyncSummaryFromDialog = (syncReport: SynchronizationReport) => { setOpenImportPackageDialog(false); setToImportWizard([]); openSyncSummary(syncReport); diff --git a/src/presentation/react/components/packages-diff-dialog/utils.tsx b/src/presentation/react/components/packages-diff-dialog/utils.tsx index 55ea17154..38ad7264c 100644 --- a/src/presentation/react/components/packages-diff-dialog/utils.tsx +++ b/src/presentation/react/components/packages-diff-dialog/utils.tsx @@ -6,8 +6,8 @@ import { FieldUpdate, MetadataPackageDiff, } from "../../../../domain/packages/entities/MetadataPackageDiff"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import { useAppContext } from "../../contexts/AppContext"; import { PackageToDiff } from "./PackagesDiffDialog"; @@ -45,10 +45,10 @@ export function usePackageImporter( metadataDiff: MetadataPackageDiff | undefined, onClose: () => void ) { - const { compositionRoot, api } = useAppContext(); + const { compositionRoot } = useAppContext(); const loading = useLoading(); const snackbar = useSnackbar(); - const [syncReport, setSyncReport] = useState(); + const [syncReport, setSyncReport] = useState(); const closeSyncReport = useCallback(() => { setSyncReport(undefined); @@ -61,12 +61,13 @@ export function usePackageImporter( loading.show(true, i18n.t("Importing package {{name}}", { name: packageName })); const result = await compositionRoot.metadata.import(metadataDiff.mergeableMetadata); - const report = SyncReport.create("metadata"); + const report = SynchronizationReport.create("metadata"); report.setStatus( result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" : "DONE" ); report.addSyncResult({ ...result, origin: instance?.toPublicObject() }); - await report.save(api); + + await compositionRoot.reports.save(report); setSyncReport(report); } @@ -74,7 +75,7 @@ export function usePackageImporter( performImport() .catch(err => snackbar.error(err.message)) .finally(() => loading.reset()); - }, [packageName, metadataDiff, compositionRoot, loading, snackbar, api, instance]); + }, [packageName, metadataDiff, compositionRoot, loading, snackbar, instance]); return { importPackage, syncReport, closeSyncReport }; } diff --git a/src/presentation/react/components/sync-summary/SyncSummary.tsx b/src/presentation/react/components/sync-summary/SyncSummary.tsx index aaf9b8cf1..afcd16b2e 100644 --- a/src/presentation/react/components/sync-summary/SyncSummary.tsx +++ b/src/presentation/react/components/sync-summary/SyncSummary.tsx @@ -19,14 +19,14 @@ import React, { useEffect, useState } from "react"; import ReactJson from "react-json-view"; import { PublicInstance } from "../../../../domain/instance/entities/Instance"; import { Store } from "../../../../domain/stores/entities/Store"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import { ErrorMessage, SynchronizationResult, SynchronizationStats, -} from "../../../../domain/synchronization/entities/SynchronizationResult"; +} from "../../../../domain/reports/entities/SynchronizationResult"; import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import { useAppContext } from "../../contexts/AppContext"; const useStyles = makeStyles(theme => ({ @@ -172,7 +172,7 @@ const getTypeName = (reportType: SynchronizationType, syncType: string) => { }; interface SyncSummaryProps { - response: SyncReport; + response: SynchronizationReport; onClose: () => void; } @@ -192,7 +192,9 @@ const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { const [results, setResults] = useState([]); useEffect(() => { - response.loadSyncResults(api).then(setResults); + // TODO: Add use-case + //response.loadSyncResults(api).then(setResults); + setResults([]); }, [api, response]); if (results.length === 0) return null; @@ -228,7 +230,7 @@ const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { > }> - {`Type: ${getTypeName(type, response.syncReport.type)}`} + {`Type: ${getTypeName(type, response.type)}`}
{origin && `${i18n.t("Origin")}: ${getOriginName(origin)}`} {origin &&
} @@ -278,7 +280,7 @@ const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { ) )} - {response.syncReport.dataStats && ( + {response.dataStats && ( }> @@ -287,11 +289,7 @@ const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { - {buildDataStatsTable( - response.syncReport.type, - response.syncReport.dataStats, - classes - )} + {buildDataStatsTable(response.type, response.dataStats, classes)} )} diff --git a/src/presentation/webapp/pages/history/HistoryPage.tsx b/src/presentation/webapp/pages/history/HistoryPage.tsx index e4241a5a6..1a8349751 100644 --- a/src/presentation/webapp/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/pages/history/HistoryPage.tsx @@ -16,18 +16,17 @@ import { import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { Link, useHistory, useParams } from "react-router-dom"; -import { SynchronizationReport } from "../../../../domain/synchronization/entities/SynchronizationReport"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import { SynchronizationRule } from "../../../../domain/synchronization/entities/SynchronizationRule"; import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import SyncRule from "../../../../models/syncRule"; import { getValueForCollection } from "../../../../utils/d2-ui-components"; import { isAppConfigurator } from "../../../../utils/permissions"; -import { useAppContext } from "../../../react/contexts/AppContext"; import Dropdown from "../../../react/components/dropdown/Dropdown"; import PageHeader from "../../../react/components/page-header/PageHeader"; import SyncSummary, { formatStatusTag } from "../../../react/components/sync-summary/SyncSummary"; +import { useAppContext } from "../../../react/contexts/AppContext"; const config = { metadata: { @@ -77,7 +76,7 @@ const HistoryPage: React.FC = () => { const { title } = config[type]; const [syncRules, setSyncRules] = useState([]); - const [syncReport, setSyncReport] = useState(null); + const [syncReport, setSyncReport] = useState(null); const [toDelete, setToDelete] = useState([]); const [selection, updateSelection] = useState([]); const [response, updateResponse] = useState<{ @@ -93,21 +92,24 @@ const HistoryPage: React.FC = () => { const updateTable = useCallback( (tableState?: TableState) => { - SyncReport.list( + // TODO: Add use-case + /**SyncReport.list( api, { type, statusFilter, syncRuleFilter }, tableState ?? initialState - ).then(updateResponse); + ).then(updateResponse);**/ + updateResponse({ rows: [], pager: {} }); updateSelection(oldSelection => tableState?.selection ?? oldSelection); }, - [api, statusFilter, syncRuleFilter, type, updateSelection] + [statusFilter, syncRuleFilter, type, updateSelection] ); useEffect(() => { SyncRule.list(api, { type }, { paging: false }).then(({ objects }) => setSyncRules(objects) ); - if (id) SyncReport.get(api, id).then(setSyncReport); + // TODO: Add use-case + //if (id) SyncReport.get(api, id).then(setSyncReport); isAppConfigurator(api).then(setAppConfigurator); }, [api, id, type]); @@ -181,7 +183,7 @@ const HistoryPage: React.FC = () => { if (!id) return; const item = _.find(response.rows, ["id", id]); - if (item) setSyncReport(SyncReport.build(item)); + if (item) setSyncReport(SynchronizationReport.build(item)); }; const actions: TableAction[] = [ @@ -213,12 +215,14 @@ const HistoryPage: React.FC = () => { const notifications = _(toDelete) .map(id => _.find(response.rows, ["id", id])) .compact() - .map(data => new SyncReport(data)) + .map(data => SynchronizationReport.build(data)) .value(); - const results = []; + const results: any[] = []; for (const notification of notifications) { - results.push(await notification.remove(api)); + // TODO: Add use-case + //results.push(await notification.remove(api)); + console.log(notification, results); } loading.reset(); diff --git a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx index 853df789d..1bf4c810b 100644 --- a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx @@ -9,6 +9,8 @@ import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../domain/instance/entities/Instance"; +import { Store } from "../../../../domain/stores/entities/Store"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../locales"; import { D2Model } from "../../../../models/dhis/default"; @@ -21,14 +23,12 @@ import { IndicatorMappedModel, } from "../../../../models/dhis/mapping"; import { DataElementGroupModel, DataElementGroupSetModel } from "../../../../models/dhis/metadata"; -import SyncReport from "../../../../models/syncReport"; import SyncRule from "../../../../models/syncRule"; import { Ref } from "../../../../types/d2-api"; import { MetadataType } from "../../../../utils/d2"; import { isAppConfigurator } from "../../../../utils/permissions"; -import { InstanceSelectionOption } from "../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; -import { useAppContext } from "../../../react/contexts/AppContext"; import DeletedObjectsTable from "../../../react/components/delete-objects-table/DeletedObjectsTable"; +import { InstanceSelectionOption } from "../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; import MetadataTable from "../../../react/components/metadata-table/MetadataTable"; import PageHeader from "../../../react/components/page-header/PageHeader"; import { @@ -38,8 +38,8 @@ import { import SyncDialog from "../../../react/components/sync-dialog/SyncDialog"; import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../react/contexts/AppContext"; import InstancesSelectors from "./InstancesSelectors"; -import { Store } from "../../../../domain/stores/entities/Store"; const config: Record< SynchronizationType, @@ -87,7 +87,7 @@ const ManualSyncPage: React.FC = () => { const [syncRule, updateSyncRule] = useState(SyncRule.createOnDemand(type)); const [appConfigurator, updateAppConfigurator] = useState(false); - const [syncReport, setSyncReport] = useState(null); + const [syncReport, setSyncReport] = useState(null); const [syncDialogOpen, setSyncDialogOpen] = useState(false); const [sourceInstance, setSourceInstance] = useState(); const [destinationInstance, setDestinationInstanceBase] = useState(); @@ -139,7 +139,7 @@ const ManualSyncPage: React.FC = () => { } }; - const finishSynchronization = (importResponse?: SyncReport) => { + const finishSynchronization = (importResponse?: SynchronizationReport) => { setSyncDialogOpen(false); if (importResponse) { @@ -174,7 +174,8 @@ const ManualSyncPage: React.FC = () => { const synchronize = async () => { for await (const { message, syncReport, done } of sync.execute()) { if (message) loading.show(true, message); - if (syncReport) await syncReport.save(api); + // TODO: Use-case + //if (syncReport) await syncReport.save(api); if (done) { finishSynchronization(syncReport); return; diff --git a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx index e52d7711e..1d936d8c9 100644 --- a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx @@ -4,8 +4,8 @@ import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "rea import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { Store } from "../../../../domain/stores/entities/Store"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import { CreatePackageFromFileDialog } from "../../../react/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; import { ModulePackageListTable, @@ -24,14 +24,14 @@ export interface ModulePackageListPageProps { presentation: PresentationOption; externalComponents?: ReactNode; pageSizeOptions?: number[]; - openSyncSummary?: (result: SyncReport) => void; + openSyncSummary?: (result: SynchronizationReport) => void; paginationOptions?: PaginationOptions; actionButtonLabel?: ReactNode; } export const ModulePackageListPage: React.FC = () => { const history = useHistory(); - const [syncReport, setSyncReport] = useState(); + const [syncReport, setSyncReport] = useState(); const [openImportPackageDialog, setOpenImportPackageDialog] = useState(false); const [addPackageDialogOpen, setAddPackageDialogOpen] = useState(false); const [selectedInstance, setSelectedInstance] = useState(); @@ -83,7 +83,7 @@ export const ModulePackageListPage: React.FC = () => { [tableOption] ); - const handleOpenSyncSummaryFromDialog = (syncReport: SyncReport) => { + const handleOpenSyncSummaryFromDialog = (syncReport: SynchronizationReport) => { setOpenImportPackageDialog(false); setSyncReport(syncReport); diff --git a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx index a996bc6e2..ce74d1b7a 100644 --- a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx @@ -23,17 +23,17 @@ import { AppNotification } from "../../../../domain/notifications/entities/Notif import { CancelPullRequestError } from "../../../../domain/notifications/usecases/CancelPullRequestUseCase"; import { ImportPullRequestError } from "../../../../domain/notifications/usecases/ImportPullRequestUseCase"; import { UpdatePullRequestStatusError } from "../../../../domain/notifications/usecases/UpdatePullRequestStatusUseCase"; -import { SynchronizationResult } from "../../../../domain/synchronization/entities/SynchronizationResult"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; +import { SynchronizationResult } from "../../../../domain/reports/entities/SynchronizationResult"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { useAppContext } from "../../../react/contexts/AppContext"; import Dropdown from "../../../react/components/dropdown/Dropdown"; import { NotificationViewerDialog } from "../../../react/components/notification-viewer-dialog/NotificationViewerDialog"; import PageHeader from "../../../react/components/page-header/PageHeader"; import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; +import { useAppContext } from "../../../react/contexts/AppContext"; export const NotificationsListPage: React.FC = () => { - const { api, compositionRoot } = useAppContext(); + const { compositionRoot } = useAppContext(); const history = useHistory(); const snackbar = useSnackbar(); const loading = useLoading(); @@ -45,7 +45,7 @@ export const NotificationsListPage: React.FC = () => { const [statusFilter, setStatusFilter] = useState(""); const [resetKey, setResetKey] = useState(Math.random()); const [detailsNotification, setDetailsNotification] = useState(); - const [syncReport, setSyncReport] = useState(); + const [syncReport, setSyncReport] = useState(); const backHome = useCallback(() => { history.push("/"); @@ -151,14 +151,15 @@ export const NotificationsListPage: React.FC = () => { async (result: Either) => { await result.match({ success: async result => { - const report = SyncReport.create("metadata"); + const report = SynchronizationReport.create("metadata"); report.setStatus( result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" : "DONE" ); report.addSyncResult(result); - await report.save(api); + + await compositionRoot.reports.save(report); setSyncReport(report); }, @@ -192,7 +193,7 @@ export const NotificationsListPage: React.FC = () => { }, }); }, - [snackbar, api] + [snackbar, compositionRoot] ); const validateUpdateStatusAction = useCallback( diff --git a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx index 8c6f5563f..558589149 100644 --- a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx @@ -21,10 +21,10 @@ import { Moment } from "moment"; import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../domain/instance/entities/Instance"; +import { SynchronizationReport } from "../../../../domain/reports/entities/SynchronizationReport"; import { SynchronizationRule } from "../../../../domain/synchronization/entities/SynchronizationRule"; import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; import SyncRule from "../../../../models/syncRule"; import { getValueForCollection } from "../../../../utils/d2-ui-components"; import { getValidationMessages } from "../../../../utils/old-validations"; @@ -36,7 +36,6 @@ import { UserInfo, } from "../../../../utils/permissions"; import { requestJSONDownload } from "../../../../utils/synchronization"; -import { useAppContext } from "../../../react/contexts/AppContext"; import Dropdown from "../../../react/components/dropdown/Dropdown"; import PageHeader from "../../../react/components/page-header/PageHeader"; import { @@ -46,6 +45,7 @@ import { import { SharingDialog } from "../../../react/components/sharing-dialog/SharingDialog"; import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../react/contexts/AppContext"; const config: { [key: string]: { @@ -85,7 +85,7 @@ const SyncRulesPage: React.FC = () => { const [targetInstanceFilter, setTargetInstanceFilter] = useState(""); const [enabledFilter, setEnabledFilter] = useState(""); const [lastExecutedFilter, setLastExecutedFilter] = useState(null); - const [syncReport, setSyncReport] = useState(null); + const [syncReport, setSyncReport] = useState(null); const [sharingSettingsObject, setSharingSettingsObject] = useState(null); const [pullRequestProps, setPullRequestProps] = useState(); const [dialogProps, updateDialog] = useState(null); @@ -205,6 +205,8 @@ const SyncRulesPage: React.FC = () => { const confirmDelete = async () => { loading.show(true, i18n.t("Deleting Sync Rules")); + // TODO: Add use-case + /** const results = []; for (const id of toDelete) { const rule = await SyncRule.get(api, id); @@ -220,11 +222,10 @@ const SyncRulesPage: React.FC = () => { ); for (const syncReportData of syncReports.rows) { - const editedSyncReport = { + const syncReport = SyncReport.build({ ...syncReportData, deletedSyncRuleLabel: deletedRuleLabel, - }; - const syncReport = SyncReport.build(editedSyncReport); + }); const syncResults = await syncReport.loadSyncResults(api); syncReport.addSyncResult(syncResults[0]); @@ -238,7 +239,7 @@ const SyncRulesPage: React.FC = () => { snackbar.success( i18n.t("Successfully deleted {{count}} rules", { count: toDelete.length }) ); - } + }**/ loading.reset(); setToDelete([]); @@ -299,7 +300,7 @@ const SyncRulesPage: React.FC = () => { const synchronize = async () => { for await (const { message, syncReport, done } of sync.execute()) { if (message) loading.show(true, message); - if (syncReport) await syncReport.save(api); + if (syncReport) await compositionRoot.reports.save(syncReport); if (done && syncReport) setSyncReport(syncReport); } }; diff --git a/src/scheduler/scheduler.ts b/src/scheduler/scheduler.ts index d951979f4..48bcbe291 100644 --- a/src/scheduler/scheduler.ts +++ b/src/scheduler/scheduler.ts @@ -25,7 +25,7 @@ export default class Scheduler { const synchronize = async () => { for await (const { message, syncReport, done } of sync.execute()) { if (message) logger.debug(message); - if (syncReport) await syncReport.save(this.api); + if (syncReport) await this.compositionRoot.reports.save(syncReport); if (done && syncReport && syncReport.id) { const reportUrl = this.buildUrl(type, syncReport.id); logger.debug(`Finished. Report available at ${reportUrl}`); diff --git a/src/types/synchronization.ts b/src/types/synchronization.ts index 8460b1c59..ea289021e 100644 --- a/src/types/synchronization.ts +++ b/src/types/synchronization.ts @@ -1,8 +1,8 @@ import { DataSynchronizationParams } from "../domain/aggregated/types"; +import { FilterRule } from "../domain/metadata/entities/FilterRule"; import { MetadataEntities } from "../domain/metadata/entities/MetadataEntities"; -import SyncReport from "../models/syncReport"; +import { SynchronizationReport } from "../domain/reports/entities/SynchronizationReport"; import { MetadataImportParams } from "./d2"; -import { FilterRule } from "../domain/metadata/entities/FilterRule"; //TODO: Review this to move it to domain @@ -52,6 +52,6 @@ export interface NestedRules { export interface SynchronizationState { message?: string; - syncReport?: SyncReport; + syncReport?: SynchronizationReport; done?: boolean; } From e0154f1f951a60a87af83c03407abd9a0fc67256 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 2 Dec 2020 10:39:49 +0100 Subject: [PATCH 038/163] Fix tests --- src/data/transformations/__tests__/integration/helpers.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 8f2681bff..500c9b9e9 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -16,7 +16,7 @@ import { TransformationD2ApiRepository } from "../../../transformations/Transfor import { SynchronizationBuilder } from "./../../../../types/synchronization"; export function buildRepositoryFactory() { - const repositoryFactory: RepositoryFactory = new RepositoryFactory(); + const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); @@ -121,9 +121,11 @@ export async function executeMetadataSync( const useCase = new MetadataSyncUseCase(builder, repositoryFactory, localInstance, ""); - for await (const _sync of useCase.execute()) { - // no-op + let done = false; + for await (const sync of useCase.execute()) { + done = !!sync.done; } + expect(done).toBeTruthy(); expect(local.db.metadata.where({})).toHaveLength(0); From 48fdf4c8ed398a114f6f982ad21cd6f3f19d8d08 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 2 Dec 2020 10:51:51 +0100 Subject: [PATCH 039/163] Add missing call for save use-case --- .../react/components/packages-diff-dialog/utils.tsx | 1 - src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx | 3 +-- .../webapp/pages/notifications-list/NotificationsListPage.tsx | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/presentation/react/components/packages-diff-dialog/utils.tsx b/src/presentation/react/components/packages-diff-dialog/utils.tsx index 38ad7264c..54fb59d98 100644 --- a/src/presentation/react/components/packages-diff-dialog/utils.tsx +++ b/src/presentation/react/components/packages-diff-dialog/utils.tsx @@ -66,7 +66,6 @@ export function usePackageImporter( result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" : "DONE" ); report.addSyncResult({ ...result, origin: instance?.toPublicObject() }); - await compositionRoot.reports.save(report); setSyncReport(report); diff --git a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx index 1bf4c810b..665086410 100644 --- a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx @@ -174,8 +174,7 @@ const ManualSyncPage: React.FC = () => { const synchronize = async () => { for await (const { message, syncReport, done } of sync.execute()) { if (message) loading.show(true, message); - // TODO: Use-case - //if (syncReport) await syncReport.save(api); + if (syncReport) await compositionRoot.reports.save(syncReport); if (done) { finishSynchronization(syncReport); return; diff --git a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx index ce74d1b7a..2f9734d61 100644 --- a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx @@ -158,7 +158,6 @@ export const NotificationsListPage: React.FC = () => { : "DONE" ); report.addSyncResult(result); - await compositionRoot.reports.save(report); setSyncReport(report); From d2dc96a1007a3bf724cd315f84e2078d9d5973de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 2 Dec 2020 12:28:04 +0100 Subject: [PATCH 040/163] Create variant --- scripts/run.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/run.ts b/scripts/run.ts index c11069114..f040255bb 100644 --- a/scripts/run.ts +++ b/scripts/run.ts @@ -22,6 +22,12 @@ const variants = [ title: "Module/Package Generation", file: "metadata-synchronization-module-package-generation", }, + { + type: "app", + name: "msf-aggregate-data-app", + title: "MSF Aggregate Data", + file: "metadata-synchronization-msf-aggregate-data", + }, { type: "widget", name: "modules-list", From c3f2c493135a8560014fde62211a2fbf09afca23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 2 Dec 2020 17:24:25 +0100 Subject: [PATCH 041/163] Modify web app folder structure - create core subfolder - move Root under web app - create msf-aggregate-data subfolder --- i18n/en.pot | 7 +- i18n/es.po | 5 +- i18n/fr.po | 5 +- i18n/pt.po | 5 +- .../components/auth/RouteWithSession.tsx | 9 +- .../module-list-table/ModuleListTable.tsx | 2 +- .../package-list-table/PackageListTable.tsx | 2 +- src/presentation/webapp/Root.tsx | 127 ++++++++++++++++++ src/presentation/webapp/WebApp.tsx | 2 +- .../{ => core}/pages/history/HistoryPage.tsx | 26 ++-- .../webapp/{ => core}/pages/home/HomePage.tsx | 33 +++-- .../pages/instance-creation/FieldValidator.js | 0 .../instance-creation/GeneralInfoForm.tsx | 8 +- .../InstanceCreationPage.tsx | 10 +- .../pages/instance-creation/SaveButton.tsx | 2 +- .../pages/instance-list/InstanceListPage.tsx | 14 +- .../InstanceMappingLandingPage.tsx | 10 +- .../instance-mapping/InstanceMappingPage.tsx | 14 +- .../pages/manual-sync/InstancesSelectors.tsx | 6 +- .../pages/manual-sync/ManualSyncPage.tsx | 47 ++++--- .../ModulePackageListPage.tsx | 20 +-- .../modules-creation/ModuleCreationPage.tsx | 12 +- .../NotificationsListPage.tsx | 26 ++-- .../ResponsiblesListPage.tsx | 20 +-- .../store-creation/StoreCreationPage.tsx | 12 +- .../pages/store-list/StoreListPage.tsx | 10 +- .../SyncRulesCreationPage.tsx | 16 +-- .../sync-rules-list/SyncRulesListPage.tsx | 34 ++--- .../msf-aggregate-data/pages/MSFHomePage.tsx | 5 + src/presentation/webapp/pages/Root.jsx | 109 --------------- 30 files changed, 330 insertions(+), 268 deletions(-) create mode 100644 src/presentation/webapp/Root.tsx rename src/presentation/webapp/{ => core}/pages/history/HistoryPage.tsx (90%) rename src/presentation/webapp/{ => core}/pages/home/HomePage.tsx (92%) rename src/presentation/webapp/{ => core}/pages/instance-creation/FieldValidator.js (100%) rename src/presentation/webapp/{ => core}/pages/instance-creation/GeneralInfoForm.tsx (96%) rename src/presentation/webapp/{ => core}/pages/instance-creation/InstanceCreationPage.tsx (88%) rename src/presentation/webapp/{ => core}/pages/instance-creation/SaveButton.tsx (95%) rename src/presentation/webapp/{ => core}/pages/instance-list/InstanceListPage.tsx (94%) rename src/presentation/webapp/{ => core}/pages/instance-mapping/InstanceMappingLandingPage.tsx (87%) rename src/presentation/webapp/{ => core}/pages/instance-mapping/InstanceMappingPage.tsx (88%) rename src/presentation/webapp/{ => core}/pages/manual-sync/InstancesSelectors.tsx (92%) rename src/presentation/webapp/{ => core}/pages/manual-sync/ManualSyncPage.tsx (86%) rename src/presentation/webapp/{ => core}/pages/module-package-list/ModulePackageListPage.tsx (87%) rename src/presentation/webapp/{ => core}/pages/modules-creation/ModuleCreationPage.tsx (82%) rename src/presentation/webapp/{ => core}/pages/notifications-list/NotificationsListPage.tsx (94%) rename src/presentation/webapp/{ => core}/pages/responsibles-list/ResponsiblesListPage.tsx (73%) rename src/presentation/webapp/{ => core}/pages/store-creation/StoreCreationPage.tsx (95%) rename src/presentation/webapp/{ => core}/pages/store-list/StoreListPage.tsx (95%) rename src/presentation/webapp/{ => core}/pages/sync-rules-creation/SyncRulesCreationPage.tsx (79%) rename src/presentation/webapp/{ => core}/pages/sync-rules-list/SyncRulesListPage.tsx (93%) create mode 100644 src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx delete mode 100644 src/presentation/webapp/pages/Root.jsx diff --git a/i18n/en.pot b/i18n/en.pot index 5ab7f3105..1bd98fdd1 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-12-01T09:53:24.942Z\n" -"PO-Revision-Date: 2020-12-01T09:53:24.942Z\n" +"POT-Creation-Date: 2020-12-02T16:23:09.972Z\n" +"PO-Revision-Date: 2020-12-02T16:23:09.972Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1372,6 +1372,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index d5264700c..366aa1474 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-12-01T09:53:24.942Z\n" +"POT-Creation-Date: 2020-12-02T16:09:04.218Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1379,6 +1379,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 5aa9402f3..312d5cb2c 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-12-01T09:53:24.942Z\n" +"POT-Creation-Date: 2020-12-02T16:09:04.218Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1375,6 +1375,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 5aa9402f3..312d5cb2c 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-12-01T09:53:24.942Z\n" +"POT-Creation-Date: 2020-12-02T16:09:04.218Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1375,6 +1375,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" diff --git a/src/presentation/react/components/auth/RouteWithSession.tsx b/src/presentation/react/components/auth/RouteWithSession.tsx index a41794fe7..b07a7d454 100644 --- a/src/presentation/react/components/auth/RouteWithSession.tsx +++ b/src/presentation/react/components/auth/RouteWithSession.tsx @@ -5,13 +5,18 @@ import WithSession from "./WithSession"; export interface RouteWithSessionProps { render: (props: RouteComponentProps) => React.ReactNode; path?: string | string[]; + exact?: boolean; } -const RouteWithSession: React.FC = ({ path, render }) => { +const RouteWithSession: React.FC = ({ path, render, exact }) => { const key = path?.toString() ?? ""; return ( - {render(props)}} /> + {render(props)}} + /> ); }; diff --git a/src/presentation/react/components/module-list-table/ModuleListTable.tsx b/src/presentation/react/components/module-list-table/ModuleListTable.tsx index 263219885..20d573743 100644 --- a/src/presentation/react/components/module-list-table/ModuleListTable.tsx +++ b/src/presentation/react/components/module-list-table/ModuleListTable.tsx @@ -28,7 +28,7 @@ import { PullRequestCreationDialog, } from "../pull-request-creation-dialog/PullRequestCreationDialog"; import { SharingDialog } from "../sharing-dialog/SharingDialog"; -import { ModulePackageListPageProps } from "../../../webapp/pages/module-package-list/ModulePackageListPage"; +import { ModulePackageListPageProps } from "../../../webapp/core/pages/module-package-list/ModulePackageListPage"; import { useAppContext } from "../../contexts/AppContext"; import { NewPackageDialog } from "./NewPackageDialog"; import { getValidationsByVersionFeedback } from "./utils"; diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx index cf21ce60c..665a02bdc 100644 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/components/package-list-table/PackageListTable.tsx @@ -30,7 +30,7 @@ import { ListPackage, Package } from "../../../../domain/packages/entities/Packa import i18n from "../../../../locales"; import SyncReport from "../../../../models/syncReport"; import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; -import { ModulePackageListPageProps } from "../../../webapp/pages/module-package-list/ModulePackageListPage"; +import { ModulePackageListPageProps } from "../../../webapp/core/pages/module-package-list/ModulePackageListPage"; import { useAppContext } from "../../contexts/AppContext"; import Dropdown from "../dropdown/Dropdown"; import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx new file mode 100644 index 000000000..763789fdd --- /dev/null +++ b/src/presentation/webapp/Root.tsx @@ -0,0 +1,127 @@ +import React from "react"; +import { HashRouter, Switch } from "react-router-dom"; +import RouteWithSession from "../react/components/auth/RouteWithSession"; +import RouteWithSessionAndAuth from "../react/components/auth/RouteWithSessionAndAuth"; +import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; +import HistoryPage from "./core/pages/history/HistoryPage"; +import InstanceListPage from "./core/pages/instance-list/InstanceListPage"; +import InstanceMappingLandingPage from "./core/pages/instance-mapping/InstanceMappingLandingPage"; +import InstanceMappingPage from "./core/pages/instance-mapping/InstanceMappingPage"; +import ManualSyncPage from "./core/pages/manual-sync/ManualSyncPage"; +import ModulePackageListPage from "./core/pages/module-package-list/ModulePackageListPage"; +import ModuleCreationPage from "./core/pages/modules-creation/ModuleCreationPage"; +import NotificationsListPage from "./core/pages/notifications-list/NotificationsListPage"; +import ResponsiblesListPage from "./core/pages/responsibles-list/ResponsiblesListPage"; +import StoreCreationPage from "./core/pages/store-creation/StoreCreationPage"; +import StoreListPage from "./core/pages/store-list/StoreListPage"; +import SyncRulesCreationPage, { + SyncRulesCreationParams, +} from "./core/pages/sync-rules-creation/SyncRulesCreationPage"; +import SyncRulesPage from "./core/pages/sync-rules-list/SyncRulesListPage"; +import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; +import { useAppContext } from "../react/contexts/AppContext"; +import * as permissions from "../../utils/permissions"; +import HomePage from "./core/pages/home/HomePage"; +import { MSFHomePage } from "./msf-aggregate-data/pages/MSFHomePage"; + +export type AppVariant = + | "core-app" + | "data-metadata-app" + | "module-package-app" + | "msf-aggregate-data-app"; + +const Root: React.FC = () => { + const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; + const { api } = useAppContext(); + + return ( + + + } + /> + + } + /> + + } + /> + + } /> + + { + const { type } = props.match.params as { type: SynchronizationType }; + return type !== "deleted" || permissions.shouldShowDeletedObjects(api); + }} + render={() => } + /> + + { + const { id } = props.match.params as SyncRulesCreationParams; + + return permissions.verifyUserHasAccessToSyncRule(api, id); + }} + render={() => } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } /> + + } + /> + + } /> + + } + /> + + ( + + )} + /> + + {appVariant === "msf-aggregate-data-app" && ( + } /> + )} + + + ); +}; + +export default Root; diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index fdb189da6..5a34d5427 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -20,7 +20,7 @@ import { muiTheme } from "../react/themes/dhis2.theme"; import { CompositionRoot } from "../CompositionRoot"; import Migrations from "../react/components/migrations/Migrations"; import Share from "../react/components/share/Share"; -import Root from "./pages/Root"; +import Root from "./Root"; import "./WebApp.css"; const generateClassName = createGenerateClassName({ diff --git a/src/presentation/webapp/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx similarity index 90% rename from src/presentation/webapp/pages/history/HistoryPage.tsx rename to src/presentation/webapp/core/pages/history/HistoryPage.tsx index e4241a5a6..c2e2cf138 100644 --- a/src/presentation/webapp/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -16,18 +16,20 @@ import { import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { Link, useHistory, useParams } from "react-router-dom"; -import { SynchronizationReport } from "../../../../domain/synchronization/entities/SynchronizationReport"; -import { SynchronizationRule } from "../../../../domain/synchronization/entities/SynchronizationRule"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import SyncRule from "../../../../models/syncRule"; -import { getValueForCollection } from "../../../../utils/d2-ui-components"; -import { isAppConfigurator } from "../../../../utils/permissions"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import Dropdown from "../../../react/components/dropdown/Dropdown"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import SyncSummary, { formatStatusTag } from "../../../react/components/sync-summary/SyncSummary"; +import { SynchronizationReport } from "../../../../../domain/synchronization/entities/SynchronizationReport"; +import { SynchronizationRule } from "../../../../../domain/synchronization/entities/SynchronizationRule"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import SyncRule from "../../../../../models/syncRule"; +import { getValueForCollection } from "../../../../../utils/d2-ui-components"; +import { isAppConfigurator } from "../../../../../utils/permissions"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import Dropdown from "../../../../react/components/dropdown/Dropdown"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import SyncSummary, { + formatStatusTag, +} from "../../../../react/components/sync-summary/SyncSummary"; const config = { metadata: { diff --git a/src/presentation/webapp/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx similarity index 92% rename from src/presentation/webapp/pages/home/HomePage.tsx rename to src/presentation/webapp/core/pages/home/HomePage.tsx index 84f896d30..246e6ec65 100644 --- a/src/presentation/webapp/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -2,17 +2,16 @@ import { Badge, Icon } from "@material-ui/core"; import _ from "lodash"; import React, { useEffect, useMemo, useState } from "react"; import { useHistory } from "react-router-dom"; -import i18n from "../../../../locales"; +import i18n from "../../../../../locales"; import { isAppConfigurator, isAppExecutor, shouldShowDeletedObjects, -} from "../../../../utils/permissions"; -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"; +} from "../../../../../utils/permissions"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import { Card, Landing } from "../../../../react/components/landing/Landing"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { AppVariant } from "../../../Root"; const appVariantConfiguration: Record = { "core-app": [ @@ -25,9 +24,21 @@ const appVariantConfiguration: Record = { ], "data-metadata-app": ["aggregated", "events", "metadata", "other", "configuration"], "module-package-app": ["metadata-distribution", "configuration"], + "msf-aggregate-data-app": [ + "aggregated", + "events", + "metadata", + "other", + "metadata-distribution", + "configuration", + ], }; -const LandingPage: React.FC = () => { +interface LandingPageProps { + type: "home" | "dashboard"; +} + +const LandingPage: React.FC = ({ type }) => { const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; const { api, compositionRoot } = useAppContext(); @@ -48,6 +59,10 @@ const LandingPage: React.FC = () => { }); }, [api, compositionRoot]); + const backHome = () => { + history.goBack(); + }; + const allCards: Card[] = useMemo( () => [ { @@ -230,6 +245,8 @@ const LandingPage: React.FC = () => { return ( appVariantConfiguration[appVariant].includes(card.key) )} diff --git a/src/presentation/webapp/pages/instance-creation/FieldValidator.js b/src/presentation/webapp/core/pages/instance-creation/FieldValidator.js similarity index 100% rename from src/presentation/webapp/pages/instance-creation/FieldValidator.js rename to src/presentation/webapp/core/pages/instance-creation/FieldValidator.js diff --git a/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx b/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx similarity index 96% rename from src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx rename to src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx index 70497b800..1215445f3 100644 --- a/src/presentation/webapp/pages/instance-creation/GeneralInfoForm.tsx +++ b/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx @@ -4,10 +4,10 @@ import { useSnackbar } from "d2-ui-components"; import _, { Dictionary } from "lodash"; import React, { useCallback, useState } from "react"; import { useHistory } from "react-router-dom"; -import { ValidationError } from "../../../../domain/common/entities/Validations"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../../react/contexts/AppContext"; +import { ValidationError } from "../../../../../domain/common/entities/Validations"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../../../react/contexts/AppContext"; import SaveButton from "./SaveButton"; export interface GeneralInfoFormProps { diff --git a/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx b/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx similarity index 88% rename from src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx rename to src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx index 93876bf71..8cf472822 100644 --- a/src/presentation/webapp/pages/instance-creation/InstanceCreationPage.tsx +++ b/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx @@ -1,11 +1,11 @@ import { ConfirmationDialog } from "d2-ui-components"; import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; import GeneralInfoForm from "./GeneralInfoForm"; const InstanceCreationPage = () => { diff --git a/src/presentation/webapp/pages/instance-creation/SaveButton.tsx b/src/presentation/webapp/core/pages/instance-creation/SaveButton.tsx similarity index 95% rename from src/presentation/webapp/pages/instance-creation/SaveButton.tsx rename to src/presentation/webapp/core/pages/instance-creation/SaveButton.tsx index 28e25e11f..4cc537a9e 100644 --- a/src/presentation/webapp/pages/instance-creation/SaveButton.tsx +++ b/src/presentation/webapp/core/pages/instance-creation/SaveButton.tsx @@ -1,7 +1,7 @@ import { Button } from "@material-ui/core"; import { makeStyles } from "@material-ui/styles"; import React from "react"; -import i18n from "../../../../locales"; +import i18n from "../../../../../locales"; const useStyles = makeStyles(() => ({ button: { diff --git a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx similarity index 94% rename from src/presentation/webapp/pages/instance-list/InstanceListPage.tsx rename to src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx index 5e3a3e879..4b52e8ec6 100644 --- a/src/presentation/webapp/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx @@ -18,13 +18,13 @@ import { import _ from "lodash"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import { executeAnalytics } from "../../../../utils/analytics"; -import { isAppConfigurator } from "../../../../utils/permissions"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import { executeAnalytics } from "../../../../../utils/analytics"; +import { isAppConfigurator } from "../../../../../utils/permissions"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; const InstanceListPage = () => { const { api, compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/pages/instance-mapping/InstanceMappingLandingPage.tsx b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx similarity index 87% rename from src/presentation/webapp/pages/instance-mapping/InstanceMappingLandingPage.tsx rename to src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx index ae5e576a0..a9c4ddbd5 100644 --- a/src/presentation/webapp/pages/instance-mapping/InstanceMappingLandingPage.tsx +++ b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx @@ -1,11 +1,11 @@ import _ from "lodash"; import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import { Card, Landing } from "../../../react/components/landing/Landing"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import { Card, Landing } from "../../../../react/components/landing/Landing"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; const InstanceMappingLandingPage: React.FC = () => { const { compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/pages/instance-mapping/InstanceMappingPage.tsx b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx similarity index 88% rename from src/presentation/webapp/pages/instance-mapping/InstanceMappingPage.tsx rename to src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx index 7cc2fbd24..e37b9784c 100644 --- a/src/presentation/webapp/pages/instance-mapping/InstanceMappingPage.tsx +++ b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx @@ -1,12 +1,12 @@ import _ from "lodash"; import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; import { MetadataMapping, MetadataMappingDictionary, -} from "../../../../domain/mapping/entities/MetadataMapping"; -import i18n from "../../../../locales"; +} from "../../../../../domain/mapping/entities/MetadataMapping"; +import i18n from "../../../../../locales"; import { AggregatedDataElementModel, EventProgramWithDataElementsModel, @@ -20,10 +20,10 @@ import { GlobalOptionModel, IndicatorMappedModel, OrganisationUnitMappedModel, -} from "../../../../models/dhis/mapping"; -import MappingTable from "../../../react/components/mapping-table/MappingTable"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../react/contexts/AppContext"; +} from "../../../../../models/dhis/mapping"; +import MappingTable from "../../../../react/components/mapping-table/MappingTable"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/contexts/AppContext"; export type MappingType = "aggregated" | "tracker" | "orgUnit"; diff --git a/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx b/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx similarity index 92% rename from src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx rename to src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx index 30eb48346..ccbdae42a 100644 --- a/src/presentation/webapp/pages/manual-sync/InstancesSelectors.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx @@ -2,12 +2,12 @@ import { makeStyles } from "@material-ui/core"; import Typography from "@material-ui/core/Typography"; import ArrowRightIcon from "@material-ui/icons/ArrowRightAlt"; import React, { useCallback } from "react"; -import { Ref } from "../../../../types/d2-api"; -import { Maybe } from "../../../../types/utils"; +import { Ref } from "../../../../../types/d2-api"; +import { Maybe } from "../../../../../types/utils"; import { InstanceSelectionDropdown, InstanceSelectionDropdownProps, -} from "../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; +} from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; interface InstancesSelectorsProps { sourceInstance: Maybe; diff --git a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx similarity index 86% rename from src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx rename to src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 853df789d..47b5b4ff0 100644 --- a/src/presentation/webapp/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -8,38 +8,41 @@ import { import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import { D2Model } from "../../../../models/dhis/default"; -import { metadataModels } from "../../../../models/dhis/factory"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import { D2Model } from "../../../../../models/dhis/default"; +import { metadataModels } from "../../../../../models/dhis/factory"; import { AggregatedDataElementModel, DataSetWithDataElementsModel, EventProgramWithDataElementsModel, EventProgramWithIndicatorsModel, IndicatorMappedModel, -} from "../../../../models/dhis/mapping"; -import { DataElementGroupModel, DataElementGroupSetModel } from "../../../../models/dhis/metadata"; -import SyncReport from "../../../../models/syncReport"; -import SyncRule from "../../../../models/syncRule"; -import { Ref } from "../../../../types/d2-api"; -import { MetadataType } from "../../../../utils/d2"; -import { isAppConfigurator } from "../../../../utils/permissions"; -import { InstanceSelectionOption } from "../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import DeletedObjectsTable from "../../../react/components/delete-objects-table/DeletedObjectsTable"; -import MetadataTable from "../../../react/components/metadata-table/MetadataTable"; -import PageHeader from "../../../react/components/page-header/PageHeader"; +} from "../../../../../models/dhis/mapping"; +import { + DataElementGroupModel, + DataElementGroupSetModel, +} from "../../../../../models/dhis/metadata"; +import SyncReport from "../../../../../models/syncReport"; +import SyncRule from "../../../../../models/syncRule"; +import { Ref } from "../../../../../types/d2-api"; +import { MetadataType } from "../../../../../utils/d2"; +import { isAppConfigurator } from "../../../../../utils/permissions"; +import { InstanceSelectionOption } from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import DeletedObjectsTable from "../../../../react/components/delete-objects-table/DeletedObjectsTable"; +import MetadataTable from "../../../../react/components/metadata-table/MetadataTable"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; import { PullRequestCreation, PullRequestCreationDialog, -} from "../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; -import SyncDialog from "../../../react/components/sync-dialog/SyncDialog"; -import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +} from "../../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import SyncDialog from "../../../../react/components/sync-dialog/SyncDialog"; +import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; import InstancesSelectors from "./InstancesSelectors"; -import { Store } from "../../../../domain/stores/entities/Store"; +import { Store } from "../../../../../domain/stores/entities/Store"; const config: Record< SynchronizationType, diff --git a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx similarity index 87% rename from src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx rename to src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index e52d7711e..8b5afe1fe 100644 --- a/src/presentation/webapp/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -2,20 +2,20 @@ import { Icon } from "@material-ui/core"; import { PaginationOptions } from "d2-ui-components"; 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/stores/entities/Store"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { CreatePackageFromFileDialog } from "../../../react/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { CreatePackageFromFileDialog } from "../../../../react/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; import { ModulePackageListTable, PresentationOption, ViewOption, -} from "../../../react/components/module-package-list-table/ModulePackageListTable"; -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"; +} from "../../../../react/components/module-package-list-table/ModulePackageListTable"; +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; diff --git a/src/presentation/webapp/pages/modules-creation/ModuleCreationPage.tsx b/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx similarity index 82% rename from src/presentation/webapp/pages/modules-creation/ModuleCreationPage.tsx rename to src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx index 954bde8d7..9860d78fc 100644 --- a/src/presentation/webapp/pages/modules-creation/ModuleCreationPage.tsx +++ b/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx @@ -1,12 +1,12 @@ import { ConfirmationDialog } from "d2-ui-components"; import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import { Module } from "../../../../domain/modules/entities/Module"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import { ModuleWizard } from "../../../react/components/module-wizard/ModuleWizard"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { MetadataModule } from "../../../../domain/modules/entities/MetadataModule"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import { ModuleWizard } from "../../../../react/components/module-wizard/ModuleWizard"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; const ModuleCreationPage: React.FC = () => { const { compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx similarity index 94% rename from src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx rename to src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index a996bc6e2..40390efd1 100644 --- a/src/presentation/webapp/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -18,19 +18,19 @@ import { } from "d2-ui-components"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Either } from "../../../../domain/common/entities/Either"; -import { AppNotification } from "../../../../domain/notifications/entities/Notification"; -import { CancelPullRequestError } from "../../../../domain/notifications/usecases/CancelPullRequestUseCase"; -import { ImportPullRequestError } from "../../../../domain/notifications/usecases/ImportPullRequestUseCase"; -import { UpdatePullRequestStatusError } from "../../../../domain/notifications/usecases/UpdatePullRequestStatusUseCase"; -import { SynchronizationResult } from "../../../../domain/synchronization/entities/SynchronizationResult"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import Dropdown from "../../../react/components/dropdown/Dropdown"; -import { NotificationViewerDialog } from "../../../react/components/notification-viewer-dialog/NotificationViewerDialog"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; +import { Either } from "../../../../../domain/common/entities/Either"; +import { AppNotification } from "../../../../../domain/notifications/entities/Notification"; +import { CancelPullRequestError } from "../../../../../domain/notifications/usecases/CancelPullRequestUseCase"; +import { ImportPullRequestError } from "../../../../../domain/notifications/usecases/ImportPullRequestUseCase"; +import { UpdatePullRequestStatusError } from "../../../../../domain/notifications/usecases/UpdatePullRequestStatusUseCase"; +import { SynchronizationResult } from "../../../../../domain/synchronization/entities/SynchronizationResult"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import Dropdown from "../../../../react/components/dropdown/Dropdown"; +import { NotificationViewerDialog } from "../../../../react/components/notification-viewer-dialog/NotificationViewerDialog"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; export const NotificationsListPage: React.FC = () => { const { api, compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx similarity index 73% rename from src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx rename to src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx index e35b4c085..bcc31e3a1 100644 --- a/src/presentation/webapp/pages/responsibles-list/ResponsiblesListPage.tsx +++ b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx @@ -1,18 +1,18 @@ import React, { useCallback, useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { MetadataResponsible } from "../../../../domain/metadata/entities/MetadataResponsible"; -import { Store } from "../../../../domain/stores/entities/Store"; -import i18n from "../../../../locales"; -import { DataSetModel, ProgramModel } from "../../../../models/dhis/metadata"; -import { isAppConfigurator } from "../../../../utils/permissions"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { MetadataResponsible } from "../../../../../domain/metadata/entities/MetadataResponsible"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import { DataSetModel, ProgramModel } from "../../../../../models/dhis/metadata"; +import { isAppConfigurator } from "../../../../../utils/permissions"; import { InstanceSelectionDropdown, InstanceSelectionOption, -} from "../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; -import MetadataTable from "../../../react/components/metadata-table/MetadataTable"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../react/contexts/AppContext"; +} from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; +import MetadataTable from "../../../../react/components/metadata-table/MetadataTable"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/contexts/AppContext"; export const ResponsiblesListPage: React.FC = () => { const { compositionRoot, api } = useAppContext(); diff --git a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx similarity index 95% rename from src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx rename to src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx index 1e127d761..5dae531f4 100644 --- a/src/presentation/webapp/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx @@ -9,12 +9,12 @@ import { import React, { useCallback, useEffect, useMemo, useState } from "react"; import Linkify from "react-linkify"; import { useHistory, useParams } from "react-router-dom"; -import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/stores/entities/Store"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import helpStoreGithub from "../../../assets/img/help-store-github.png"; +import { GitHubError } from "../../../../../domain/packages/entities/Errors"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import helpStoreGithub from "../../../../assets/img/help-store-github.png"; const StoreCreationPage: React.FC = () => { const { compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx similarity index 95% rename from src/presentation/webapp/pages/store-list/StoreListPage.tsx rename to src/presentation/webapp/core/pages/store-list/StoreListPage.tsx index 55c35c29c..fe7386c2f 100644 --- a/src/presentation/webapp/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx @@ -12,11 +12,11 @@ import { } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/stores/entities/Store"; -import i18n from "../../../../locales"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../react/contexts/AppContext"; +import { GitHubError } from "../../../../../domain/packages/entities/Errors"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/contexts/AppContext"; import SettingsInputAntenaIcon from "@material-ui/icons/SettingsInputAntenna"; export const StoreListPage: React.FC = () => { diff --git a/src/presentation/webapp/pages/sync-rules-creation/SyncRulesCreationPage.tsx b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx similarity index 79% rename from src/presentation/webapp/pages/sync-rules-creation/SyncRulesCreationPage.tsx rename to src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx index ad6d092ff..94c1cc53c 100644 --- a/src/presentation/webapp/pages/sync-rules-creation/SyncRulesCreationPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx @@ -1,15 +1,15 @@ import { ConfirmationDialog, useLoading } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import SyncWizard from "../../../react/components/sync-wizard/SyncWizard"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import SyncRule from "../../../../models/syncRule"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; +import SyncWizard from "../../../../react/components/sync-wizard/SyncWizard"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import SyncRule from "../../../../../models/syncRule"; -interface SyncRulesCreationParams { +export interface SyncRulesCreationParams { id: string; action: "edit" | "new"; type: SynchronizationType; diff --git a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx similarity index 93% rename from src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx rename to src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index 8c6f5563f..fdab04e89 100644 --- a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -20,32 +20,32 @@ import _ from "lodash"; import { Moment } from "moment"; import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationRule } from "../../../../domain/synchronization/entities/SynchronizationRule"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import SyncRule from "../../../../models/syncRule"; -import { getValueForCollection } from "../../../../utils/d2-ui-components"; -import { getValidationMessages } from "../../../../utils/old-validations"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { SynchronizationRule } from "../../../../../domain/synchronization/entities/SynchronizationRule"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import SyncRule from "../../../../../models/syncRule"; +import { getValueForCollection } from "../../../../../utils/d2-ui-components"; +import { getValidationMessages } from "../../../../../utils/old-validations"; import { getUserInfo, isAppConfigurator, isAppExecutor, isGlobalAdmin, UserInfo, -} from "../../../../utils/permissions"; -import { requestJSONDownload } from "../../../../utils/synchronization"; -import { useAppContext } from "../../../react/contexts/AppContext"; -import Dropdown from "../../../react/components/dropdown/Dropdown"; -import PageHeader from "../../../react/components/page-header/PageHeader"; +} from "../../../../../utils/permissions"; +import { requestJSONDownload } from "../../../../../utils/synchronization"; +import { useAppContext } from "../../../../react/contexts/AppContext"; +import Dropdown from "../../../../react/components/dropdown/Dropdown"; +import PageHeader from "../../../../react/components/page-header/PageHeader"; import { PullRequestCreation, PullRequestCreationDialog, -} from "../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; -import { SharingDialog } from "../../../react/components/sharing-dialog/SharingDialog"; -import SyncSummary from "../../../react/components/sync-summary/SyncSummary"; -import { TestWrapper } from "../../../react/components/test-wrapper/TestWrapper"; +} from "../../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import { SharingDialog } from "../../../../react/components/sharing-dialog/SharingDialog"; +import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; +import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; const config: { [key: string]: { diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx new file mode 100644 index 000000000..ccda5853b --- /dev/null +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const MSFHomePage: React.FC = () => { + return
MSF!!!
; +}; diff --git a/src/presentation/webapp/pages/Root.jsx b/src/presentation/webapp/pages/Root.jsx deleted file mode 100644 index 1b098b124..000000000 --- a/src/presentation/webapp/pages/Root.jsx +++ /dev/null @@ -1,109 +0,0 @@ -import React from "react"; -import { HashRouter, Switch } from "react-router-dom"; -import * as permissions from "../../../utils/permissions"; -import RouteWithSession from "../../react/components/auth/RouteWithSession"; -import RouteWithSessionAndAuth from "../../react/components/auth/RouteWithSessionAndAuth"; -import { useAppContext } from "../../react/contexts/AppContext"; -import HistoryPage from "./history/HistoryPage"; -import HomePage from "./home/HomePage"; -import InstanceCreationPage from "./instance-creation/InstanceCreationPage"; -import InstanceListPage from "./instance-list/InstanceListPage"; -import InstanceMappingLandingPage from "./instance-mapping/InstanceMappingLandingPage"; -import InstanceMappingPage from "./instance-mapping/InstanceMappingPage"; -import ManualSyncPage from "./manual-sync/ManualSyncPage"; -import ModulePackageListPage from "./module-package-list/ModulePackageListPage"; -import ModuleCreationPage from "./modules-creation/ModuleCreationPage"; -import NotificationsListPage from "./notifications-list/NotificationsListPage"; -import ResponsiblesListPage from "./responsibles-list/ResponsiblesListPage"; -import StoreCreationPage from "./store-creation/StoreCreationPage"; -import StoreListPage from "./store-list/StoreListPage"; -import SyncRulesCreationPage from "./sync-rules-creation/SyncRulesCreationPage"; -import SyncRulesPage from "./sync-rules-list/SyncRulesListPage"; - -function Root() { - const { api } = useAppContext(); - - return ( - - - } - /> - - } - /> - - } - /> - - } - /> - - - props.match.params.type !== "deleted" || - permissions.shouldShowDeletedObjects(api) - } - render={props => } - /> - - - permissions.verifyUserHasAccessToSyncRule(api, props.match.params.id) - } - render={props => } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } /> - - } - /> - - } - /> - - } - /> - - } /> - - - ); -} - -export default Root; From 0f01a1dded27f36e550d2fde4596c10f7416392e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 08:19:51 +0100 Subject: [PATCH 042/163] Create MSF home page design --- i18n/en.pot | 25 +++- i18n/es.po | 23 +++- i18n/fr.po | 23 +++- i18n/pt.po | 23 +++- .../msf-aggregate-data/pages/MSFHomePage.tsx | 110 +++++++++++++++++- 5 files changed, 198 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 1bd98fdd1..1e0b4dbb8 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-12-02T16:23:09.972Z\n" -"PO-Revision-Date: 2020-12-02T16:23:09.972Z\n" +"POT-Creation-Date: 2020-12-03T07:18:49.558Z\n" +"PO-Revision-Date: 2020-12-03T07:18:49.558Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1721,6 +1721,27 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Agreggate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "MSF Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 366aa1474..d57f8783e 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-12-02T16:09:04.218Z\n" +"POT-Creation-Date: 2020-12-03T07:18:49.558Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1728,6 +1728,27 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Agreggate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "MSF Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 312d5cb2c..6887daf7e 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-12-02T16:09:04.218Z\n" +"POT-Creation-Date: 2020-12-03T07:18:49.558Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1724,6 +1724,27 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Agreggate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "MSF Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 312d5cb2c..6887daf7e 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-12-02T16:09:04.218Z\n" +"POT-Creation-Date: 2020-12-03T07:18:49.558Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1724,6 +1724,27 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Agreggate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "MSF Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index ccda5853b..32651d11e 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,5 +1,113 @@ +import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; +import i18n from "d2-ui-components/locales"; import React from "react"; +import { useHistory } from "react-router-dom"; +import PageHeader from "../../../react/components/page-header/PageHeader"; export const MSFHomePage: React.FC = () => { - return
MSF!!!
; + const classes = useStyles(); + const history = useHistory(); + + const handleAggregateData = () => {}; + const handleAdvancedSettings = () => {}; + const handleMSFSettings = () => {}; + const handleGoToDashboard = () => { + history.push("/dashboard"); + }; + const handleGoToHistory = () => { + history.push("/history/events"); + }; + + return ( + + + + + + + + + + + + {i18n.t("Synchronization Progress")} + + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + {"Synchronizing Sync Rule 1 ..."} + + + + + + + + + + + + + + + + + + ); }; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + marginTop: theme.spacing(2), + padding: theme.spacing(2), + }, + runButton: { + margin: "0 auto", + }, + log: { + width: "60%", + margin: theme.spacing(4), + padding: theme.spacing(4), + overflow: "auto", + minHeight: 300, + maxHeight: 300, + }, + actionButton: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(1), + }, +})); From c7220f0cc830cc8a091faacf459672c289cc1b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 09:18:19 +0100 Subject: [PATCH 043/163] Create period selection dialog --- i18n/en.pot | 6 +-- i18n/es.po | 4 +- i18n/fr.po | 4 +- i18n/pt.po | 4 +- .../PeriodSelectionDialog.tsx | 53 +++++++++++++++++++ .../period-selection/PeriodSelection.tsx | 6 ++- .../msf-aggregate-data/pages/MSFHomePage.tsx | 36 +++++++++++-- 7 files changed, 99 insertions(+), 14 deletions(-) create mode 100644 src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 1e0b4dbb8..adb42090c 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-12-03T07:18:49.558Z\n" -"PO-Revision-Date: 2020-12-03T07:18:49.558Z\n" +"POT-Creation-Date: 2020-12-03T08:16:31.442Z\n" +"PO-Revision-Date: 2020-12-03T08:16:31.442Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1724,7 +1724,7 @@ msgstr[1] "" msgid "Aggregate Data For HMIS" msgstr "" -msgid "Agreggate Data" +msgid "Aggregate Data" msgstr "" msgid "Synchronization Progress" diff --git a/i18n/es.po b/i18n/es.po index d57f8783e..b5367d67b 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-12-03T07:18:49.558Z\n" +"POT-Creation-Date: 2020-12-03T08:15:32.383Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1731,7 +1731,7 @@ msgstr[1] "" msgid "Aggregate Data For HMIS" msgstr "" -msgid "Agreggate Data" +msgid "Aggregate Data" msgstr "" msgid "Synchronization Progress" diff --git a/i18n/fr.po b/i18n/fr.po index 6887daf7e..fe39e1395 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-12-03T07:18:49.558Z\n" +"POT-Creation-Date: 2020-12-03T08:15:32.383Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1727,7 +1727,7 @@ msgstr[1] "" msgid "Aggregate Data For HMIS" msgstr "" -msgid "Agreggate Data" +msgid "Aggregate Data" msgstr "" msgid "Synchronization Progress" diff --git a/i18n/pt.po b/i18n/pt.po index 6887daf7e..fe39e1395 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-12-03T07:18:49.558Z\n" +"POT-Creation-Date: 2020-12-03T08:15:32.383Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1727,7 +1727,7 @@ msgstr[1] "" msgid "Aggregate Data For HMIS" msgstr "" -msgid "Agreggate Data" +msgid "Aggregate Data" msgstr "" msgid "Synchronization Progress" diff --git a/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx new file mode 100644 index 000000000..545828fb1 --- /dev/null +++ b/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx @@ -0,0 +1,53 @@ +import { Box, makeStyles, Theme } from "@material-ui/core"; +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useState } from "react"; +import i18n from "../../../../locales"; +import { PeriodFilter } from "../../../webapp/msf-aggregate-data/pages/MSFHomePage"; +import PeriodSelection from "../period-selection/PeriodSelection"; + +export interface PeriodSelectionDialogProps { + title?: string; + period: PeriodFilter; + onClose(): void; + onSave(value: PeriodFilter): void; +} + +export const PeriodSelectionDialog: React.FC = ({ + title, + onClose, + onSave, + period, +}) => { + const classes = useStyles(); + const [periodState, setPeriodState] = useState(period); + + return ( + onSave(periodState)} + cancelText={i18n.t("Cancel")} + saveText={i18n.t("Save")} + > + + + + + ); +}; + +const useStyles = makeStyles((theme: Theme) => ({ + periodContainer: { + margin: "0 auto", + }, + periodContent: { + margin: theme.spacing(2), + }, +})); diff --git a/src/presentation/react/components/period-selection/PeriodSelection.tsx b/src/presentation/react/components/period-selection/PeriodSelection.tsx index ed2ee2fdb..12900bf4c 100644 --- a/src/presentation/react/components/period-selection/PeriodSelection.tsx +++ b/src/presentation/react/components/period-selection/PeriodSelection.tsx @@ -30,6 +30,7 @@ export interface PeriodSelectionProps { value: ObjectWithPeriod[Field] ): void; skipPeriods?: Set; + className?: string; } export type OnChange = Required["onChange"]; @@ -57,6 +58,7 @@ const PeriodSelection: React.FC = props => { onFieldChange = _.noop as OnFieldChange, skipPeriods = new Set(), periodTitle = i18n.t("Period"), + className, } = props; const objectWithPeriod: ObjectWithPeriod = { @@ -105,7 +107,7 @@ const PeriodSelection: React.FC = props => { ); return ( - +
= props => {
)} -
+ ); }; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 32651d11e..1ba5d25b7 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,15 +1,28 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; import i18n from "d2-ui-components/locales"; -import React from "react"; +import React, { useState } from "react"; import { useHistory } from "react-router-dom"; +import { DataSyncPeriod } from "../../../../domain/aggregated/types"; import PageHeader from "../../../react/components/page-header/PageHeader"; +import { PeriodSelectionDialog } from "../../../react/components/period-selection-dialog/PeriodSelectionDialog"; + +export interface PeriodFilter { + period: DataSyncPeriod; + startDate?: Date; + endDate?: Date; +} export const MSFHomePage: React.FC = () => { const classes = useStyles(); const history = useHistory(); + const [showPeriodDialog, setShowPeriodDialog] = useState(false); + const [period, setPeriod] = useState({ period: "ALL" }); + const handleAggregateData = () => {}; - const handleAdvancedSettings = () => {}; + const handleAdvancedSettings = () => { + setShowPeriodDialog(true); + }; const handleMSFSettings = () => {}; const handleGoToDashboard = () => { history.push("/dashboard"); @@ -18,6 +31,15 @@ export const MSFHomePage: React.FC = () => { history.push("/history/events"); }; + const handleCloseAdvancedSettings = () => { + setShowPeriodDialog(false); + }; + + const handleSaveAdvancedSettings = (period: PeriodFilter) => { + setShowPeriodDialog(false); + setPeriod(period); + }; + return ( @@ -30,7 +52,7 @@ export const MSFHomePage: React.FC = () => { color="primary" className={classes.runButton} > - {i18n.t("Agreggate Data")} + {i18n.t("Aggregate Data")} @@ -86,6 +108,14 @@ export const MSFHomePage: React.FC = () => { + + {showPeriodDialog && ( + + )} ); }; From d35cf91d3605fbcbdf2e3b08402699ed4f17fec0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 09:36:33 +0100 Subject: [PATCH 044/163] Hide buttons for non admin users --- i18n/en.pot | 4 +- .../msf-aggregate-data/pages/MSFHomePage.tsx | 42 ++++++++++++------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index adb42090c..0d9b94250 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-12-03T08:16:31.442Z\n" -"PO-Revision-Date: 2020-12-03T08:16:31.442Z\n" +"POT-Creation-Date: 2020-12-03T08:33:07.995Z\n" +"PO-Revision-Date: 2020-12-03T08:33:07.995Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 1ba5d25b7..4bf1668a8 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,10 +1,12 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; import i18n from "d2-ui-components/locales"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { DataSyncPeriod } from "../../../../domain/aggregated/types"; +import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/components/period-selection-dialog/PeriodSelectionDialog"; +import { useAppContext } from "../../../react/contexts/AppContext"; export interface PeriodFilter { period: DataSyncPeriod; @@ -18,6 +20,12 @@ export const MSFHomePage: React.FC = () => { const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [period, setPeriod] = useState({ period: "ALL" }); + const [globalAdmin, setGlobalAdmin] = useState(false); + const { api } = useAppContext(); + + useEffect(() => { + isGlobalAdmin(api).then(setGlobalAdmin); + }, [api]); const handleAggregateData = () => {}; const handleAdvancedSettings = () => { @@ -81,22 +89,26 @@ export const MSFHomePage: React.FC = () => { > {i18n.t("Advanced Settings")} - + {globalAdmin && ( + + )} - + {globalAdmin && ( + + )} + + + + + + + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, + versionRow: { + width: "100%", + display: "flex", + flex: "1 1 auto", + marginBottom: 25, + }, + marginRight: { + marginRight: 10, + }, +}); diff --git a/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx b/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx new file mode 100644 index 000000000..5017b029d --- /dev/null +++ b/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx @@ -0,0 +1,105 @@ +import SyncIcon from "@material-ui/icons/Sync"; +import { + ObjectsTable, + ObjectsTableDetailField, + ReferenceObject, + TableColumn, + TableState, + DatePicker, +} from "d2-ui-components"; +import React, { useEffect, useState } from "react"; +import DeletedObject from "../../../../../models/deletedObjects"; +import SyncRule from "../../../../../models/syncRule"; +import { MetadataType } from "../../../../../utils/d2"; +import moment from "moment"; +import { useAppContext } from "../../contexts/AppContext"; +import i18n from "../../../../../locales"; + +export interface DeletedObjectsTableProps { + openSynchronizationDialog: () => void; + syncRule: SyncRule; + onChange: (syncRule: SyncRule) => void; +} + +const DeletedObjectsTable: React.FC = ({ + openSynchronizationDialog, + syncRule, + onChange, +}) => { + const { api } = useAppContext(); + + const [deletedObjectsRows, setDeletedObjectsRows] = useState([]); + const [search, setSearch] = useState(undefined); + const [dateFilter, setDateFilter] = useState(null); + + const deletedObjectsColumns: TableColumn[] = [ + { name: "id", text: i18n.t("Identifier"), sortable: true }, + { name: "code", text: i18n.t("Code"), sortable: true }, + { name: "klass", text: i18n.t("Metadata type"), sortable: true }, + { name: "deletedAt", text: i18n.t("Deleted date"), sortable: true }, + { name: "deletedBy", text: i18n.t("Deleted by"), sortable: true }, + ]; + + const deletedObjectsDetails: ObjectsTableDetailField[] = [ + { name: "id", text: i18n.t("Identifier") }, + { name: "code", text: i18n.t("Code") }, + { name: "klass", text: i18n.t("Metadata type") }, + { name: "deletedAt", text: i18n.t("Deleted date") }, + { name: "deletedBy", text: i18n.t("Deleted by") }, + ]; + + const deletedObjectsActions = [ + { + name: "details", + text: i18n.t("Details"), + multiple: false, + type: "details", + }, + ]; + + useEffect(() => { + DeletedObject.list( + api, + { + search, + lastUpdatedDate: + dateFilter !== null ? moment(dateFilter).startOf("day") : undefined, + }, + {} + ).then(({ objects }) => setDeletedObjectsRows(objects)); + }, [api, search, dateFilter]); + + const handleTableChange = (tableState: TableState) => { + const { selection } = tableState; + onChange(syncRule.updateMetadataIds(selection.map(({ id }) => id))); + }; + + const filterComponents = ( + + + + ); + + return ( + + rows={deletedObjectsRows} + columns={deletedObjectsColumns} + details={deletedObjectsDetails} + actions={deletedObjectsActions} + forceSelectionColumn={true} + onActionButtonClick={openSynchronizationDialog} + onChange={handleTableChange} + actionButtonLabel={} + onChangeSearch={setSearch} + searchBoxLabel={i18n.t("Search deleted objects")} + filterComponents={filterComponents} + /> + ); +}; + +export default DeletedObjectsTable; diff --git a/src/presentation/react/core/components/dropdown/Dropdown.tsx b/src/presentation/react/core/components/dropdown/Dropdown.tsx new file mode 100644 index 000000000..cc5ea84be --- /dev/null +++ b/src/presentation/react/core/components/dropdown/Dropdown.tsx @@ -0,0 +1,121 @@ +import { FormControl, InputLabel, MenuItem, MuiThemeProvider, Select } from "@material-ui/core"; +import { createMuiTheme } from "@material-ui/core/styles"; +import _ from "lodash"; +import React from "react"; +import i18n from "../../../../../locales"; + +export interface DropdownOption { + id: string; + name: string; +} + +export type DropdownViewOption = "filter" | "inline" | "full-width"; + +interface DropdownProps { + items: DropdownOption[]; + value: string; + label?: string; + onChange?: Function; + onValueChange?(value: string): void; + hideEmpty?: boolean; + emptyLabel?: string; + view?: DropdownViewOption; + disabled?: boolean; +} + +const getTheme = (view: DropdownViewOption) => { + switch (view) { + case "filter": + return createMuiTheme({ + overrides: { + MuiFormLabel: { + root: { + color: "#aaaaaa", + "&$focused": { + color: "#aaaaaa", + }, + top: "-9px !important", + marginLeft: 10, + }, + }, + MuiInput: { + root: { + marginLeft: 10, + }, + formControl: { + minWidth: 250, + marginTop: "8px !important", + }, + input: { + color: "#565656", + }, + }, + }, + }); + case "inline": + return createMuiTheme({ + overrides: { + MuiFormControl: { + root: { + verticalAlign: "middle", + marginBottom: 5, + }, + }, + }, + }); + default: + return {}; + } +}; + +const Dropdown: React.FC = ({ + items, + value, + onChange = _.noop, + onValueChange = _.noop, + label, + hideEmpty = false, + emptyLabel, + view = "filter", + disabled = false, +}) => { + const inlineStyles = { minWidth: 120, paddingLeft: 25, paddingRight: 25 }; + const styles = view === "inline" ? inlineStyles : {}; + + return ( + + + {view !== "inline" && label && {label}} + + + + ); +}; + +export default Dropdown; diff --git a/src/presentation/react/core/components/filter-rules-table/FilterRuleDialog.tsx b/src/presentation/react/core/components/filter-rules-table/FilterRuleDialog.tsx new file mode 100644 index 000000000..666a954f3 --- /dev/null +++ b/src/presentation/react/core/components/filter-rules-table/FilterRuleDialog.tsx @@ -0,0 +1,136 @@ +import { makeStyles } from "@material-ui/core"; +import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useMemo, useState } from "react"; +import { + FilterRule, + FilterRuleField, + FilterWhere, + updateFilterRule, + updateStringMatch, + validateFilterRule, + whereNames, +} from "../../../../../domain/metadata/entities/FilterRule"; +import i18n from "../../../../../locales"; +import { metadataModels } from "../../../../../models/dhis/factory"; +import Dropdown from "../dropdown/Dropdown"; +import PeriodSelection from "../period-selection/PeriodSelection"; +import TextFieldOnBlur from "../text-field-on-blur/TextFieldOnBlur"; +import { Section } from "./Section"; + +export interface NewFilterRuleDialogProps { + action: "new" | "edit"; + onClose(): void; + onSave(filterRule: FilterRule): void; + initialFilterRule: FilterRule; +} + +export const FilterRuleDialog: React.FC = props => { + const { onClose, onSave, action, initialFilterRule } = props; + const classes = useStyles(); + const snackbar = useSnackbar(); + const [filterRule, setFilterRule] = useState(initialFilterRule); + + const metadataTypeItems = useMemo(() => { + return metadataModels.map(model => ({ + id: model.getMetadataType(), + name: model.getModelName(), + })); + }, []); + + function updateField(field: Field) { + return function (value: FilterRule[Field]) { + setFilterRule(filterRule => updateFilterRule(filterRule, field, value)); + }; + } + + const save = useCallback(() => { + const errors = validateFilterRule(filterRule); + if (_.isEmpty(errors)) { + onSave(filterRule); + } else { + snackbar.error(errors.map(error => error.description).join("\n")); + } + }, [filterRule, onSave, snackbar]); + + function updateStringMatchWhere(where: FilterWhere | "") { + const value = { where: where || null, ...(where ? {} : { value: "" }) }; + setFilterRule(filterRule => updateStringMatch(filterRule, value)); + } + + const title = action === "new" ? i18n.t("Create new filter") : i18n.t("Edit filter"); + const saveText = action === "new" ? i18n.t("Create") : i18n.t("Update"); + + return ( + + +
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ + setFilterRule(filterRule => + updateStringMatch(filterRule, { value }) + ) + } + label={i18n.t("String to match (*)")} + value={filterRule.stringMatch?.value || ""} + /> +
+
+
+
+ ); +}; + +const whereItems = _.map(whereNames, (name, key) => ({ id: key, name })); + +const useStyles = makeStyles({ + dropdown: { + marginTop: 20, + }, + textField: { + marginLeft: 10, + }, +}); diff --git a/src/presentation/react/core/components/filter-rules-table/FilterRulesTable.tsx b/src/presentation/react/core/components/filter-rules-table/FilterRulesTable.tsx new file mode 100644 index 000000000..197ffbb17 --- /dev/null +++ b/src/presentation/react/core/components/filter-rules-table/FilterRulesTable.tsx @@ -0,0 +1,163 @@ +import { Button, Icon } from "@material-ui/core"; +import { + ObjectsTable, + PaginationOptions, + TableAction, + TableColumn, + TableSelection, + TableState, +} from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useMemo, useState } from "react"; +import { updateObject as updateObjectInList } from "../../../../../domain/common/entities/Ref"; +import { + FilterRule, + getDateFilterString, + getInitialFilterRule, + getStringMatchString, +} from "../../../../../domain/metadata/entities/FilterRule"; +import i18n from "../../../../../locales"; +import { metadataModels } from "../../../../../models/dhis/factory"; +import { useOpenState } from "../../hooks/useOpenState"; +import { FilterRuleDialog, NewFilterRuleDialogProps } from "./FilterRuleDialog"; + +export interface FilterRulesTableProps { + filterRules: FilterRule[]; + onChange: (filterRules: FilterRule[]) => void; +} + +type Action = { type: "new" | "edit"; filterRule: FilterRule }; + +const FilterRulesTable: React.FC = props => { + const { filterRules, onChange } = props; + const [selection, updateSelection] = useState([]); + const newFilterRuleDialog = useOpenState(); + + const modelNames = useMemo(() => { + return _(metadataModels) + .map(model => [model.getMetadataType(), model.getModelName()] as [string, string]) + .fromPairs() + .value(); + }, []); + + const editRule = useCallback( + (ids: string[]) => { + const filterRule = _.find(filterRules, ({ id }) => id === ids[0]); + if (filterRule) newFilterRuleDialog.open({ type: "edit", filterRule }); + }, + [filterRules, newFilterRuleDialog] + ); + + const deleteRule = useCallback( + async (ids: string[]) => { + const newFilterRules = filterRules.filter(filterRule => !ids.includes(filterRule.id)); + onChange(newFilterRules); + updateSelection([]); + }, + [filterRules, onChange] + ); + + const updateTable = useCallback( + ({ selection }: TableState) => { + updateSelection(selection); + }, + [updateSelection] + ); + + const columns: TableColumn[] = useMemo( + () => [ + { + name: "metadataType", + text: i18n.t("Metadata type"), + getValue: rule => modelNames[rule.metadataType] || "-", + }, + { + name: "created", + text: i18n.t("Created"), + getValue: rule => getDateFilterString(rule.created), + }, + { + name: "lastUpdated", + text: i18n.t("Last updated"), + getValue: rule => getDateFilterString(rule.lastUpdated), + }, + { + name: "stringMatch", + text: i18n.t("Name/code/description"), + getValue: rule => getStringMatchString(rule.stringMatch) || "-", + }, + ], + [modelNames] + ); + + const actions: TableAction[] = useMemo( + () => [ + { + name: "edit", + text: i18n.t("Edit"), + multiple: false, + onClick: editRule, + icon: edit, + }, + { + name: "delete", + text: i18n.t("Delete"), + multiple: true, + onClick: deleteRule, + icon: delete, + }, + ], + [deleteRule, editRule] + ); + + const openNewDialog = useCallback(() => { + const newFilterRule = { type: "new" as const, filterRule: getInitialFilterRule() }; + newFilterRuleDialog.open(newFilterRule); + }, [newFilterRuleDialog]); + + const extraComponents = ( + + ); + + const { close: closeFilterRuleDialog } = newFilterRuleDialog; + const save = useCallback( + filterRule => { + const newFilterRules = updateObjectInList(filterRules, filterRule); + onChange(newFilterRules); + closeFilterRuleDialog(); + }, + [filterRules, onChange, closeFilterRuleDialog] + ); + + return ( + + + rows={filterRules} + columns={columns} + actions={actions} + filterComponents={extraComponents} + selection={selection} + onChange={updateTable} + paginationOptions={paginationOptions} + /> + + {newFilterRuleDialog.value && ( + + )} + + ); +}; + +const paginationOptions: PaginationOptions = { + pageSizeOptions: [10], + pageSizeInitialValue: 10, +}; + +export default React.memo(FilterRulesTable); diff --git a/src/presentation/react/core/components/filter-rules-table/Section.tsx b/src/presentation/react/core/components/filter-rules-table/Section.tsx new file mode 100644 index 000000000..97b8fd20d --- /dev/null +++ b/src/presentation/react/core/components/filter-rules-table/Section.tsx @@ -0,0 +1,38 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import Card from "@material-ui/core/Card"; +import CardContent from "@material-ui/core/CardContent"; +import Typography from "@material-ui/core/Typography"; + +const useStyles = makeStyles({ + root: { + minWidth: 275, + marginBottom: 15, + }, + bullet: { + display: "inline-block", + margin: "0 2px", + transform: "scale(0.8)", + }, + title: { + fontSize: 14, + fontWeight: "bold", + }, +}); + +export const Section: React.FC<{ title: string }> = props => { + const { title, children } = props; + const classes = useStyles(); + + return ( + + + + {title} + + + {children} + + + ); +}; diff --git a/src/presentation/react/core/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx b/src/presentation/react/core/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx new file mode 100644 index 000000000..b6d5d0051 --- /dev/null +++ b/src/presentation/react/core/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx @@ -0,0 +1,95 @@ +import _ from "lodash"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import { Maybe } from "../../../../../types/utils"; +import Dropdown, { DropdownViewOption } from "../dropdown/Dropdown"; +import { useAppContext } from "../../contexts/AppContext"; +import { Store } from "../../../../../domain/stores/entities/Store"; + +export type InstanceSelectionOption = "local" | "remote" | "store"; + +export type InstanceSelectionConfig = Partial>; + +export interface InstanceSelectionDropdownProps { + showInstances: InstanceSelectionConfig; + selectedInstance: Maybe; + onChangeSelected: ( + type: T, + instance?: T extends "remote" ? Instance : T extends "store" ? Store : never + ) => void; + view?: DropdownViewOption; + title?: string; + refreshKey?: number; +} + +export const InstanceSelectionDropdown: React.FC = React.memo( + ({ + showInstances, + selectedInstance, + onChangeSelected, + view = "filter", + title = i18n.t("Instances"), + refreshKey, + }) => { + const { compositionRoot } = useAppContext(); + + const [instances, setInstances] = useState([]); + const [stores, setStores] = useState([]); + + const updateSelectedInstance = useCallback( + (id: string) => { + if (id === "LOCAL") { + onChangeSelected("local"); + } else { + const store = stores.find(store => store.id === id); + const instance = instances.find(instance => instance.id === id); + + onChangeSelected(instance ? "remote" : "store", instance ?? store); + } + }, + [instances, stores, onChangeSelected] + ); + + const instanceItems = useMemo(() => { + const localInstance = { id: "LOCAL", name: i18n.t("This instance") }; + const storeInstances = stores.map(store => ({ + id: store.id, + name: `${store.account} - ${store.repository} (${i18n.t("Store")})`, + })); + + return _.compact([ + showInstances.local && localInstance, + ...(showInstances.store ? storeInstances : []), + ...(showInstances.remote ? instances.filter(item => item.type === "dhis") : []), + ]); + }, [showInstances, instances, stores]); + + useEffect(() => { + compositionRoot.instances.list().then(setInstances); + + if (showInstances.store) { + compositionRoot.store.list().then(setStores); + } + }, [compositionRoot, showInstances, refreshKey]); + + useEffect(() => { + // Auto-select first instance + const firstInstanceItem = instanceItems[0]; + if (_.isNil(selectedInstance) && firstInstanceItem) { + updateSelectedInstance(firstInstanceItem.id); + } + }, [instanceItems, selectedInstance, updateSelectedInstance]); + + return ( + + ); + } +); diff --git a/src/presentation/react/core/components/landing/Landing.tsx b/src/presentation/react/core/components/landing/Landing.tsx new file mode 100644 index 000000000..ec60d752b --- /dev/null +++ b/src/presentation/react/core/components/landing/Landing.tsx @@ -0,0 +1,66 @@ +import { makeStyles } from "@material-ui/core"; +import React from "react"; +import _ from "lodash"; +import PageHeader from "../page-header/PageHeader"; +import MenuCard, { MenuCardProps } from "./MenuCard"; + +const useStyles = makeStyles({ + container: { + marginLeft: 30, + }, + title: { + fontSize: 24, + fontWeight: 300, + color: "rgba(0, 0, 0, 0.87)", + padding: "15px 0px 15px", + margin: 0, + }, + clear: { + clear: "both", + }, +}); + +export interface Card { + title?: string; + key: string; + isVisible?: boolean; + children: MenuCardProps[]; +} + +export interface LandingProps { + cards: Card[]; + title?: string; + onBackClick?: () => void; +} + +export const Landing: React.FC = ({ title, cards, onBackClick }) => { + const classes = useStyles(); + + return ( + + {!!title && } + +
+ {cards.map( + ({ key, title, isVisible = true, children }) => + isVisible && + isAnyChildVisible(children) && ( +
+ {!!title &&

{title}

} + + {children.map(props => ( + + ))} + +
+
+ ) + )} +
+ + ); +}; + +function isAnyChildVisible(children: MenuCardProps[]): boolean { + return _.some(children, ({ isVisible = true }) => isVisible); +} diff --git a/src/presentation/react/core/components/landing/MenuCard.tsx b/src/presentation/react/core/components/landing/MenuCard.tsx new file mode 100644 index 000000000..0638364aa --- /dev/null +++ b/src/presentation/react/core/components/landing/MenuCard.tsx @@ -0,0 +1,117 @@ +import { makeStyles, Tooltip } from "@material-ui/core"; +import Card from "@material-ui/core/Card"; +import CardActions from "@material-ui/core/CardActions"; +import CardContent from "@material-ui/core/CardContent"; +import CardHeader from "@material-ui/core/CardHeader"; +import IconButton from "@material-ui/core/IconButton"; +import AddIcon from "@material-ui/icons/Add"; +import ViewListIcon from "@material-ui/icons/ViewList"; +import _ from "lodash"; +import React, { ReactNode } from "react"; +import i18n from "../../../../../locales"; + +export interface MenuCardProps { + name: string; + description?: string; + icon?: ReactNode; + isVisible?: boolean; + addAction?: () => void; + listAction?: () => void; +} + +export interface MenuCardTitleProps { + text: string; + icon?: ReactNode; +} + +const useStyles = makeStyles({ + card: { + padding: "0", + margin: ".5rem", + float: "left", + width: "230px", + }, + content: { + height: "120px", + padding: ".5rem 1rem", + fontSize: "14px", + }, + actions: { + marginLeft: "auto", + }, + header: { + padding: "1rem", + height: "auto", + borderBottom: "1px solid #ddd", + cursor: "pointer", + }, + headerText: { + fontSize: "15px", + fontWeight: 500, + }, + cardTitle: { + display: "flex", + }, + icon: { + marginLeft: "auto", + display: "inline", + }, +}); + +const MenuCard: React.FC = ({ + name, + icon, + description, + isVisible, + addAction, + listAction, +}) => { + const classes = useStyles(); + + if (isVisible === false) return null; + + return ( + + } + /> + + {description} + + +
+ {addAction && ( + + + + + + )} + + {listAction && ( + + + + + + )} +
+
+
+ ); +}; + +const MenuCardTitle: React.FC = ({ text, icon }) => { + const classes = useStyles(); + + return ( + + {text} + {!!icon &&
{icon}
} +
+ ); +}; + +export default MenuCard; diff --git a/src/presentation/react/core/components/mapping-dialog/MappingDialog.tsx b/src/presentation/react/core/components/mapping-dialog/MappingDialog.tsx new file mode 100644 index 000000000..2da8c7f1b --- /dev/null +++ b/src/presentation/react/core/components/mapping-dialog/MappingDialog.tsx @@ -0,0 +1,181 @@ +import { Typography } from "@material-ui/core"; +import DialogContent from "@material-ui/core/DialogContent"; +import { makeStyles } from "@material-ui/styles"; +import { ConfirmationDialog, OrgUnitsSelector } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { DataSource, isDhisInstance } from "../../../../../domain/instance/entities/DataSource"; +import { MetadataMappingDictionary } from "../../../../../domain/mapping/entities/MetadataMapping"; +import i18n from "../../../../../locales"; +import { modelFactory } from "../../../../../models/dhis/factory"; +import { MetadataType } from "../../../../../utils/d2"; +import { useAppContext } from "../../contexts/AppContext"; +import { EXCLUDED_KEY } from "../mapping-table/utils"; +import MetadataTable from "../metadata-table/MetadataTable"; + +export interface MappingDialogConfig { + elements: string[]; + mappingType?: string; + mappingPath?: string[]; + firstElement?: MetadataType; +} + +export interface MappingDialogProps { + config: MappingDialogConfig; + instance: DataSource; + mapping: MetadataMappingDictionary; + onUpdateMapping: (items: string[], id?: string) => void; + onClose: () => void; +} + +const useStyles = makeStyles({ + orgUnitSelect: { + margin: "0 auto", + }, +}); + +const MappingDialog: React.FC = ({ + config, + instance, + mapping, + onUpdateMapping, + onClose, +}) => { + const { api: defaultApi, compositionRoot } = useAppContext(); + const classes = useStyles(); + const [connectionSuccess, setConnectionSuccess] = useState(true); + const [filterRows, setFilterRows] = useState(); + const { elements, mappingType, mappingPath, firstElement } = config; + if (!mappingType) { + throw new Error("Attempting to open mapping dialog without a valid mapping type"); + } + + const mappedId = + elements.length === 1 + ? _.last( + _(mapping) + .get([mappingType, elements[0] ?? "", "mappedId"]) + ?.split("-") + ) + : undefined; + const defaultSelection = mappedId !== "DISABLED" ? mappedId : undefined; + const [selected, updateSelected] = useState(defaultSelection); + + const model = modelFactory(mappingType); + const modelName = model.getModelName(); + const api = isDhisInstance(instance) ? compositionRoot.instances.getApi(instance) : defaultApi; + + useEffect(() => { + let mounted = true; + + if (isDhisInstance(instance)) { + compositionRoot.instances.validate(instance).then(result => { + if (result.isError()) console.error(result.value.error); + if (mounted) setConnectionSuccess(result.isSuccess()); + }); + } + + return () => { + mounted = false; + }; + }, [instance, compositionRoot]); + + useEffect(() => { + if (mappingPath) { + const parentMappedId = mappingPath[2]; + compositionRoot.mapping.getValidIds(instance, parentMappedId).then(setFilterRows); + } else if (mappingType === "programDataElements" && elements.length === 1) { + compositionRoot.mapping.getValidIds(instance, elements[0]).then(validIds => { + setFilterRows(buildDataElementFilterForProgram(validIds, elements[0], mapping)); + }); + } + }, [compositionRoot, instance, api, mappingPath, elements, mapping, mappingType]); + + const onUpdateSelection = (selectedIds: string[]) => { + const newSelection = _.last(selectedIds); + onUpdateMapping(elements, newSelection); + updateSelected(newSelection); + }; + + const OrgUnitMapper = ( +
+ +
+ ); + + const MetadataMapper = ( + + ); + + const MapperComponent = + model.getCollectionName() === "organisationUnits" ? OrgUnitMapper : MetadataMapper; + const title = + elements.length > 1 || !firstElement + ? i18n.t( + "Select {{type}} from destination instance {{instance}} to map {{total}} elements", + { + type: modelName, + instance: instance.name, + total: elements.length, + } + ) + : i18n.t( + "Select {{type}} from destination instance {{instance}} to map {{name}} ({{id}})", + { + type: modelName, + instance: instance.name, + name: firstElement.name, + id: firstElement.id, + } + ); + + return ( + 0} + title={title} + onCancel={onClose} + maxWidth={"lg"} + fullWidth={true} + cancelText={i18n.t("Close")} + > + + {connectionSuccess ? ( + MapperComponent + ) : ( + {i18n.t("Could not connect with remote instance")} + )} + + + ); +}; + +const buildDataElementFilterForProgram = ( + validIds: string[], + nestedId: string, + mapping: MetadataMappingDictionary +): string[] | undefined => { + const originProgramId = nestedId.split("-")[0]; + const { mappedId } = _.get(mapping, ["eventPrograms", originProgramId]) ?? {}; + + if (!mappedId || mappedId === EXCLUDED_KEY) return undefined; + return [...validIds, mappedId]; +}; + +export default MappingDialog; diff --git a/src/presentation/react/core/components/mapping-table/MappingTable.tsx b/src/presentation/react/core/components/mapping-table/MappingTable.tsx new file mode 100644 index 000000000..64365cd29 --- /dev/null +++ b/src/presentation/react/core/components/mapping-table/MappingTable.tsx @@ -0,0 +1,863 @@ +import { Icon, IconButton, makeStyles, Tooltip, Typography } from "@material-ui/core"; +import { + ConfirmationDialog, + RowConfig, + TableAction, + TableColumn, + TableGlobalAction, + useLoading, + useSnackbar, +} from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useMemo, useState } from "react"; +import { DataSource } from "../../../../../domain/instance/entities/DataSource"; +import { MappingConfig } from "../../../../../domain/mapping/entities/MappingConfig"; +import { + MetadataMapping, + MetadataMappingDictionary, +} from "../../../../../domain/mapping/entities/MetadataMapping"; +import { cleanOrgUnitPath } from "../../../../../domain/synchronization/utils"; +import i18n from "../../../../../locales"; +import { D2Model } from "../../../../../models/dhis/default"; +import { ProgramDataElementModel } from "../../../../../models/dhis/mapping"; +import { DataElementModel, OrganisationUnitModel } from "../../../../../models/dhis/metadata"; +import { MetadataType } from "../../../../../utils/d2"; +import { useAppContext } from "../../contexts/AppContext"; +import MappingDialog, { MappingDialogConfig } from "../mapping-dialog/MappingDialog"; +import MappingWizard, { MappingWizardConfig, prepareSteps } from "../mapping-wizard/MappingWizard"; +import MetadataTable, { MetadataTableProps } from "../metadata-table/MetadataTable"; +import { cleanNestedMappedId, EXCLUDED_KEY, getChildrenRows } from "./utils"; + +const useStyles = makeStyles({ + iconButton: { + padding: 0, + paddingLeft: 8, + paddingRight: 8, + }, + instanceDropdown: { + order: 0, + }, + actionButtons: { + order: 10, + marginRight: 10, + }, +}); + +interface WarningDialog { + title?: string; + description?: string; + action?: () => void; +} + +export interface MappingTableProps extends MetadataTableProps { + originInstance?: DataSource; + destinationInstance: DataSource; + models: typeof D2Model[]; + filterRows?: string[]; + transformRows?: (rows: MetadataType[]) => MetadataType[]; + mapping: MetadataMappingDictionary; + globalMapping: MetadataMappingDictionary; + onChangeMapping(mapping: MetadataMappingDictionary): Promise; + onApplyGlobalMapping(type: string, id: string, mapping: MetadataMapping): Promise; + isChildrenMapping?: boolean; + mappingPath?: string[]; +} + +export default function MappingTable({ + originInstance, + destinationInstance, + models, + filterRows, + transformRows, + mapping, + globalMapping, + onChangeMapping, + onApplyGlobalMapping, + isChildrenMapping = false, + mappingPath, + ...rest +}: MappingTableProps) { + const { compositionRoot } = useAppContext(); + const classes = useStyles(); + const snackbar = useSnackbar(); + const loading = useLoading(); + + const [model, setModel] = useState(() => models[0] ?? DataElementModel); + + const [rows, setRows] = useState([]); + const [selectedIds, setSelectedIds] = useState([]); + + const [warningDialog, setWarningDialog] = useState(null); + const [mappingConfig, setMappingConfig] = useState(null); + const [wizardConfig, setWizardConfig] = useState(null); + + const getMappedItem = useCallback( + (row?: MetadataType): MetadataMapping => { + const mappingType = row?.model.getMappingType(); + if (!row || !mappingType) return {}; + + const id = cleanNestedMappedId(row.id); + const localItemMapping = _.get(mapping, [mappingType, row.id]); + const globalItemMapping = _.get(globalMapping, [mappingType, id]); + + const isMapped = !!localItemMapping?.mappedId; + const isDifferent = localItemMapping?.mappedId !== globalItemMapping?.mappedId; + const itemMapping = isMapped && isDifferent ? localItemMapping : globalItemMapping; + + return itemMapping ?? {}; + }, + [mapping, globalMapping] + ); + + const applyMapping = useCallback( + async (config: MappingConfig[]) => { + try { + const newMapping = await compositionRoot.mapping.apply( + originInstance ?? compositionRoot.localInstance, + destinationInstance, + mapping, + config, + isChildrenMapping + ); + + await onChangeMapping(newMapping); + setSelectedIds([]); + } catch (e) { + console.error(e); + snackbar.error(i18n.t("Could not apply mapping, please try again.")); + } + loading.reset(); + }, + [ + compositionRoot, + originInstance, + destinationInstance, + snackbar, + loading, + mapping, + isChildrenMapping, + onChangeMapping, + ] + ); + + const makeMappingGlobal = useCallback( + async (selection: string[]) => { + const id = selection[0]; + const firstElement = _.find(rows, ["id", id]); + const mappingType = firstElement?.model.getMappingType(); + const elementMapping = mappingType ? _.get(mapping, [mappingType, id]) : {}; + + if (!firstElement || !mappingType || !elementMapping?.mappedId) { + snackbar.error(i18n.t("You need to map the item before applying a global mapping")); + } else { + await applyMapping([ + { selection, mappingType, global: false, mappedId: undefined }, + ]); + await onApplyGlobalMapping(mappingType, cleanNestedMappedId(id), elementMapping); + snackbar.success(i18n.t("Successfully applied global mapping")); + } + }, + [onApplyGlobalMapping, applyMapping, rows, mapping, snackbar] + ); + + const updateMapping = useCallback( + async (selection: string[], mappedId?: string) => { + const id = selection[0]; + const firstElement = _.find(rows, ["id", id]); + const mappingType = firstElement?.model.getMappingType(); + const global = firstElement?.model.getIsGlobalMapping(); + if (!mappingType) { + snackbar.error(i18n.t("Unable to update mapping")); + } else { + applyMapping([{ selection, mappingType, global, mappedId }]); + } + }, + [applyMapping, rows, snackbar] + ); + + const disableMapping = useCallback( + async (selection: string[]) => { + const id = selection[0]; + const firstElement = _.find(rows, ["id", id]); + const mappingType = firstElement?.model.getMappingType(); + const global = firstElement?.model.getIsGlobalMapping(); + if (selection.length > 0 && mappingType) { + setWarningDialog({ + title: i18n.t("Exclude mapping"), + description: i18n.t( + "Are you sure you want to exclude mapping for {{total}} elements?", + { + total: selection.length, + } + ), + action: () => { + applyMapping([{ selection, mappingType, global, mappedId: EXCLUDED_KEY }]); + }, + }); + } else { + snackbar.error(i18n.t("Please select at least one item to exclude mapping")); + } + }, + [snackbar, applyMapping, rows] + ); + + const resetMapping = useCallback( + async (selection: string[]) => { + const id = selection[0]; + const firstElement = _.find(rows, ["id", id]); + const mappingType = firstElement?.model.getMappingType(); + const global = firstElement?.model.getIsGlobalMapping(); + if (selection.length > 0 && mappingType) { + setWarningDialog({ + title: i18n.t("Reset mapping"), + description: i18n.t( + "Are you sure you want to reset mapping for {{total}} elements?", + { + total: selection.length, + } + ), + action: () => { + applyMapping([{ selection, mappingType, global, mappedId: undefined }]); + }, + }); + } else { + snackbar.error(i18n.t("Please select at least one item to reset mapping")); + } + }, + [snackbar, applyMapping, rows] + ); + + const applyAutoMapping = useCallback( + async (elements: string[]) => { + const types = _(rows) + .filter(({ id }) => elements.includes(id)) + .map(row => row.model.getMappingType()) + .uniq() + .compact() + .value(); + + const id = elements[0]; + const firstElement = _.find(rows, ["id", id]); + const global = firstElement?.model.getIsGlobalMapping(); + + if (types.length === 0) { + snackbar.error(i18n.t("You need to select at least one valid item")); + } else if (types.length > 1) { + snackbar.error(i18n.t("You need to select all items from the same type")); + } + + try { + loading.show( + true, + i18n.t("Preparing auto-mapping for {{total}} elements", { + total: elements.length, + }) + ); + + const { tasks, errors } = await compositionRoot.mapping.autoMap( + originInstance ?? compositionRoot.localInstance, + destinationInstance, + mapping, + types[0], + elements, + global + ); + + await applyMapping(tasks); + + if (errors.length > 0) { + snackbar.error( + errors + .map(id => + i18n.t( + "Could not find a suitable candidate to apply auto-mapping for {{id}}", + { id } + ) + ) + .join("\n") + ); + } else if (elements.length === 1) { + const firstElement = _.find(rows, ["id", elements[0]]); + const mappingType = firstElement?.model.getMappingType(); + if (firstElement && mappingType) { + setMappingConfig({ + elements, + mappingPath, + mappingType, + firstElement, + }); + } + } + } catch (e) { + console.error(e); + snackbar.error(i18n.t("Could not connect with remote instance")); + } + loading.reset(); + }, + [ + compositionRoot, + destinationInstance, + originInstance, + loading, + applyMapping, + rows, + snackbar, + mappingPath, + mapping, + ] + ); + + const openMappingDialog = useCallback( + (elements: string[]) => { + const firstElement = _.find(rows, ["id", elements[0]]); + const types = _(rows) + .filter(({ id }) => elements.includes(id)) + .map(row => row.model.getMappingType()) + .uniq() + .value(); + + if (types.length === 1) { + setMappingConfig({ elements, mappingPath, mappingType: types[0], firstElement }); + setSelectedIds([]); + } else if (types.length > 1) { + snackbar.error(i18n.t("You need to select all items from the same type")); + } else { + snackbar.error(i18n.t("You need to select at least one valid item")); + } + }, + [mappingPath, rows, snackbar] + ); + + const createValidations = useCallback( + async (dict: MetadataMappingDictionary) => { + const result = _.cloneDeep(dict); + + for (const type of _.keys(dict)) { + for (const id of _.keys(dict[type])) { + const { mappedId, mapping = {}, ...rest } = dict[type][id]; + const innerMapping = await createValidations(mapping); + + const { + mappedName, + mappedCode, + mappedLevel, + } = await compositionRoot.mapping.buildMapping({ + originInstance: originInstance ?? compositionRoot.localInstance, + destinationInstance, + originalId: id, + mappedId, + }); + + result[type][id] = _.omitBy( + { + ...rest, + mappedId, + mappedName, + mappedCode, + mappedLevel, + mapping: innerMapping, + }, + _.isUndefined + ); + } + } + + return result; + }, + [compositionRoot, destinationInstance, originInstance] + ); + + const applyValidateMapping = useCallback( + async (selection: string[]) => { + loading.show( + true, + i18n.t("Validating mapping for {{total}} elements", { total: selection.length }) + ); + + const tasks = []; + const selectedRows = _.compact(selection.map(id => _.find(rows, ["id", id]))); + const allRows = [...selectedRows, ...getChildrenRows(selectedRows, model)]; + + for (const row of allRows) { + const mappingType = row.model.getMappingType(); + const global = row.model.getIsGlobalMapping(); + if (mappingType) { + const newMapping = await createValidations({ + [mappingType]: { + [row.id]: getMappedItem(row), + }, + }); + const { mappedId, ...overrides } = newMapping[mappingType][row.id]; + tasks.push({ selection: [row.id], mappingType, global, mappedId, overrides }); + } + } + + applyMapping(tasks); + loading.reset(); + }, + [applyMapping, getMappedItem, loading, rows, createValidations, model] + ); + + const validateMapping = useCallback( + async (selection: string[]) => { + if (selection.length > 0) { + setWarningDialog({ + title: i18n.t("Validate mapping"), + description: i18n.t( + "Are you sure you want to validate mapping for {{total}} elements?", + { + total: selection.length, + } + ), + action: () => applyValidateMapping(selection), + }); + } else { + snackbar.error(i18n.t("Please select at least one item to validate mapping")); + } + }, + [snackbar, applyValidateMapping] + ); + + const openRelatedMapping = useCallback( + (selection: string[]) => { + const id = _.first(selection); + const element = _.find(rows, ["id", id]); + if (!id || !element) return; + + const mappingType = element.model.getMappingType(); + const { mapping: rowMapping = undefined } = mappingType + ? _.get(mapping, [mappingType, id]) + : {}; + + if (!rowMapping || !mappingType) { + snackbar.error( + i18n.t( + "You need to map this element before accessing its related metadata mapping" + ) + ); + } else { + setWizardConfig({ mappingPath: [mappingType, id], type: mappingType, element }); + } + }, + [mapping, rows, snackbar] + ); + + const updateSelection = (selection: string[]) => { + setSelectedIds(prevSelection => { + const removedRows = _(prevSelection) + .difference(selection) + .map(id => _.find(rows, ["id", id])) + .compact() + .value(); + const childrenRemovals = getChildrenRows(removedRows, model).map(({ id }) => id); + + return _.difference(selection, childrenRemovals); + }); + }; + + const rowConfig = useCallback( + (row: MetadataType): RowConfig => { + const mappingType = row.model.getMappingType(); + + if (!mappingType) { + return { selectable: false }; + } else if (mappingType === ProgramDataElementModel.getMappingType()) { + const parentId = _.first(row.id.split("-")) ?? row.id; + const parentMapping = _.get(mapping, ["eventPrograms", parentId, "mappedId"]); + const isParentMapped = !!parentMapping && parentMapping !== EXCLUDED_KEY; + const { mappedId } = getMappedItem(row); + + const hasErrors = isParentMapped && !mappedId; + return { + style: hasErrors ? { backgroundColor: "#ffcdd2" } : undefined, + }; + } else { + return {}; + } + }, + [getMappedItem, mapping] + ); + + const columns: TableColumn[] = useMemo( + () => + _.compact([ + { name: "lastUpdated", text: i18n.t("Last updated"), sortable: true, hidden: true }, + { + name: "id", + text: i18n.t("ID"), + getValue: (row: MetadataType) => { + return cleanNestedMappedId(row.id); + }, + }, + { + name: "metadata-type", + text: i18n.t("Metadata type"), + hidden: model.getChildrenKeys() === undefined, + getValue: (row: MetadataType) => { + return row.model.getModelName(); + }, + }, + { + name: "mapped-id", + text: i18n.t("Mapped ID"), + sortable: false, + getValue: (row: MetadataType) => { + const { mappedId } = getMappedItem(row); + const mappingType = row.model.getMappingType(); + const text = + !!mappedId && mappedId !== EXCLUDED_KEY + ? cleanOrgUnitPath(mappedId) + : "-"; + + return ( + + + {text} + + {!!mappingType && ( + + { + event.stopPropagation(); + openMappingDialog([row.id]); + }} + > + open_in_new + + + )} + + ); + }, + }, + { + name: "mapped-name", + text: i18n.t("Mapped Name"), + sortable: false, + getValue: (row: MetadataType) => { + const { + mappedName, + conflicts = false, + mapping: childrenMapping, + } = getMappedItem(row); + + const childrenConflicts = _(childrenMapping) + .values() + .map(Object.values) + .flatten() + .some(["conflicts", true]); + const showConflicts = conflicts || childrenConflicts; + + return ( + + + {mappedName ?? "-"} + + {showConflicts && ( + + { + event.stopPropagation(); + if (!isChildrenMapping) + openRelatedMapping([row.id]); + else openMappingDialog([row.id]); + }} + > + warning + + + )} + + ); + }, + }, + model === OrganisationUnitModel + ? { + name: "mapped-level", + text: i18n.t("Mapped Level"), + sortable: false, + getValue: (row: MetadataType) => { + const { mappedLevel } = getMappedItem(row); + + return ( + + + {mappedLevel ?? "-"} + + + ); + }, + } + : undefined, + { + name: "mapping-status", + text: i18n.t("Mapping Status"), + sortable: false, + getValue: (row: MetadataType) => { + const { mappedId, global = false } = getMappedItem(row); + + const notMappedStatus = !mappedId ? i18n.t("Not mapped") : undefined; + const disabledStatus = + mappedId === EXCLUDED_KEY ? i18n.t("Excluded") : undefined; + const globalStatus = global ? i18n.t("Mapped (Global)") : i18n.t("Mapped"); + + return ( + + + {notMappedStatus ?? disabledStatus ?? globalStatus} + + + ); + }, + }, + ]), + [classes, model, openMappingDialog, isChildrenMapping, openRelatedMapping, getMappedItem] + ); + + const addToSelection = useCallback( + (selection: string[]) => { + const ids = _(selection) + .map(id => _.find(rows, ["id", id])) + .compact() + .filter(row => !!row.model.getMappingType()) + .map(({ id }) => id) + .value(); + + setSelectedIds(prevSelection => { + const oldSelection = _.difference(prevSelection, ids); + const newSelection = _.difference(ids, prevSelection); + return _.uniq([...oldSelection, ...newSelection]); + }); + }, + [rows] + ); + + const actions: TableAction[] = useMemo( + () => [ + { + name: "select", + text: "Select", + onClick: addToSelection, + isActive: () => false, + primary: true, + }, + { + name: "set-mapping", + text: i18n.t("Set mapping"), + multiple: true, + onClick: openMappingDialog, + icon: open_in_new, + isActive: (selected: MetadataType[]) => { + return _.every(selected, row => row.model.getMappingType()); + }, + }, + { + name: "select-children-rows", + text: i18n.t("Select children"), + multiple: true, + onClick: (selection: string[]) => { + const selectedRows = _.compact(selection.map(id => _.find(rows, ["id", id]))); + const children = getChildrenRows(selectedRows, model).map(({ id }) => id); + setSelectedIds(prevSelection => _.uniq([...prevSelection, ...children])); + }, + icon: done_all, + isActive: (selection: MetadataType[]) => { + const children = getChildrenRows(selection, model); + return children.length > 0; + }, + }, + { + name: "global-mapping", + text: i18n.t("Make this mapping global"), + multiple: false, + onClick: makeMappingGlobal, + icon: add_circle_outline, + isActive: (selected: MetadataType[]) => { + const isRowMappedAndNotGlobal = _(selected) + .map(getMappedItem) + .every(({ mappedId, global }) => !!mappedId && !global); + const isRowCompatible = + isChildrenMapping || + _.every(selected, row => row.model.getIsGlobalMapping()); + + return isRowMappedAndNotGlobal && isRowCompatible; + }, + }, + { + name: "validate-mapping", + text: i18n.t("Validate mapping"), + multiple: true, + onClick: validateMapping, + icon: find_replace, + isActive: (selected: MetadataType[]) => { + const isGlobalMapping = _.some(selected, row => row.model.getIsGlobalMapping()); + return _(selected) + .map(getMappedItem) + .some(({ mappedId, global }) => !!mappedId && (isGlobalMapping || !global)); + }, + }, + { + name: "auto-mapping", + text: i18n.t("Auto-map element"), + multiple: true, + onClick: applyAutoMapping, + icon: compare_arrows, + isActive: (selected: MetadataType[]) => { + return _.every(selected, row => row.model.getMappingType()); + }, + }, + { + name: "disable-mapping", + text: i18n.t("Exclude mapping"), + multiple: true, + onClick: disableMapping, + icon: sync_disabled, + isActive: (selected: MetadataType[]) => { + return _.every(selected, row => row.model.getMappingType()); + }, + }, + { + name: "reset-mapping", + text: i18n.t("Reset mapping to default values"), + multiple: true, + onClick: resetMapping, + icon: clear, + isActive: (selected: MetadataType[]) => { + return _.every(selected, row => row.model.getMappingType()); + }, + }, + { + name: "related-mapping", + text: i18n.t("Related metadata mapping"), + multiple: false, + onClick: openRelatedMapping, + icon: assignment, + isActive: (selected: MetadataType[]) => { + const element = selected[0]; + const mappingType = element.model.getMappingType(); + const steps = prepareSteps(mappingType, element); + const { mappedId } = getMappedItem(element); + + return !!mappedId && !isChildrenMapping && steps.length > 0; + }, + }, + ], + [ + addToSelection, + disableMapping, + openMappingDialog, + resetMapping, + applyAutoMapping, + makeMappingGlobal, + validateMapping, + openRelatedMapping, + getMappedItem, + isChildrenMapping, + model, + rows, + ] + ); + + const globalActions: TableGlobalAction[] = _.compact([ + model !== OrganisationUnitModel + ? { + name: "validate-mapping", + text: i18n.t("Validate mapping"), + onClick: validateMapping, + icon: find_replace, + } + : undefined, + model !== OrganisationUnitModel + ? { + name: "reset-mapping", + text: i18n.t("Reset mapping"), + onClick: resetMapping, + icon: clear, + } + : undefined, + model !== OrganisationUnitModel + ? { + name: "disable-mapping", + text: i18n.t("Exclude mapping"), + onClick: disableMapping, + icon: sync_disabled, + } + : undefined, + ]); + + const notifyNewModel = useCallback(model => { + setRows([]); + setSelectedIds([]); + setModel(() => model); + }, []); + + const updateRows = useCallback( + (rows: MetadataType[]) => { + setRows([...rows, ...getChildrenRows(rows, model)]); + }, + [model] + ); + + const closeWarningDialog = () => setWarningDialog(null); + const closeMappingDialog = () => setMappingConfig(null); + const closeWizard = () => setWizardConfig(null); + + return ( + + {!!warningDialog && ( + { + if (warningDialog.action) warningDialog.action(); + setWarningDialog(null); + }} + onCancel={closeWarningDialog} + /> + )} + + {!!mappingConfig && ( + + )} + + {!!wizardConfig && ( + + )} + + + + ); +} diff --git a/src/presentation/react/core/components/mapping-table/utils.tsx b/src/presentation/react/core/components/mapping-table/utils.tsx new file mode 100644 index 000000000..35a25edaf --- /dev/null +++ b/src/presentation/react/core/components/mapping-table/utils.tsx @@ -0,0 +1,17 @@ +import _ from "lodash"; +import { D2Model } from "../../../../../models/dhis/default"; +import { MetadataType } from "../../../../../utils/d2"; + +export const EXCLUDED_KEY = "DISABLED"; + +export const cleanNestedMappedId = (id: string): string => { + return _(id).split("-").last() ?? ""; +}; + +export const getChildrenRows = (rows: MetadataType[], model: typeof D2Model): MetadataType[] => { + const childrenKeys = model.getChildrenKeys() ?? []; + + return _.flattenDeep( + rows.map(row => Object.values(_.pick(row, childrenKeys)) as MetadataType[]) + ); +}; diff --git a/src/presentation/react/core/components/mapping-wizard/MappingWizard.tsx b/src/presentation/react/core/components/mapping-wizard/MappingWizard.tsx new file mode 100644 index 000000000..7788696ed --- /dev/null +++ b/src/presentation/react/core/components/mapping-wizard/MappingWizard.tsx @@ -0,0 +1,126 @@ +import { DialogContent } from "@material-ui/core"; +import { ConfirmationDialog, Wizard, WizardStep } from "d2-ui-components"; +import _ from "lodash"; +import React, { useState } from "react"; +import { DataSource } from "../../../../../domain/instance/entities/DataSource"; +import { + MetadataMapping, + MetadataMappingDictionary, +} from "../../../../../domain/mapping/entities/MetadataMapping"; +import i18n from "../../../../../locales"; +import { MetadataType } from "../../../../../utils/d2"; +import { MappingTableProps } from "../mapping-table/MappingTable"; +import { cleanNestedMappedId } from "../mapping-table/utils"; +import { buildModelSteps } from "./Steps"; + +export interface MappingWizardStep extends WizardStep { + showOnSyncDialog?: boolean; + props: MappingTableProps; +} + +export interface MappingWizardConfig { + mappingPath: string[]; + type: string; + element: MetadataType; +} + +export interface MappingWizardProps { + originInstance?: DataSource; + destinationInstance: DataSource; + mapping: MetadataMappingDictionary; + config: MappingWizardConfig; + onUpdateMapping: (mapping: MetadataMappingDictionary) => Promise; + onApplyGlobalMapping(type: string, id: string, mapping: MetadataMapping): Promise; + onCancel?(): void; +} + +export const prepareSteps = (type: string | undefined, element: MetadataType) => { + if (!type) return []; + return buildModelSteps(type).filter(({ isVisible = _.noop }) => isVisible(type, element)); +}; + +const MappingWizard: React.FC = ({ + originInstance, + destinationInstance, + mapping: instanceMapping, + config, + onUpdateMapping, + onApplyGlobalMapping, + onCancel = _.noop, +}) => { + const { mappingPath, type, element } = config; + + const { mappedId = "", mapping = {} }: MetadataMapping = _.get( + instanceMapping, + mappingPath, + {} + ); + + const mappingKeys = _(mapping).mapValues(Object.keys).values().flatten().value(); + + const filterRows = mappingKeys.map(cleanNestedMappedId); + + const transformRows = (rows: MetadataType[]) => { + return rows.filter(({ id }) => mappingKeys.includes(id)); + }; + + const onChangeMapping = async (subMapping: MetadataMappingDictionary) => { + const newMapping = _.clone(instanceMapping); + _.set(newMapping, [...mappingPath, "mapping"], subMapping); + await onUpdateMapping(newMapping); + }; + + const steps: MappingWizardStep[] = + prepareSteps(type, element).map(({ models, ...step }) => ({ + ...step, + props: { + models, + globalMapping: instanceMapping, + mapping, + onChangeMapping, + onApplyGlobalMapping, + originInstance, + destinationInstance, + filterRows, + transformRows, + mappingPath: [...mappingPath, mappedId], + isChildrenMapping: true, + }, + })) ?? []; + + const [stepName, updateStepName] = useState(steps[0]?.label); + + const onStepChangeRequest = async (_prev: WizardStep, next: WizardStep) => { + updateStepName(next.label); + return undefined; + }; + + if (steps.length === 0) return null; + + const initialStepKey = steps.map(step => step.key)[0]; + const mainTitle = i18n.t(`Related metadata mapping for {{name}} ({{id}})`, element); + const title = _.compact([mainTitle, stepName]).join(" - "); + + return ( + + + + + + ); +}; + +export default MappingWizard; diff --git a/src/presentation/react/core/components/mapping-wizard/Steps.tsx b/src/presentation/react/core/components/mapping-wizard/Steps.tsx new file mode 100644 index 000000000..3d6a39251 --- /dev/null +++ b/src/presentation/react/core/components/mapping-wizard/Steps.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import i18n from "../../../../../locales"; +import { D2Model } from "../../../../../models/dhis/default"; +import { + CategoryOptionMappedModel, + OptionMappedModel, + ProgramStageMappedModel, +} from "../../../../../models/dhis/mapping"; +import { Dictionary } from "../../../../../types/utils"; +import { MetadataType } from "../../../../../utils/d2"; +import MappingTable, { MappingTableProps } from "../mapping-table/MappingTable"; +import { MappingWizardStep } from "./MappingWizard"; + +type MappingWizardStepBuilder = Omit & { + models: typeof D2Model[]; + isVisible?: (type: string, element: MetadataType) => boolean; +}; + +export const buildModelSteps = (type: string): MappingWizardStepBuilder[] => { + const availableSteps: { [key: string]: MappingWizardStepBuilder } = { + categoryOptions: { + key: "category-options", + label: i18n.t("Category Options"), + component: (props: MappingTableProps) => , + models: [CategoryOptionMappedModel], + isVisible: (_type: string, element: MetadataType) => { + return !!element.categoryCombo?.id; + }, + }, + options: { + key: "options", + label: i18n.t("Options"), + component: (props: MappingTableProps) => , + models: [OptionMappedModel], + isVisible: (_type: string, element: MetadataType) => { + return !!element.optionSet?.id; + }, + }, + programStages: { + key: "programStages", + label: i18n.t("Program Stages"), + component: (props: MappingTableProps) => , + models: [ProgramStageMappedModel], + isVisible: (type: string, element: MetadataType) => { + return type === "programs" && element.programType === "WITH_REGISTRATION"; + }, + }, + }; + + const modelSteps: Dictionary = { + aggregatedDataElements: [availableSteps.categoryOptions, availableSteps.options], + programDataElements: [availableSteps.options], + eventPrograms: [availableSteps.categoryOptions, availableSteps.programStages], + }; + + return modelSteps[type] ?? []; +}; diff --git a/src/presentation/react/core/components/metadata-drop-zone/MetadataDropZone.tsx b/src/presentation/react/core/components/metadata-drop-zone/MetadataDropZone.tsx new file mode 100644 index 000000000..59d5902a9 --- /dev/null +++ b/src/presentation/react/core/components/metadata-drop-zone/MetadataDropZone.tsx @@ -0,0 +1,92 @@ +import { useSnackbar } from "d2-ui-components"; +import React, { useState } from "react"; +import Dropzone from "react-dropzone"; +import i18n from "../../../../../locales"; +import CloudUploadIcon from "@material-ui/icons/CloudUpload"; +import CloudDoneIcon from "@material-ui/icons/CloudDone"; +import { makeStyles } from "@material-ui/core"; +import { MetadataPackage } from "../../../../../domain/metadata/entities/MetadataEntities"; + +interface MetadataDropZoneProps { + onChange: (fileName: string, metadataPackage: MetadataPackage) => void; +} + +const MetadataDropZone: React.FC = ({ onChange }) => { + const classes = useStyles(); + const [file, setFile] = useState(); + const snackbar = useSnackbar(); + + const onDrop = async (files: File[]) => { + const file = files[0]; + if (!file) { + snackbar.error(i18n.t("Cannot read file")); + return; + } + + const contentsFile = await file.text(); + const contentsJson = JSON.parse(contentsFile); + delete contentsJson.date; + delete contentsJson.package; + + onChange(file.name, contentsJson as MetadataPackage); + setFile(file); + }; + + return ( + + {({ getRootProps, getInputProps }) => ( +
+
+ + + +
+
+ )} +
+ ); +}; + +export default MetadataDropZone; + +const useStyles = makeStyles({ + dropzoneTextStyle: { textAlign: "center", top: "15%", position: "relative" }, + dropzoneParagraph: { fontSize: 20 }, + uploadIconSize: { width: 50, height: 50, color: "#909090" }, + dropzone: { + position: "relative", + width: "100%", + height: 270, + backgroundColor: "#f0f0f0", + border: "dashed", + borderColor: "#c8c8c8", + cursor: "pointer", + }, + stripes: { + width: "100%", + height: 270, + cursor: "pointer", + border: "solid", + borderColor: "#c8c8c8", + "-webkit-animation": "progress 2s linear infinite !important", + "-moz-animation": "progress 2s linear infinite !important", + animation: "progress 2s linear infinite !important", + backgroundSize: "150% 100%", + }, +}); diff --git a/src/presentation/react/core/components/metadata-table/MetadataTable.tsx b/src/presentation/react/core/components/metadata-table/MetadataTable.tsx new file mode 100644 index 000000000..335dcc75b --- /dev/null +++ b/src/presentation/react/core/components/metadata-table/MetadataTable.tsx @@ -0,0 +1,596 @@ +import { Checkbox, FormControlLabel, Icon, makeStyles } from "@material-ui/core"; +import DoneAllIcon from "@material-ui/icons/DoneAll"; +import { isCancel } from "d2-api"; +import { + DatePicker, + ObjectsTable, + ObjectsTableDetailField, + ObjectsTableProps, + OrgUnitsSelector, + ReferenceObject, + TableAction, + TableColumn, + TablePagination, + TableSelection, + TableState, + useSnackbar, +} from "d2-ui-components"; +import _ from "lodash"; +import React, { ChangeEvent, ReactNode, useCallback, useEffect, useState } from "react"; +import { NamedRef } from "../../../../../domain/common/entities/Ref"; +import { + DataSource, + isDhisInstance, + isJSONDataSource, +} from "../../../../../domain/instance/entities/DataSource"; +import { MetadataResponsible } from "../../../../../domain/metadata/entities/MetadataResponsible"; +import { ListMetadataParams } from "../../../../../domain/metadata/repositories/MetadataRepository"; +import i18n from "../../../../../locales"; +import { D2Model } from "../../../../../models/dhis/default"; +import { DataElementModel } from "../../../../../models/dhis/metadata"; +import { MetadataType } from "../../../../../utils/d2"; +import { useAppContext } from "../../contexts/AppContext"; +import Dropdown from "../dropdown/Dropdown"; +import { ResponsibleDialog } from "../responsible-dialog/ResponsibleDialog"; +import { getFilterData, getOrgUnitSubtree } from "./utils"; + +export type MetadataTableFilters = "group" | "level" | "orgUnit" | "lastUpdated" | "onlySelected"; + +export interface MetadataTableProps + extends Omit, "rows" | "columns"> { + remoteInstance?: DataSource; + filterRows?: string[]; + transformRows?: (rows: MetadataType[]) => MetadataType[]; + models: typeof D2Model[]; + selectedIds?: string[]; + excludedIds?: string[]; + childrenKeys?: string[]; + initialShowOnlySelected?: boolean; + additionalColumns?: TableColumn[]; + additionalActions?: TableAction[]; + showIndeterminateSelection?: boolean; + notifyNewSelection?(selectedIds: string[], excludedIds: string[]): void; + notifyNewModel?(model: typeof D2Model): void; + notifyRowsChange?(rows: MetadataType[]): void; + allowChangingResponsible?: boolean; + showResponsible?: boolean; + externalFilterComponents?: ReactNode; + viewFilters?: MetadataTableFilters[]; +} + +const useStyles = makeStyles({ + checkbox: { + paddingLeft: 10, + marginTop: 8, + }, + orgUnitFilter: { + order: -1, + marginRight: "1rem", + }, + metadataFilter: { + order: 1, + }, + dateFilter: { + order: 2, + }, + groupFilter: { + order: 3, + }, + levelFilter: { + order: 4, + }, + onlySelectedFilter: { + order: 5, + }, +}); + +const initialState = { + sorting: { + field: "displayName" as const, + order: "asc" as const, + }, + pagination: { + page: 1, + pageSize: 25, + }, +}; + +const uniqCombine = (items: any[]) => { + return _(items).compact().reverse().uniqBy("name").reverse().value(); +}; + +const MetadataTable: React.FC = ({ + remoteInstance, + filterRows, + transformRows = rows => rows, + models, + selectedIds = [], + excludedIds = [], + notifyNewSelection = _.noop, + notifyNewModel = _.noop, + notifyRowsChange = _.noop, + childrenKeys = [], + additionalColumns = [], + additionalActions = [], + loading: providedLoading, + initialShowOnlySelected = false, + showIndeterminateSelection = false, + allowChangingResponsible = false, + showResponsible = true, + externalFilterComponents, + viewFilters = ["group", "level", "orgUnit", "lastUpdated", "onlySelected"], + ...rest +}) => { + const { compositionRoot, api: defaultApi } = useAppContext(); + const classes = useStyles(); + + const snackbar = useSnackbar(); + + const [model, updateModel] = useState(() => models[0] ?? DataElementModel); + const [ids, updateIds] = useState([]); + const [responsibles, updateResponsibles] = useState([]); + const [sharingSettingsElement, setSharingSettingsElement] = useState(); + + const [selectedRows, setSelectedRows] = useState(selectedIds); + const [filters, setFilters] = useState({ + type: model.getCollectionName(), + showOnlySelected: initialShowOnlySelected, + order: initialState.sorting, + page: initialState.pagination.page, + pageSize: initialState.pagination.pageSize, + }); + + const updateFilters = useCallback( + (partialFilters: Partial) => { + setFilters(state => ({ ...state, page: 1, ...partialFilters })); + }, + [setFilters] + ); + + const api = + remoteInstance && isDhisInstance(remoteInstance) + ? compositionRoot.instances.getApi(remoteInstance) + : defaultApi; + + const [expandOrgUnits, updateExpandOrgUnits] = useState(); + const [groupFilterData, setGroupFilterData] = useState([]); + const [levelFilterData, setLevelFilterData] = useState([]); + + const [rows, setRows] = useState([]); + const [pager, setPager] = useState>({}); + const [loading, setLoading] = useState(true); + + const showResponsibles = + showResponsible && + (model.getCollectionName() === "dataSets" || model.getCollectionName() === "programs"); + + const changeModelFilter = (modelName: string) => { + if (models.length === 0) throw new Error("You need to provide at least one model"); + const model = _.find(models, model => model.getMetadataType() === modelName) ?? models[0]; + updateModel(() => model); + notifyNewModel(model); + updateFilters({ type: model.getCollectionName() }); + }; + + const changeSearchFilter = (value: string) => { + const hasSearch = value.trim() !== ""; + const { field, operator } = model.getSearchFilter(); + updateFilters({ + search: hasSearch ? { field, operator, value } : undefined, + }); + }; + + const changeLastUpdatedFilter = (date: Date | null) => { + updateFilters({ lastUpdated: date ?? undefined }); + }; + + const changeGroupFilter = (value: string) => { + updateFilters({ + group: { type: model.getGroupFilterName(), value }, + }); + }; + + const changeLevelFilter = (level: string) => { + updateFilters({ level, parents: [] }); + }; + + const changeOnlySelectedFilter = (event: ChangeEvent) => { + const showOnlySelected = event.target?.checked; + updateFilters({ + selectedIds: showOnlySelected ? selectedRows : undefined, + showOnlySelected, + }); + }; + + const changeParentOrgUnitFilter = useCallback( + (parents: string[]) => { + updateFilters({ parents, level: "" }); + }, + [updateFilters] + ); + + const selectOrgUnitChildren = async (selectedOUs: string[]) => { + const ids = new Set(); + for (const selectedOU of selectedOUs) { + const subtree = await getOrgUnitSubtree(api, selectedOU); + subtree.forEach(id => ids.add(id)); + } + const includedIds = _.uniq([...selectedIds, ...Array.from(ids)]); + notifyNewSelection(includedIds, excludedIds); + + const orgUnitPaths = _(rows) + .intersectionBy( + selectedOUs.map(id => ({ id })), + "id" + ) + .map(({ path }) => path) + .compact() + .value(); + updateExpandOrgUnits(orgUnitPaths); + changeParentOrgUnitFilter(orgUnitPaths); + }; + + const addToSelection = (ids: string[]) => { + const oldSelection = _.difference(selectedIds, ids); + const newSelection = _.difference(ids, selectedIds); + + notifyNewSelection([...oldSelection, ...newSelection], excludedIds); + }; + + const openResponsibleDialog = (ids: string[]) => { + const { id, name } = rows.find(({ id }) => ids[0] === id) ?? {}; + if (!id || !name) return; + + setSharingSettingsElement({ id, name }); + }; + + const filterComponents = ( + + {externalFilterComponents} + + {models.length > 1 && ( +
+ ({ + id: model.getMetadataType(), + name: model.getModelName(), + }))} + onValueChange={changeModelFilter} + value={model.getMetadataType()} + label={i18n.t("Metadata type")} + hideEmpty={true} + /> +
+ )} + + {viewFilters.includes("lastUpdated") && ( +
+ +
+ )} + + {viewFilters.includes("group") && model.getGroupFilterName() && ( +
+ +
+ )} + + {viewFilters.includes("level") && model.getLevelFilterName() && ( +
+ +
+ )} + + {viewFilters.includes("onlySelected") && ( +
+ + } + label={i18n.t("Only selected items")} + /> +
+ )} +
+ ); + + const orgUnitTreeFilter = viewFilters.includes("orgUnit") && + model.getCollectionName() === "organisationUnits" && ( +
+ +
+ ); + + const handleError = useCallback( + (error: Error) => { + if (!isCancel(error)) { + snackbar.error(error.message); + setRows([]); + setPager({}); + setLoading(false); + } + }, + [snackbar] + ); + + const tableActions = [ + { + name: "details", + text: i18n.t("Details"), + multiple: false, + type: "details", + }, + { + name: "select-children", + text: i18n.t("Select with children subtree"), + multiple: true, + onClick: selectOrgUnitChildren, + icon: , + isActive: () => { + return model.getMetadataType() === "organisationUnit"; + }, + }, + { + name: "select", + text: i18n.t("Select"), + primary: true, + multiple: true, + onClick: addToSelection, + isActive: () => false, + }, + { + name: "set-responsible", + text: i18n.t("Set metadata custodian"), + multiple: false, + icon: supervisor_account, + onClick: openResponsibleDialog, + isActive: () => { + return allowChangingResponsible && !remoteInstance && showResponsibles; + }, + }, + ]; + + useEffect(() => { + updateFilters({ + page: initialState.pagination.page, + }); + }, [updateFilters, remoteInstance]); + + useEffect(() => { + if (model.getCollectionName() === "organisationUnits") return; + if (remoteInstance && isJSONDataSource(remoteInstance)) return; + + compositionRoot.metadata + .listAll({ ...filters, filterRows, fields: { id: true } }, remoteInstance) + .then(objects => { + updateIds(objects.map(({ id }) => id)); + }); + }, [filters, filterRows, model, compositionRoot, remoteInstance]); + + useEffect(() => { + if (model.getCollectionName() !== "organisationUnits") return; + if (remoteInstance && isJSONDataSource(remoteInstance)) { + changeParentOrgUnitFilter([]); + return; + } + + compositionRoot.instances + .getOrgUnitRoots(remoteInstance) + .then(roots => changeParentOrgUnitFilter(roots.map(({ path }) => path))) + .catch(handleError); + }, [compositionRoot, remoteInstance, model, handleError, changeParentOrgUnitFilter]); + + useEffect(() => { + if (model.getCollectionName() === "organisationUnits" && !filters.parents) return; + const fields = model.getFields(); + const includeParents = model.getCollectionName() === "organisationUnits"; + + setLoading(true); + compositionRoot.metadata + .list({ ...filters, filterRows, fields, includeParents }, remoteInstance) + .then(({ objects, pager }) => { + const rows = model.getApiModelTransform()((objects as unknown) as MetadataType[]); + notifyRowsChange(rows); + + setRows(rows); + setPager(pager); + setLoading(false); + }) + .catch(handleError); + }, [ + compositionRoot, + notifyRowsChange, + remoteInstance, + filters, + filterRows, + model, + handleError, + ]); + + useEffect(() => { + if (model && model.getGroupFilterName()) { + getFilterData( + model.getGroupFilterName(), + "group", + api.apiPath, + api + ).then(({ objects }) => setGroupFilterData(objects)); + } + + if (model && model.getLevelFilterName()) { + getFilterData(model.getLevelFilterName(), "level", api.apiPath, api).then( + ({ objects }) => { + setLevelFilterData( + objects.map(({ name, level }) => ({ + id: String(level), + name: `${level}. ${name}`, + })) + ); + } + ); + } + }, [api, model]); + + useEffect(() => { + if (remoteInstance && isJSONDataSource(remoteInstance)) return; + + compositionRoot.responsibles.list(remoteInstance).then(updateResponsibles); + }, [compositionRoot, remoteInstance]); + + const handleTableChange = (tableState: TableState) => { + const { sorting, pagination, selection } = tableState; + + const included = _.reject(selection, { indeterminate: true }).map(({ id }) => id); + const newlySelectedIds = _.difference(included, selectedIds); + const newlyUnselectedIds = _.difference(selectedIds, included); + + const parseChildren = (ids: string[]) => + _(rows) + .filter(({ id }) => !!ids.includes(id)) + .map(row => (_.values(_.pick(row, childrenKeys)) as unknown) as MetadataType) + .flattenDeep() + .map(({ id }) => id) + .value(); + + const excluded = _(excludedIds) + .union(newlyUnselectedIds) + .difference(parseChildren(newlyUnselectedIds)) + .difference(newlySelectedIds) + .difference(parseChildren(newlySelectedIds)) + .filter(id => !_.find(rows, { id })) + .value(); + + notifyNewSelection(included, excluded); + setSelectedRows(included); + updateFilters({ + order: sorting, + page: pagination.page, + pageSize: pagination.pageSize, + }); + }; + + const exclusion = excludedIds.map(id => ({ id })); + const selection = selectedIds.map(id => ({ + id, + checked: true, + indeterminate: false, + })); + + const childrenSelection: TableSelection[] = showIndeterminateSelection + ? _(rows) + .intersectionBy(selection, "id") + .map(row => (_.values(_.pick(row, childrenKeys)) as unknown) as MetadataType[]) + .flattenDeep() + .differenceBy(selection, "id") + .differenceBy(exclusion, "id") + .map(({ id }) => { + return { + id, + checked: true, + indeterminate: !_.find(selection, { id }), + }; + }) + .value() + : []; + + const responsibleField = showResponsibles + ? { + name: "responsible", + text: i18n.t("Custodian"), + getValue: (row: MetadataType) => { + const { users = [], userGroups = [] } = + responsibles.find(({ id }) => row.id === id) ?? {}; + + const results = [...users, ...userGroups].map(({ name }) => name); + return results.length === 0 ? "-" : results.join(", "); + }, + } + : undefined; + + const columns: TableColumn[] = uniqCombine([ + ...model.getColumns(), + ...additionalColumns, + { ...responsibleField, sortable: false }, + ]); + + const details: ObjectsTableDetailField[] = uniqCombine([ + ...model.getDetails(), + responsibleField, + ]); + + const actions: TableAction[] = uniqCombine([ + ...tableActions, + ...additionalActions, + ]); + + return ( + + setSharingSettingsElement(undefined)} + /> + + + rows={transformRows(rows)} + columns={columns} + details={details} + onChangeSearch={changeSearchFilter} + initialState={initialState} + searchBoxLabel={i18n.t(`Search by `) + model.getSearchFilter().field} + pagination={pager} + onChange={handleTableChange} + ids={ids} + loading={providedLoading || loading} + selection={[...selection, ...childrenSelection]} + childrenKeys={childrenKeys} + filterComponents={filterComponents} + forceSelectionColumn={true} + actions={actions} + sideComponents={orgUnitTreeFilter} + {...rest} + /> + + ); +}; + +export default MetadataTable; diff --git a/src/presentation/react/core/components/metadata-table/utils.tsx b/src/presentation/react/core/components/metadata-table/utils.tsx new file mode 100644 index 000000000..7e0777301 --- /dev/null +++ b/src/presentation/react/core/components/metadata-table/utils.tsx @@ -0,0 +1,38 @@ +import memoize from "nano-memoize"; +import { MetadataEntities } from "../../../../../domain/metadata/entities/MetadataEntities"; +import { modelFactory } from "../../../../../models/dhis/factory"; +import { D2Api } from "../../../../../types/d2-api"; + +/** + * Load memoized filter data from an instance (This should be removed with a cache on d2-api) + * Note: _baseUrl is used as cacheKey to avoid memoizing values between instances + */ +export const getFilterData = memoize( + (modelName: keyof MetadataEntities, type: "group" | "level", _baseUrl: string, api: D2Api) => + modelFactory(modelName) + .getApiModel(api) + .get({ + paging: false, + fields: + type === "group" + ? { + id: true as const, + name: true as const, + } + : { + name: true as const, + level: true as const, + }, + order: type === "group" ? undefined : `level:iasc`, + }) + .getData(), + { maxArgs: 3 } +); + +export async function getOrgUnitSubtree(api: D2Api, orgUnitId: string): Promise { + const { organisationUnits } = (await api + .get(`/organisationUnits/${orgUnitId}`, { fields: "id", includeDescendants: true }) + .getData()) as { organisationUnits: { id: string }[] }; + + return organisationUnits.map(({ id }) => id); +} diff --git a/src/presentation/react/core/components/migrations/Migrations.tsx b/src/presentation/react/core/components/migrations/Migrations.tsx new file mode 100644 index 000000000..f21438785 --- /dev/null +++ b/src/presentation/react/core/components/migrations/Migrations.tsx @@ -0,0 +1,148 @@ +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useCallback, useEffect, useState } from "react"; +import i18n from "../../../../../locales"; +import { MigrationsRunner } from "../../../../../migrations"; + +export interface MigrationsProps { + runner: MigrationsRunner; + onFinish: () => void; +} + +type State = + | { type: "show-info" } + | { type: "app-out-of-date" } + | { type: "migrating" } + | { type: "success" }; + +const Migrations: React.FC = props => { + const { runner, onFinish } = props; + const [messages, setMessages] = useState([]); + const [state, setState] = useState(getInitialState(runner)); + useEffect(followContents, [messages]); + + const debug = useCallback((message: string) => { + setMessages(messages => [...messages, message]); + }, []); + + const startMigration = useCallback(() => { + runMigrations(runner, debug, setState).then(setState); + }, [runner, debug]); + + const actionText = getActionText(state); + + if (state.type === "app-out-of-date") { + return ; + } + + return ( + (state.type === "success" ? onFinish() : startMigration())} + saveText={actionText} + onCancel={undefined} + disableSave={state.type === "migrating" || !actionText} + maxWidth="md" + fullWidth={true} + > +
+

{getPendingMigrationsText(runner)}

+ +

+ {messages.map((msg, idx) => ( + + {msg} +
+
+ ))} +

+ +

+ {state.type === "success" && + i18n.t("Migrations finished successfully, you may now continue to the app")} +

+
+
+ ); +}; + +function runMigrations( + runner: MigrationsRunner, + debug: (message: string) => void, + setState: React.Dispatch> +): Promise { + setState({ type: "migrating" }); + + return runner + .setDebug(debug) + .execute() + .then(() => ({ type: "success" as const })) + .catch(() => { + debug("---"); + debug( + i18n.t( + "There has been an error. You can either retry or contact your administrator if you think there has been an un recoverable error" + ) + ); + return { type: "show-info" as const }; + }); +} + +function followContents() { + const contentsEl = document.getElementById("migrations-contents"); + const divEl = contentsEl ? contentsEl.parentElement : null; + if (divEl) divEl.scrollTop = divEl.scrollHeight; +} + +function getActionText(state: State): string | undefined { + switch (state.type) { + case "show-info": + return i18n.t("Migrate instance"); + case "migrating": + return i18n.t("Migrating..."); + case "success": + return i18n.t("Continue to the App"); + case "app-out-of-date": + return; + } +} + +function getInitialState(runner: MigrationsRunner): State { + if (runner.instanceVersion === runner.appVersion) { + return { type: "success" }; + } else if (runner.instanceVersion > runner.appVersion) { + return { type: "app-out-of-date" }; + } else { + return { type: "show-info" }; + } +} + +function getPendingMigrationsText(runner: MigrationsRunner): string { + return i18n.t( + "The app needs to run all pending migrations (v{{instanceVersion}} -> v{{appVersion}}) in order to continue. This may take a long time, make sure the process is not interrupted.", + runner + ); +} + +const isDebug = process.env.NODE_ENV === "development"; + +const MigrationsError: React.FC<{ runner: MigrationsRunner; onFinish: () => void }> = ({ + runner, + onFinish, +}) => ( + + {i18n.t( + "The database version (v{{instanceVersion}}) is greater than the app version (v{{appVersion}}), we cannot continue. Please contact the administrator to update the app.", + runner + )} + +); + +export default Migrations; diff --git a/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx b/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx new file mode 100644 index 000000000..e77ea7994 --- /dev/null +++ b/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx @@ -0,0 +1,556 @@ +import { Icon } from "@material-ui/core"; +import { + ConfirmationDialog, + ConfirmationDialogProps, + MetaObject, + ObjectsTable, + ObjectsTableDetailField, + SearchResult, + ShareUpdate, + TableAction, + TableColumn, + TableSelection, + TableState, + useLoading, + useSnackbar, +} from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import { Package } from "../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../locales"; +import { promiseMap } from "../../../../../utils/common"; +import { getUserInfo, isGlobalAdmin, UserInfo } from "../../../../../utils/permissions"; +import Dropdown from "../dropdown/Dropdown"; +import { + PullRequestCreation, + PullRequestCreationDialog, +} from "../pull-request-creation-dialog/PullRequestCreationDialog"; +import { SharingDialog } from "../sharing-dialog/SharingDialog"; +import { ModulePackageListPageProps } from "../../../../webapp/core/pages/module-package-list/ModulePackageListPage"; +import { useAppContext } from "../../contexts/AppContext"; +import { NewPackageDialog } from "./NewPackageDialog"; +import { getValidationsByVersionFeedback } from "./utils"; + +export const ModulesListTable: React.FC = ({ + remoteInstance, + onActionButtonClick, + presentation = "app", + externalComponents, + openSyncSummary = _.noop, + paginationOptions, +}) => { + const { compositionRoot, api } = useAppContext(); + const snackbar = useSnackbar(); + const loading = useLoading(); + const history = useHistory(); + + const [rows, setRows] = useState([]); + const [selection, updateSelection] = useState([]); + + const [resetKey, setResetKey] = useState(Math.random()); + const [isTableLoading, setIsTableLoading] = useState(false); + const [newPackageModule, setNewPackageModule] = useState(); + const [sharingSettingsObject, setSharingSettingsObject] = useState(null); + const [pullRequestProps, setPullRequestProps] = useState(); + const [dialogProps, updateDialog] = useState(null); + const [departmentFilter, setDepartmentFilter] = useState(""); + + const [globalAdmin, setGlobalAdmin] = useState(false); + const [userInfo, setUserInfo] = useState(); + + const editModule = useCallback( + (ids: string[]) => { + const item = _.find(rows, ({ id }) => id === ids[0]); + if (!item) snackbar.error(i18n.t("Invalid module")); + else history.push({ pathname: `/modules/edit/${item.id}`, state: { module: item } }); + }, + [rows, history, snackbar] + ); + + const downloadSnapshot = useCallback( + async (ids: string[]) => { + const module = _.find(rows, ({ id }) => id === ids[0]); + if (!module) snackbar.error(i18n.t("Invalid module")); + else { + loading.show(true, i18n.t("Downloading snapshot for module {{name}}", module)); + + const originInstance = remoteInstance?.id ?? "LOCAL"; + const contents = await compositionRoot.sync[module.type]({ + ...module.toSyncBuilder(), + originInstance, + targetInstances: [], + }).buildPayload(); + + await compositionRoot.modules.download(module, contents); + loading.reset(); + } + }, + [compositionRoot, remoteInstance, rows, snackbar, loading] + ); + + const createPackage = useCallback( + async (ids: string[]) => { + const module = _.find(rows, ({ id }) => id === ids[0]); + if (!module) snackbar.error(i18n.t("Invalid module")); + else setNewPackageModule(module); + }, + [rows, snackbar] + ); + + const savePackage = useCallback( + async (item: Package, versions: string[]) => { + setNewPackageModule(undefined); + const module = _.find(rows, ({ id }) => id === item.module.id); + if (!module) snackbar.error(i18n.t("Invalid module")); + else { + const validationsByVersion = _.fromPairs( + await promiseMap(versions, async dhisVersion => { + loading.show( + true, + i18n.t("Creating {{dhisVersion}} package for module {{name}}", { + name: module.name, + dhisVersion, + }) + ); + + const validations = await compositionRoot.packages.create( + remoteInstance?.id ?? "LOCAL", + item, + module, + dhisVersion + ); + + return [dhisVersion, validations]; + }) + ); + + const [level, msg] = getValidationsByVersionFeedback(module, validationsByVersion); + snackbar.openSnackbar(level, msg); + + loading.reset(); + setResetKey(Math.random()); + } + }, + [compositionRoot, remoteInstance, rows, snackbar, loading] + ); + + const pullModule = useCallback( + async (ids: string[]) => { + const module = _.find(rows, ({ id }) => id === ids[0]); + if (!module) snackbar.error(i18n.t("Invalid module")); + else { + loading.show(true, i18n.t("Pulling metadata from module {{name}}", module)); + + const originInstance = remoteInstance?.id ?? "LOCAL"; + const builder = { + ...module.toSyncBuilder(), + originInstance, + targetInstances: ["LOCAL"], + }; + + const result = await compositionRoot.sync.prepare(module.type, builder); + const sync = compositionRoot.sync[module.type](builder); + + const createPullRequest = () => { + if (!remoteInstance) { + snackbar.error(i18n.t("Unable to create pull request")); + } else { + setPullRequestProps({ + instance: remoteInstance, + builder, + type: module.type, + }); + } + }; + + const synchronize = async () => { + for await (const { message, syncReport, done } of sync.execute()) { + if (message) loading.show(true, message); + if (syncReport) await syncReport.save(api); + if (done) { + openSyncSummary(syncReport); + return; + } + } + }; + + await result.match({ + success: async () => { + await synchronize(); + }, + error: async code => { + switch (code) { + case "PULL_REQUEST": + createPullRequest(); + break; + case "PULL_REQUEST_RESPONSIBLE": + updateDialog({ + title: i18n.t("Pull metadata"), + description: i18n.t( + "You are one of the reponsibles for the selected items.\nDo you want to directly pull the metadata?" + ), + onCancel: () => { + updateDialog(null); + }, + onSave: async () => { + updateDialog(null); + await synchronize(); + }, + onInfoAction: () => { + updateDialog(null); + createPullRequest(); + }, + cancelText: i18n.t("Cancel"), + saveText: i18n.t("Proceed"), + infoActionText: i18n.t("Create pull request"), + }); + break; + case "INSTANCE_NOT_FOUND": + snackbar.warning(i18n.t("Couldn't connect with instance")); + break; + default: + snackbar.error(i18n.t("Unknown synchronization error")); + } + }, + }); + + loading.reset(); + } + }, + [compositionRoot, openSyncSummary, remoteInstance, loading, rows, snackbar, api] + ); + + const replicateModule = useCallback( + async (ids: string[]) => { + const item = _.find(rows, ({ id }) => id === ids[0]); + if (!item) { + snackbar.error(i18n.t("Invalid module")); + return; + } + + history.push({ + pathname: `/modules/new`, + state: { module: item.replicate() }, + }); + }, + [history, rows, snackbar] + ); + + const deleteModule = useCallback( + async (ids: string[]) => { + loading.show(true, "Deleting modules"); + for (const id of ids) { + await compositionRoot.modules.delete(id); + } + loading.reset(); + setResetKey(Math.random()); + updateSelection([]); + }, + [compositionRoot, loading] + ); + + const openSharingSettings = useCallback( + async (ids: string[]) => { + const module = _.find(rows, ({ id }) => id === ids[0]); + if (!module) { + snackbar.error(i18n.t("Invalid module")); + return; + } + + setSharingSettingsObject({ + object: module, + meta: { allowPublicAccess: true, allowExternalAccess: false }, + }); + }, + [rows, snackbar] + ); + + const updateTable = useCallback( + ({ selection }: TableState) => { + updateSelection(selection); + }, + [updateSelection] + ); + + const verifyUserHasWritePermissions = useCallback( + (modules: Module[]) => { + if (globalAdmin) return true; + + for (const module of modules) { + if (!!userInfo && !module.hasPermissions("write", userInfo.id, userInfo.userGroups)) + return false; + } + + return true; + }, + [globalAdmin, userInfo] + ); + + const columns: TableColumn[] = useMemo( + () => [ + { name: "name", text: i18n.t("Name"), sortable: true }, + { + name: "department", + text: i18n.t("Department"), + sortable: true, + getValue: ({ department }) => { + return department.name; + }, + }, + { name: "description", text: i18n.t("Description"), sortable: true, hidden: true }, + { + name: "metadataIds", + text: "Selected metadata", + getValue: module => `${module.metadataIds.length} elements`, + }, + { name: "lastUpdated", text: i18n.t("Last updated"), hidden: true }, + { name: "lastUpdatedBy", text: i18n.t("Last updated by"), hidden: true }, + { name: "created", text: i18n.t("Created"), hidden: true }, + { name: "user", text: i18n.t("Created by"), hidden: true }, + ], + [] + ); + + const details: ObjectsTableDetailField[] = useMemo( + () => [ + { name: "name", text: i18n.t("Name") }, + { + name: "department", + text: i18n.t("Department"), + getValue: ({ department }) => { + return department.name; + }, + }, + { name: "description", text: i18n.t("Description") }, + { + name: "metadataIds", + text: i18n.t("Selected metadata"), + getValue: module => `${module.metadataIds.length} elements`, + }, + { name: "lastUpdated", text: i18n.t("Last updated") }, + { name: "lastUpdatedBy", text: i18n.t("Last updated by") }, + { name: "created", text: i18n.t("Created") }, + { name: "user", text: i18n.t("Created by") }, + ], + [] + ); + + const actions: TableAction[] = useMemo( + () => [ + { + name: "details", + text: i18n.t("Details"), + multiple: false, + primary: presentation !== "app" && !remoteInstance, + }, + { + name: "edit", + text: i18n.t("Edit"), + multiple: false, + isActive: modules => + presentation === "app" && + !remoteInstance && + verifyUserHasWritePermissions(modules), + onClick: editModule, + primary: presentation === "app" && !remoteInstance, + icon: edit, + }, + { + name: "delete", + text: i18n.t("Delete"), + multiple: true, + isActive: modules => + presentation === "app" && + !remoteInstance && + verifyUserHasWritePermissions(modules), + onClick: deleteModule, + icon: delete, + }, + { + name: "replicate", + text: i18n.t("Replicate"), + multiple: false, + onClick: replicateModule, + icon: content_copy, + isActive: () => presentation === "app" && !remoteInstance, + }, + { + name: "download", + text: i18n.t("Download metadata package"), + multiple: false, + onClick: downloadSnapshot, + icon: cloud_download, + }, + { + name: "package-data-store", + text: i18n.t("Generate package from module"), + multiple: false, + icon: description, + isActive: () => presentation === "app" && !remoteInstance, + onClick: createPackage, + }, + { + name: "pull-metadata", + text: i18n.t("Pull metadata"), + multiple: false, + icon: arrow_downward, + isActive: () => presentation === "app" && !!remoteInstance, + onClick: pullModule, + }, + { + name: "sharingSettings", + text: i18n.t("Sharing settings"), + multiple: false, + isActive: verifyUserHasWritePermissions, + onClick: openSharingSettings, + icon: share, + }, + ], + [ + createPackage, + deleteModule, + downloadSnapshot, + editModule, + openSharingSettings, + presentation, + pullModule, + remoteInstance, + replicateModule, + verifyUserHasWritePermissions, + ] + ); + + const departmentFilterItems = useMemo(() => { + return _(rows) + .map(({ department }) => department) + .uniqBy(({ id }) => id) + .sortBy(({ name }) => name) + .value(); + }, [rows]); + + const filterComponents = useMemo(() => { + const departmentFilterComponent = ( + + ); + + return [externalComponents, departmentFilterComponent]; + }, [externalComponents, departmentFilter, departmentFilterItems]); + + const rowsFiltered = useMemo(() => { + return departmentFilter + ? rows.filter(({ department }) => department.id === departmentFilter) + : rows; + }, [departmentFilter, rows]); + + const onSearchRequest = useCallback( + async (key: string) => + api + .get("/sharing/search", { key }) + .getData(), + [api] + ); + + const onSharingChanged = useCallback( + async (updatedAttributes: ShareUpdate) => { + if (!sharingSettingsObject) return; + + const module = (sharingSettingsObject.object as Module).update(updatedAttributes); + + await compositionRoot.modules.save(module); + setSharingSettingsObject({ + meta: sharingSettingsObject.meta, + object: module, + }); + }, + [sharingSettingsObject, compositionRoot] + ); + + const closeSharingSettingsDialog = useCallback(() => { + setSharingSettingsObject(null); + setResetKey(Math.random()); + }, []); + + useEffect(() => { + setIsTableLoading(true); + compositionRoot.modules + .list(globalAdmin, remoteInstance) + .then(rows => { + setRows(rows); + setIsTableLoading(false); + }) + .catch((error: Error) => { + snackbar.error(error.message); + setRows([]); + setIsTableLoading(false); + }); + }, [compositionRoot, remoteInstance, resetKey, snackbar, setIsTableLoading, globalAdmin]); + + useEffect(() => { + setDepartmentFilter(""); + }, [remoteInstance]); + + useEffect(() => { + isGlobalAdmin(api).then(setGlobalAdmin); + getUserInfo(api).then(setUserInfo); + }, [api]); + + return ( + + + rows={rowsFiltered} + loading={isTableLoading} + columns={columns} + details={details} + actions={actions} + onActionButtonClick={onActionButtonClick} + forceSelectionColumn={presentation === "app"} + filterComponents={filterComponents} + selection={selection} + onChange={updateTable} + paginationOptions={paginationOptions} + /> + + {!!newPackageModule && ( + setNewPackageModule(undefined)} + module={newPackageModule} + /> + )} + + {!!pullRequestProps && ( + setPullRequestProps(undefined)} + /> + )} + + {!!sharingSettingsObject && ( + + )} + + {dialogProps && } + + ); +}; diff --git a/src/presentation/react/core/components/module-list-table/NewPackageDialog.tsx b/src/presentation/react/core/components/module-list-table/NewPackageDialog.tsx new file mode 100644 index 000000000..0117175cf --- /dev/null +++ b/src/presentation/react/core/components/module-list-table/NewPackageDialog.tsx @@ -0,0 +1,168 @@ +import { makeStyles, TextField } from "@material-ui/core"; +import Autocomplete from "@material-ui/lab/Autocomplete"; +import { ConfirmationDialog } from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useEffect, useState } from "react"; +import semver from "semver"; +import { ValidationError } from "../../../../../domain/common/entities/Validations"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import { Package } from "../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../locales"; +import { Dictionary } from "../../../../../types/utils"; +import { useAppContext } from "../../contexts/AppContext"; + +export const NewPackageDialog: React.FC = ({ module, save, close }) => { + const { compositionRoot } = useAppContext(); + const classes = useStyles(); + + const [versions, updateVersions] = useState([]); + const [item, updateItem] = useState( + Package.build({ + name: i18n.t("Package of {{name}}", module), + module, + version: + semver.parse(module.lastPackageVersion.split("-")[0])?.inc("patch").format() ?? + "1.0.0", + }) + ); + + const [errors, setErrors] = useState>({}); + + const updateModel = useCallback( + (field: keyof Package, value: string) => { + const newPackage = item.update({ [field]: value }); + const errors = _.keyBy(newPackage.validate([field], module), "property"); + + setErrors(errors); + updateItem(newPackage); + }, + [item, module] + ); + + const onChangeField = useCallback( + (field: keyof Package) => { + return (event: React.ChangeEvent<{ value: unknown }>) => { + updateModel(field, event.target.value as string); + }; + }, + [updateModel] + ); + + const updateVersionNumber = useCallback( + (event: React.ChangeEvent<{ value: unknown }>) => { + const revision = event.target.value as string; + const tag = item.version.split("-")[1]; + const newVersion = [revision, tag].join("-"); + updateModel("version", newVersion); + }, + [item, updateModel] + ); + + const updateVersionTag = useCallback( + (event: React.ChangeEvent<{ value: unknown }>) => { + const revision = item.version.split("-")[0]; + const tag = event.target.value ? (event.target.value as string) : undefined; + const newVersion = semver.parse([revision, tag].join("-"))?.format(); + updateModel("version", newVersion ?? revision); + }, + [item, updateModel] + ); + + const onSave = useCallback(() => { + const errors = item.validate(undefined, module); + const messages = _.keyBy(errors, "property"); + + if (errors.length === 0) save(item, versions); + else setErrors(messages); + }, [item, save, module, versions]); + + useEffect(() => { + compositionRoot.instances.getVersion().then(version => { + if (versions.length === 0) updateVersions([version]); + }); + }, [compositionRoot, versions, updateVersions]); + + return ( + + + +
+ + +
+ + updateVersions(value)} + renderTags={(values: string[]) => values.sort().join(", ")} + renderInput={params => ( + + )} + /> + + +
+ ); +}; + +export interface NewPackageDialogProps { + module: Module; + save: (item: Package, versions: string[]) => void; + close: () => void; +} + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, + versionRow: { + width: "100%", + display: "flex", + flex: "1 1 auto", + marginBottom: 25, + }, + marginRight: { + marginRight: 10, + }, +}); diff --git a/src/presentation/react/core/components/module-list-table/utils.ts b/src/presentation/react/core/components/module-list-table/utils.ts new file mode 100644 index 000000000..7882c2aad --- /dev/null +++ b/src/presentation/react/core/components/module-list-table/utils.ts @@ -0,0 +1,56 @@ +import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; +import _ from "lodash"; +import { ValidationError } from "../../../../../domain/common/entities/Validations"; +import { SnackbarLevel } from "d2-ui-components"; +import i18n from "../../../../../locales"; + +export function getValidationsByVersionFeedback( + module: MetadataModule, + validationsByVersion: _.Dictionary +): [SnackbarLevel, string] { + const successVersions = _(validationsByVersion) + .pickBy(validations => _.isEmpty(validations)) + .keys() + .value(); + + const errorVersions = _(validationsByVersion) + .pickBy(validations => !_.isEmpty(validations)) + .keys() + .value(); + + const msg = _.compact([ + i18n.t("Module: {{module}}", { + module: module.name, + nsSeparator: false, + }), + successVersions.length > 0 + ? i18n.t("{{n}} package(s) created successfully: {{list}}", { + n: successVersions.length, + list: successVersions.join(", "), + nsSeparator: false, + }) + : null, + errorVersions.length > 0 + ? i18n.t("{{n}} package(s) could not be created: {{list}}", { + n: errorVersions.length, + list: errorVersions.join(", "), + nsSeparator: false, + }) + : null, + ..._(validationsByVersion) + .toPairs() + .sortBy(([version, _validations]) => version) + .flatMap(([version, validations]) => + validations.map(v => `[${version}] ${v.description}`) + ) + .value(), + ]).join("\n"); + + const level = _.isEmpty(errorVersions) + ? "success" + : _.isEmpty(successVersions) + ? "error" + : "warning"; + + return [level, msg]; +} diff --git a/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx b/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx new file mode 100644 index 000000000..f021db8de --- /dev/null +++ b/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx @@ -0,0 +1,133 @@ +import { PaginationOptions } from "d2-ui-components"; +import React, { ReactNode, useCallback, useMemo, useState } from "react"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { ModulesListTable } from "../module-list-table/ModuleListTable"; +import { PackagesListTable } from "../package-list-table/PackageListTable"; +import Dropdown from "../dropdown/Dropdown"; +import { + InstanceSelectionConfig, + InstanceSelectionDropdown, + InstanceSelectionOption, +} from "../instance-selection-dropdown/InstanceSelectionDropdown"; +import { useViewSelector, ViewSelectorConfig } from "./useViewSelector"; +import { Store } from "../../../../../domain/stores/entities/Store"; + +export interface ModulePackageListTableProps { + onCreate?(): void; + onViewChange?(option: ViewOption): void; + viewValue?: ViewOption; + presentation: PresentationOption; + showSelector: ViewSelectorConfig; + showInstances: InstanceSelectionConfig; + openSyncSummary?: (syncReport: SyncReport) => void; + onInstanceChange?: (instance?: Instance | Store) => void; + actionButtonLabel?: ReactNode; +} + +export type ViewOption = "modules" | "packages"; +export type PresentationOption = "app" | "widget"; + +export const ModulePackageListTable: React.FC = ({ + onCreate, + onViewChange, + viewValue: propsViewValue, + presentation, + showSelector, + showInstances, + openSyncSummary, + onInstanceChange, + actionButtonLabel, +}) => { + const [selectedInstance, setSelectedInstance] = useState(); + const [selectedStore, setSelectedStore] = useState(); + const [selection, setSelection] = useState([]); + + const viewSelector = useViewSelector(showSelector, propsViewValue); + + const setValue = useCallback( + (value: ViewOption) => { + viewSelector.setValue(value); + if (onViewChange) onViewChange(value); + }, + [viewSelector, onViewChange] + ); + + const updateSelectedInstance = useCallback( + (type: InstanceSelectionOption, source?: Instance | Store) => { + setSelection([]); + setSelectedStore(type === "store" ? (source as Store) : undefined); + setSelectedInstance(type === "remote" ? (source as Instance) : undefined); + + if (onInstanceChange) { + onInstanceChange(source); + } + }, + [onInstanceChange] + ); + + const filters = useMemo( + () => ( + + + + {viewSelector.items.length > 1 && viewSelector.value && ( + + )} + + ), + [ + showInstances, + selectedInstance, + setValue, + viewSelector, + updateSelectedInstance, + selectedStore, + ] + ); + + const Table = viewSelector.value === "packages" ? PackagesListTable : ModulesListTable; + + return ( + + ); +}; + +const paginationOptions: PaginationOptions = { + pageSizeOptions: [10], + pageSizeInitialValue: 10, +}; diff --git a/src/presentation/react/core/components/module-package-list-table/useViewSelector.tsx b/src/presentation/react/core/components/module-package-list-table/useViewSelector.tsx new file mode 100644 index 000000000..97d8b3489 --- /dev/null +++ b/src/presentation/react/core/components/module-package-list-table/useViewSelector.tsx @@ -0,0 +1,29 @@ +import _ from "lodash"; +import { useMemo, useState } from "react"; +import i18n from "../../../../../locales"; +import { ViewOption } from "./ModulePackageListTable"; + +export interface ViewSelectorConfig { + modules?: boolean; + packages?: boolean; +} + +export function useViewSelector( + { modules = true, packages = true }: ViewSelectorConfig, + initialValue?: ViewOption +) { + const items = useMemo( + () => + _.compact([ + modules && { id: "modules" as const, name: i18n.t("Modules") }, + packages && { id: "packages" as const, name: i18n.t("Packages") }, + ]), + [modules, packages] + ); + + const [value, setValue] = useState( + () => initialValue ?? _.first(items.map(item => item.id)) + ); + + return useMemo(() => ({ items, value, setValue }), [items, value, setValue]); +} diff --git a/src/presentation/react/core/components/module-wizard/ModuleWizard.tsx b/src/presentation/react/core/components/module-wizard/ModuleWizard.tsx new file mode 100644 index 000000000..d34c2199d --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/ModuleWizard.tsx @@ -0,0 +1,49 @@ +import { Wizard, WizardStep } from "d2-ui-components"; +import _ from "lodash"; +import React from "react"; +import { useLocation } from "react-router-dom"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import { metadataModuleSteps, ModuleWizardStepProps } from "./Steps"; + +export interface ModuleWizardProps { + isEdit: boolean; + onCancel: () => void; + onClose: () => void; + module: Module; + onChange: (module: Module) => void; +} + +export const ModuleWizard: React.FC = ({ + isEdit, + onCancel, + onClose, + module, + onChange, +}) => { + const location = useLocation(); + + const props: ModuleWizardStepProps = { module, onChange, onCancel, onClose, isEdit }; + const steps = metadataModuleSteps.map(step => ({ ...step, props })); + + const onStepChangeRequest = async (_currentStep: WizardStep, newStep: WizardStep) => { + const index = _(steps).findIndex(step => step.key === newStep.key); + return _.take(steps, index).flatMap(({ validationKeys }) => + module.validate(validationKeys).map(({ description }) => description) + ); + }; + + const urlHash = location.hash.slice(1); + const stepExists = steps.find(step => step.key === urlHash); + const firstStepKey = steps.map(step => step.key)[0]; + const initialStepKey = stepExists ? urlHash : firstStepKey; + + return ( + + ); +}; diff --git a/src/presentation/react/core/components/module-wizard/Steps.ts b/src/presentation/react/core/components/module-wizard/Steps.ts new file mode 100644 index 000000000..03d1c6914 --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/Steps.ts @@ -0,0 +1,62 @@ +import { WizardStep } from "d2-ui-components"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import i18n from "../../../../../locales"; +import { GeneralInfoStep } from "./common/GeneralInfoStep"; +import { MetadataSelectionStep } from "./common/MetadataSelectionStep"; +import { SummaryStep } from "./common/SummaryStep"; +import { AdvancedMetadataOptionsStep } from "./metadata/AdvancedMetadataOptionsStep"; +import { MetadataIncludeExcludeStep } from "./metadata/MetadataIncludeExcludeStep"; + +export interface SyncWizardStep extends WizardStep { + validationKeys: string[]; + showOnSyncDialog?: boolean; +} + +export interface ModuleWizardStepProps { + module: T; + onChange: (module: T) => void; + onCancel: () => void; + onClose: () => void; + isEdit: boolean; +} + +const commonSteps: { + [key: string]: SyncWizardStep; +} = { + generalInfo: { + key: "general-info", + label: i18n.t("General info"), + component: GeneralInfoStep, + validationKeys: ["name", "department"], + }, + summary: { + key: "summary", + label: i18n.t("Summary"), + component: SummaryStep, + validationKeys: [], + showOnSyncDialog: true, + }, +}; + +export const metadataModuleSteps: SyncWizardStep[] = [ + commonSteps.generalInfo, + { + key: "metadata", + label: i18n.t("Metadata"), + component: MetadataSelectionStep, + validationKeys: ["metadataIds"], + }, + { + key: "dependencies-selection", + label: i18n.t("Select dependencies"), + component: MetadataIncludeExcludeStep, + validationKeys: ["metadataIncludeExclude"], + }, + { + key: "advanced-metadata-options", + label: i18n.t("Advanced options"), + component: AdvancedMetadataOptionsStep, + validationKeys: [], + }, + commonSteps.summary, +]; diff --git a/src/presentation/react/core/components/module-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/core/components/module-wizard/common/GeneralInfoStep.tsx new file mode 100644 index 000000000..4fd4d17e6 --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/common/GeneralInfoStep.tsx @@ -0,0 +1,85 @@ +import { makeStyles, TextField } from "@material-ui/core"; +import _ from "lodash"; +import React, { useCallback, useEffect, useState } from "react"; +import { NamedRef } from "../../../../../../domain/common/entities/Ref"; +import { ValidationError } from "../../../../../../domain/common/entities/Validations"; +import { Module } from "../../../../../../domain/modules/entities/Module"; +import i18n from "../../../../../../locales"; +import { Dictionary } from "../../../../../../types/utils"; +import { useAppContext } from "../../../contexts/AppContext"; +import Dropdown from "../../dropdown/Dropdown"; +import { ModuleWizardStepProps } from "../Steps"; + +export const GeneralInfoStep = ({ module, onChange, isEdit }: ModuleWizardStepProps) => { + const { compositionRoot } = useAppContext(); + const classes = useStyles(); + + const [errors, setErrors] = useState>({}); + const [userGroups, setUserGroups] = useState([]); + + const onChangeField = useCallback( + (field: keyof Module) => { + return (event: React.ChangeEvent) => { + const newModule = module.update({ [field]: event.target.value }); + const errors = _.keyBy(newModule.validate([field]), "property"); + + setErrors(errors); + onChange(newModule); + }; + }, + [module, onChange] + ); + + const onChangeDepartment = useCallback( + (id: string) => { + const department = userGroups.find(group => group.id === id); + onChange(module.update({ department })); + }, + [module, onChange, userGroups] + ); + + useEffect(() => { + compositionRoot.instances.getUserGroups().then(setUserGroups); + }, [compositionRoot]); + + return ( + + + + + + + + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, +}); diff --git a/src/presentation/react/core/components/module-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/core/components/module-wizard/common/MetadataSelectionStep.tsx new file mode 100644 index 000000000..7d5d09b24 --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/common/MetadataSelectionStep.tsx @@ -0,0 +1,60 @@ +import { useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback } from "react"; +import { MetadataModule } from "../../../../../../domain/modules/entities/MetadataModule"; +import i18n from "../../../../../../locales"; +import { DashboardModel, DataSetModel, ProgramModel } from "../../../../../../models/dhis/metadata"; +import MetadataTable from "../../metadata-table/MetadataTable"; +import { ModuleWizardStepProps } from "../Steps"; + +const config = { + module: { + metadata: { models: [DataSetModel, ProgramModel, DashboardModel], childrenKeys: [] }, + }, +}; + +export const MetadataSelectionStep = ({ + module, + onChange, +}: ModuleWizardStepProps) => { + const snackbar = useSnackbar(); + const { models, childrenKeys } = config["module"][module.type]; + + const changeSelection = useCallback( + (newMetadataIds: string[], newExcludedIds: string[]) => { + const additions = _.difference(newMetadataIds, module.metadataIds); + if (additions.length > 0) { + snackbar.info( + i18n.t("Selected {{difference}} elements", { difference: additions.length }), + { + autoHideDuration: 1000, + } + ); + } + + const removals = _.difference(module.metadataIds, newMetadataIds); + if (removals.length > 0) { + snackbar.info( + i18n.t("Removed {{difference}} elements", { + difference: Math.abs(removals.length), + }), + { autoHideDuration: 1000 } + ); + } + + onChange(module.update({ metadataIds: newMetadataIds, excludedIds: newExcludedIds })); + }, + [module, onChange, snackbar] + ); + + return ( + + ); +}; diff --git a/src/presentation/react/core/components/module-wizard/common/SummaryStep.tsx b/src/presentation/react/core/components/module-wizard/common/SummaryStep.tsx new file mode 100644 index 000000000..33257483c --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/common/SummaryStep.tsx @@ -0,0 +1,150 @@ +import { Button, LinearProgress, makeStyles } from "@material-ui/core"; +import { useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { ReactNode, useEffect, useState } from "react"; +import { NamedRef } from "../../../../../../domain/common/entities/Ref"; +import { MetadataModule } from "../../../../../../domain/modules/entities/MetadataModule"; +import { Module } from "../../../../../../domain/modules/entities/Module"; +import i18n from "../../../../../../locales"; +import { Dictionary } from "../../../../../../types/utils"; +import { getMetadata } from "../../../../../../utils/synchronization"; +import { useAppContext } from "../../../contexts/AppContext"; +import { ModuleWizardStepProps } from "../Steps"; +import { MetadataEntities } from "../../../../../../domain/metadata/entities/MetadataEntities"; + +export const SummaryStep = ({ module, onCancel, onClose }: ModuleWizardStepProps) => { + const classes = useStyles(); + const snackbar = useSnackbar(); + const { api, compositionRoot } = useAppContext(); + + const [isSaving, setIsSaving] = useState(false); + const [metadata, updateMetadata] = useState>({}); + + const save = async () => { + setIsSaving(true); + + const errors = await compositionRoot.modules.save(module); + + if (errors.length > 0) { + snackbar.error(errors.join("\n")); + } else { + onClose(); + } + + setIsSaving(false); + }; + + useEffect(() => { + getMetadata(api, module.metadataIds, "id,name").then(updateMetadata); + }, [api, module]); + + return ( + +
    + {getEntries(module).map(LiEntry)} + + {module.type === "metadata" && + _.keys(metadata).map(metadataType => { + const items = metadata[metadataType]; + + return ( + items.length > 0 && ( + +
      + {items.map(({ id, name }) => ( + + ))} +
    +
    + ) + ); + })} +
+
+
+ + +
+
+ {isSaving && } +
+ ); +}; + +const useStyles = makeStyles({ + saveButton: { + margin: 10, + backgroundColor: "#2b98f0", + color: "white", + }, + buttonContainer: { + display: "flex", + justifyContent: "space-between", + }, +}); + +interface Entry { + label: string; + value?: string | number; + children?: ReactNode; + hide?: boolean; +} + +const LiEntry = ({ label, value, children, hide = false }: Entry) => { + if (hide) return null; + + return ( +
  • + {_.compact([label, value]).join(": ")} + {children} +
  • + ); +}; + +const getEntries = (module: Module): Entry[] => { + switch (module.type) { + case "metadata": + return buildMetadataEntries(module as MetadataModule); + default: + return buildCommonEntries(module); + } +}; + +const buildCommonEntries = ({ name, description }: Module): Entry[] => { + return [ + { label: i18n.t("Name"), value: name }, + { + label: i18n.t("Description"), + value: description, + }, + ]; +}; + +const buildMetadataEntries = (module: MetadataModule): Entry[] => { + return [ + ...buildCommonEntries(module), + { + label: i18n.t("Department"), + value: module.department.name, + }, + { + label: i18n.t("Selected metadata"), + value: `${module.metadataIds.length} elements`, + }, + { + label: i18n.t("Metadata exclusions"), + value: `${module.excludedIds.length} elements`, + hide: module.excludedIds.length === 0, + }, + ]; +}; diff --git a/src/presentation/react/core/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx b/src/presentation/react/core/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx new file mode 100644 index 000000000..55fd5e8e8 --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { MetadataModule } from "../../../../../../domain/modules/entities/MetadataModule"; +import i18n from "../../../../../../locales"; +import { Toggle } from "../../toggle/Toggle"; +import { ModuleWizardStepProps } from "../Steps"; + +export const AdvancedMetadataOptionsStep: React.FC> = ({ + module, + onChange, +}) => { + const changeSharingSettings = (includeUserInformation: boolean) => { + onChange(module.update({ includeUserInformation })); + }; + + const changeOrgUnitReferences = (removeOrgUnitReferences: boolean) => { + onChange(module.update({ removeOrgUnitReferences })); + }; + + return ( + +
    + +
    +
    + +
    +
    + ); +}; diff --git a/src/presentation/react/core/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx b/src/presentation/react/core/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx new file mode 100644 index 000000000..4e230f2ac --- /dev/null +++ b/src/presentation/react/core/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx @@ -0,0 +1,127 @@ +import { makeStyles } from "@material-ui/core"; +import { D2SchemaProperties } from "d2-api/schemas"; +import { MultiSelector } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { MetadataPackage } from "../../../../../../domain/metadata/entities/MetadataEntities"; +import { includeExcludeRulesFriendlyNames } from "../../../../../../domain/metadata/entities/MetadataFriendlyNames"; +import { MetadataModule } from "../../../../../../domain/modules/entities/MetadataModule"; +import i18n from "../../../../../../locales"; +import { D2Model } from "../../../../../../models/dhis/default"; +import { modelFactory } from "../../../../../../models/dhis/factory"; +import { getMetadata } from "../../../../../../utils/synchronization"; +import { useAppContext } from "../../../contexts/AppContext"; +import Dropdown, { DropdownOption } from "../../dropdown/Dropdown"; +import { Toggle } from "../../toggle/Toggle"; +import { ModuleWizardStepProps } from "../Steps"; + +export const MetadataIncludeExcludeStep: React.FC> = ({ + module, + onChange, +}) => { + const classes = useStyles(); + const { d2, api } = useAppContext(); + + const [modelSelectItems, setModelSelectItems] = useState([]); + const [models, setModels] = useState([]); + const [selectedType, setSelectedType] = useState(""); + + useEffect(() => { + getMetadata(api, module.metadataIds, "id,name").then((metadata: MetadataPackage) => { + const models = _.keys(metadata).map((type: string) => { + return modelFactory(type); + }); + + const options = models + .map((model: typeof D2Model) => api.models[model.getCollectionName()].schema) + .map((schema: D2SchemaProperties) => ({ + name: schema.displayName, + id: schema.name, + })); + + setModels(models); + setModelSelectItems(options); + }); + }, [d2, api, module]); + + const { includeRules = [], excludeRules = [] } = + module.metadataIncludeExcludeRules[selectedType] ?? {}; + const allRules = [...includeRules, ...excludeRules]; + const ruleOptions = allRules.map(rule => ({ + value: rule, + text: includeExcludeRulesFriendlyNames[rule] ?? rule, + })); + + const changeUseDefaultIncludeExclude = (useDefault: boolean) => { + onChange( + useDefault + ? module.markToUseDefaultIncludeExclude() + : module.markToNotUseDefaultIncludeExclude(models) + ); + }; + + const changeModelName = (modelName: string) => { + setSelectedType(modelName); + }; + + const changeInclude = (currentIncludeRules: any) => { + const type: string = selectedType; + + const oldIncludeRules: string[] = includeRules; + + const ruleToExclude = _.difference(oldIncludeRules, currentIncludeRules); + const ruleToInclude = _.difference(currentIncludeRules, oldIncludeRules); + + if (ruleToInclude.length > 0) { + onChange(module.moveRuleFromExcludeToInclude(type, ruleToInclude)); + } else if (ruleToExclude.length > 0) { + onChange(module.moveRuleFromIncludeToExclude(type, ruleToExclude)); + } + }; + + return ( + + + + {!module.useDefaultIncludeExclude && ( +
    + + + {selectedType && ( +
    + +
    + )} +
    + )} +
    + ); +}; + +const useStyles = makeStyles({ + includeExcludeContainer: { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + marginTop: "20px", + }, + multiselectorContainer: { + width: "100%", + }, +}); diff --git a/src/presentation/react/core/components/notification-viewer-dialog/NotificationViewerDialog.tsx b/src/presentation/react/core/components/notification-viewer-dialog/NotificationViewerDialog.tsx new file mode 100644 index 000000000..5a951ae44 --- /dev/null +++ b/src/presentation/react/core/components/notification-viewer-dialog/NotificationViewerDialog.tsx @@ -0,0 +1,72 @@ +import { makeStyles } from "@material-ui/core"; +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useEffect, useState } from "react"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { AppNotification } from "../../../../../domain/notifications/entities/Notification"; +import i18n from "../../../../../locales"; +import { DashboardModel, DataSetModel, ProgramModel } from "../../../../../models/dhis/metadata"; +import { useAppContext } from "../../contexts/AppContext"; +import MetadataTable from "../metadata-table/MetadataTable"; + +export interface NotificationViewerDialogProps { + notification: AppNotification; + onClose: () => void; +} + +export const NotificationViewerDialog: React.FC = ({ + notification, + onClose, +}) => { + const classes = useStyles(); + const { compositionRoot } = useAppContext(); + + const [remoteInstance, setRemoteInstance] = useState(); + const [error, setError] = useState(false); + + useEffect(() => { + if (notification.type === "sent-pull-request") { + compositionRoot.instances.getById(notification.instance.id).then(result => + result.match({ + success: setRemoteInstance, + error: () => setError(true), + }) + ); + } + }, [compositionRoot, notification]); + + return ( + + {notification.text &&

    {notification.text}

    } + + {error && i18n.t("Could not connect with remote instance")} + + {!error && + (notification.type === "received-pull-request" || + notification.type === "sent-pull-request") && ( + + )} +
    + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, +}); diff --git a/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx new file mode 100644 index 000000000..3591a32e5 --- /dev/null +++ b/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx @@ -0,0 +1,235 @@ +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"; +import { JSONDataSource } from "../../../../../domain/instance/entities/JSONDataSource"; +import { PackageImportRule } from "../../../../../domain/package-import/entities/PackageImportRule"; +import { + isInstance, + isStore, + PackageSource, +} from "../../../../../domain/package-import/entities/PackageSource"; +import { mapToImportedPackage } from "../../../../../domain/package-import/mappers/ImportedPackageMapper"; +import { Package } from "../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { useAppContext } from "../../contexts/AppContext"; +import { PackageImportWizard } from "../package-import-wizard/PackageImportWizard"; + +interface PackageImportDialogProps { + isOpen: boolean; + instance: PackageSource; + selectedPackagesId?: string[]; + onClose: () => void; + openSyncSummary?: (result: SyncReport) => void; + disablePackageSelection?: boolean; +} + +const PackageImportDialog: React.FC = ({ + isOpen, + instance, + selectedPackagesId, + onClose, + openSyncSummary, + disablePackageSelection, +}) => { + const [enableImport, setEnableImport] = useState(false); + const snackbar = useSnackbar(); + const loading = useLoading(); + const { compositionRoot, api } = useAppContext(); + + const [packageImportRule, setPackageImportRule] = useState( + PackageImportRule.create(instance, selectedPackagesId) + ); + + useEffect(() => { + const rule = PackageImportRule.create(instance, selectedPackagesId); + setPackageImportRule(rule); + setEnableImport(rule.validate().length === 0); + }, [instance, selectedPackagesId]); + + const handlePackageImportRuleChange = (packageImportRule: PackageImportRule) => { + setEnableImport(packageImportRule.validate().length === 0); + setPackageImportRule(packageImportRule); + }; + + const saveImportedPackages = async ( + packages: Package[], + author: NamedRef, + packageSource: PackageSource, + storePackageUrls: Record + ) => { + const importedPackages = packages.map(pkg => + mapToImportedPackage(pkg, author, packageSource, storePackageUrls[pkg.id]) + ); + + const result = await compositionRoot.importedPackages.save(importedPackages); + + result.match({ + success: () => {}, + error: () => { + snackbar.error("An error has ocurred tracking the imported packages"); + }, + }); + }; + + const getPackage = (packageId: string): Promise> => { + if (isInstance(packageImportRule.source)) { + return compositionRoot.packages.get(packageId, packageImportRule.source); + } else { + return compositionRoot.packages.getStore(packageImportRule.source.id, packageId); + } + }; + + const handleExecuteImport = async () => { + // TODO: this steps coordination to import several packages, save the result + // and save the imported package should be in the domain layer, + // may be a new use case? ImportPackagesUseCase.execute (packageIds:string[]) + // Steps: + // - Retrieve current user + // - for each packageId + // 1 - retrieve package (store or instance) (using PackageRepository) + // 2 - Import (using MetadataRepository) + // 3 - Save Result (using ResultRepository) + // 4 - Save ImportedPackage (using ImportedPackageRepository) + const importedPackages: Package[] = []; + + 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 author = { id: currentUser.id, name: currentUser.userCredentials.username }; + + const executePackageImport = async (packageId: string) => { + const getPackageResult = await getPackage(packageId); + + await getPackageResult.match({ + success: async originPackage => { + loading.show( + true, + i18n.t("Importing package {{name}}", { name: originPackage.name }) + ); + + if (isStore(packageImportRule.source)) { + storePackageUrls[originPackage.id] = packageId; + } + + 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) + : undefined; + + const originDataSource = + originInstance?.value.data ?? + JSONDataSource.build(originPackage.dhisVersion, originPackage.contents); + + const result = await compositionRoot.packages.import( + originPackage, + mapping?.mappingDictionary, + originDataSource + ); + + report.setTypes( + _.uniq([...report.syncReport.types, ..._.keys(originPackage.contents)]) + ); + + report.setStatus( + result.status === "ERROR" || result.status === "NETWORK ERROR" + ? "FAILURE" + : "DONE" + ); + + const origin = isInstance(packageImportRule.source) + ? packageImportRule.source.toPublicObject() + : packageImportRule.source; + + report.addSyncResult({ + ...result, + originPackage: originPackage.toRef(), + origin: origin, + }); + + if (result.status === "SUCCESS") { + importedPackages.push(originPackage); + } + }, + error: async () => { + loading.reset(); + snackbar.error(i18n.t("Couldn't load package")); + }, + }); + }; + + for (const id of packageImportRule.packageIds) { + await executePackageImport(id); + } + + loading.show(true, i18n.t("Saving imported packages")); + + await report.save(api); + + await saveImportedPackages( + importedPackages, + author, + packageImportRule.source, + storePackageUrls + ); + + loading.reset(); + + if (openSyncSummary) { + openSyncSummary(report); + } + } catch (error) { + loading.reset(); + snackbar.error(i18n.t("An error has ocurred importing packages")); + } + }; + + return ( + handleExecuteImport()} + onCancel={onClose} + saveText={i18n.t("Import")} + maxWidth={"lg"} + fullWidth={true} + disableSave={!enableImport} + > + + + + + ); +}; + +export default PackageImportDialog; diff --git a/src/presentation/react/core/components/package-import-wizard/PackageImportWizard.tsx b/src/presentation/react/core/components/package-import-wizard/PackageImportWizard.tsx new file mode 100644 index 000000000..a2aab75c7 --- /dev/null +++ b/src/presentation/react/core/components/package-import-wizard/PackageImportWizard.tsx @@ -0,0 +1,95 @@ +import { Wizard, WizardStep } from "d2-ui-components"; +import _ from "lodash"; +import React from "react"; +import { useLocation } from "react-router-dom"; +import { PackageImportRule } from "../../../../../domain/package-import/entities/PackageImportRule"; +import i18n from "../../../../../locales"; +import { InstanceStoreSelectionStep } from "./steps/InstanceStoreSelectionStep"; +import { PackageMappingStep } from "./steps/PackageMappingStep"; +import { PackageSelectionStep } from "./steps/PackageSelectionStep"; +import { SummaryStep } from "./steps/SummaryStep"; + +export interface PackageImportWizardStep extends WizardStep { + validationKeys: string[]; +} + +export interface PackageImportWizardStepProps { + packageImportRule: PackageImportRule; + onChange: (packageImportRule: PackageImportRule) => void; + onCancel: () => void; + onClose: () => void; +} + +export const stepsBaseInfo = [ + { + key: "instance-playstore", + label: i18n.t("Instances & Play Stores"), + component: InstanceStoreSelectionStep, + validationKeys: [], + }, + { + key: "packages", + label: i18n.t("Packages"), + component: PackageSelectionStep, + validationKeys: ["packageIds"], + }, + { + key: "package-mapping", + label: i18n.t("Packages mapping"), + component: PackageMappingStep, + validationKeys: [], + }, + { + key: "summary", + label: i18n.t("Summary"), + component: SummaryStep, + validationKeys: [], + }, +]; + +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 + .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); + const validationMessages = _.take(steps, index).map(({ validationKeys }) => + props.packageImportRule.validate(validationKeys).map(({ description }) => description) + ); + + return _.flatten(validationMessages); + }; + + const urlHash = location.hash.slice(1); + const stepExists = steps.find(step => step.key === urlHash); + const firstStepKey = steps.map(step => step.key)[0]; + const initialStepKey = stepExists ? urlHash : firstStepKey; + + return ( + + ); +}; diff --git a/src/presentation/react/core/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx b/src/presentation/react/core/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx new file mode 100644 index 000000000..0291d1bc8 --- /dev/null +++ b/src/presentation/react/core/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx @@ -0,0 +1,54 @@ +import { Box, Icon, IconButton } from "@material-ui/core"; +import React, { useState } from "react"; +import { PackageSource } from "../../../../../../domain/package-import/entities/PackageSource"; +import { Store } from "../../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../../locales"; +import { + InstanceSelectionDropdown, + InstanceSelectionOption, +} from "../../instance-selection-dropdown/InstanceSelectionDropdown"; +import StoreCreationDialog from "../../store-creation/StoreCreationDialog"; +import { PackageImportWizardProps } from "../PackageImportWizard"; + +const showInstances = { remote: true, store: true }; + +export const InstanceStoreSelectionStep: React.FC = ({ + packageImportRule, + onChange, +}) => { + const [creationDialogOpen, setCreationDialogOpen] = useState(false); + const [refreshKey, setRefreshKey] = useState(Math.random); + + const handleSelectionChange = (_type: InstanceSelectionOption, source?: PackageSource) => { + if (source) onChange(packageImportRule.updateSource(source)); + }; + + const handleOnSaved = (store: Store) => { + setCreationDialogOpen(false); + onChange(packageImportRule.updateSource(store)); + setRefreshKey(Math.random); + }; + + return ( + + + + setCreationDialogOpen(true)}> + add_circle_outline + + + + setCreationDialogOpen(false)} + onSaved={handleOnSaved} + /> + + ); +}; diff --git a/src/presentation/react/core/components/package-import-wizard/steps/PackageMappingStep.tsx b/src/presentation/react/core/components/package-import-wizard/steps/PackageMappingStep.tsx new file mode 100644 index 000000000..f05fb8fab --- /dev/null +++ b/src/presentation/react/core/components/package-import-wizard/steps/PackageMappingStep.tsx @@ -0,0 +1,302 @@ +import { useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useEffect, useState } from "react"; +import { DataSource } from "../../../../../../domain/instance/entities/DataSource"; +import { JSONDataSource } from "../../../../../../domain/instance/entities/JSONDataSource"; +import { DataSourceMapping } from "../../../../../../domain/mapping/entities/DataSourceMapping"; +import { + MetadataMapping, + MetadataMappingDictionary, +} from "../../../../../../domain/mapping/entities/MetadataMapping"; +import { + MetadataEntities, + MetadataPackage, +} from "../../../../../../domain/metadata/entities/MetadataEntities"; +import { + isInstance, + PackageSource, +} from "../../../../../../domain/package-import/entities/PackageSource"; +import { ListPackage } from "../../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../../locales"; +import { + AggregatedDataElementModel, + GlobalCategoryComboModel, + GlobalCategoryModel, + GlobalCategoryOptionGroupModel, + GlobalCategoryOptionGroupSetModel, + GlobalCategoryOptionModel, + GlobalOptionModel, + IndicatorMappedModel, + OrganisationUnitMappedModel, +} from "../../../../../../models/dhis/mapping"; +import { isGlobalAdmin } from "../../../../../../utils/permissions"; +import { useAppContext } from "../../../contexts/AppContext"; +import Dropdown from "../../dropdown/Dropdown"; +import MappingTable from "../../mapping-table/MappingTable"; +import { PackageImportWizardProps } from "../PackageImportWizard"; +import Alert from "@material-ui/lab/Alert/Alert"; +import { makeStyles, Theme } from "@material-ui/core"; + +const models = [ + GlobalCategoryModel, + GlobalCategoryComboModel, + GlobalCategoryOptionModel, + GlobalCategoryOptionGroupModel, + GlobalCategoryOptionGroupSetModel, + AggregatedDataElementModel, + GlobalOptionModel, + IndicatorMappedModel, + OrganisationUnitMappedModel, +]; + +export const PackageMappingStep: React.FC = ({ + packageImportRule, + onChange, +}) => { + const classes = useStyles(); + const { compositionRoot, api } = useAppContext(); + const snackbar = useSnackbar(); + + const [globalAdmin, setGlobalAdmin] = useState(false); + const [packages, setPackages] = useState([]); + const [instance, setInstance] = useState(); + + const [packageFilter, setPackageFilter] = useState(packageImportRule.packageIds[0]); + const [dataSourceMapping, setDataSourceMapping] = useState(); + const [packageContents, setPackageContents] = useState(); + const [mappingMessage, setMappingMessage] = useState(""); + + const onChangeMapping = useCallback( + async (metadataMapping: MetadataMappingDictionary) => { + if (!dataSourceMapping) { + snackbar.error(i18n.t("Attempting to update mapping without a valid data source")); + return; + } + + const newMapping = dataSourceMapping.updateMappingDictionary(metadataMapping); + + 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); + }, + }); + } else { + onChange(packageImportRule.addOrUpdateTemporalPackageMapping(newMapping)); + } + }, + [compositionRoot, dataSourceMapping, snackbar, packageImportRule, onChange] + ); + + const onApplyGlobalMapping = useCallback( + async (type: string, id: string, subMapping: MetadataMapping) => { + if (!dataSourceMapping) return; + const newMapping = _.clone(dataSourceMapping.mappingDictionary); + _.set(newMapping, [type, id], { ...subMapping, global: true }); + await onChangeMapping(newMapping); + }, + [dataSourceMapping, onChangeMapping] + ); + + const packageFilterComponent = ( + + ); + + const updateDataSource = useCallback( + async (source: PackageSource, packageId: string) => { + if (isInstance(source)) { + const mapping = await compositionRoot.mapping.get({ + type: "instance", + id: source.id, + }); + + const packageResult = await compositionRoot.packages.get(packageId, source); + + await packageResult.match({ + error: async () => { + snackbar.error(i18n.t("Unknown error happened loading package")); + }, + success: async ({ dhisVersion, module, contents }) => { + setPackageContents(contents); + + const fullModule = await compositionRoot.modules.get(module.id, source); + + if (fullModule) { + if (fullModule.autogenerated) { + 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)); + } else { + setDataSourceMapping(mapping); + setInstance(source); + } + } else { + snackbar.error(i18n.t("Unknown error happened loading module")); + } + }, + }); + } else { + const result = await compositionRoot.packages.getStore(source.id, packageId); + + await result.match({ + error: async () => { + snackbar.error(i18n.t("Unknown error happened loading store")); + }, + success: async ({ dhisVersion, contents, module }) => { + const owner = { + type: "store" as const, + id: source.id, + moduleId: module.id, + }; + + const mapping = await compositionRoot.mapping.get(owner); + const defaultMapping = DataSourceMapping.build({ + owner, + mappingDictionary: {}, + }); + + setPackageContents(contents); + setDataSourceMapping(mapping ?? defaultMapping); + setInstance(JSONDataSource.build(dhisVersion, contents)); + }, + }); + } + }, + [compositionRoot, snackbar, packageImportRule] + ); + + useEffect(() => { + updateDataSource(packageImportRule.source, packageFilter); + }, [updateDataSource, packageFilter, packageImportRule.source]); + + useEffect(() => { + if (packageContents && dataSourceMapping) { + const mapeableModels = models.map(model => model.getCollectionName()); + + const contentsIds: string[] = Object.entries(packageContents).reduce( + (acc: string[], [key, items]) => { + const modelKey = key as keyof MetadataEntities; + + const ids: string[] = + mapeableModels.includes(modelKey) && items + ? items.map(item => item.id) + : []; + return [...acc, ...ids]; + }, + [] + ); + + const mappingIds: string[] = Object.entries(dataSourceMapping.mappingDictionary).reduce( + (acc: string[], [_, mapping]) => [...acc, ...Object.keys(mapping)], + [] + ); + + const noMappedIds = _.difference(contentsIds, mappingIds); + + const message = + 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( + "Some elements have been already mapped previously, please continue mapping remaining one or changed previous mapping" + ) + : i18n.t("No mapping found"); + + setMappingMessage(message); + } + }, [packageContents, dataSourceMapping]); + + useEffect(() => { + isGlobalAdmin(api).then(setGlobalAdmin); + }, [api]); + + useEffect(() => { + if (isInstance(packageImportRule.source)) { + compositionRoot.packages + .list(globalAdmin, packageImportRule.source) + .then(packages => { + const importPackages = packages.filter(pkg => + packageImportRule.packageIds.includes(pkg.id) + ); + + setPackages(importPackages); + }) + .catch((error: Error) => { + snackbar.error(error.message); + setPackages([]); + }); + } else { + compositionRoot.packages.listStore(packageImportRule.source.id).then(result => { + result.match({ + success: packages => { + const importPackages = packages.filter(pkg => + packageImportRule.packageIds.includes(pkg.id) + ); + + setPackages(importPackages); + }, + error: error => { + snackbar.error(error); + setPackages([]); + }, + }); + }); + } + }, [compositionRoot, packageImportRule, globalAdmin, snackbar]); + + if (!dataSourceMapping || !instance) return null; + + return ( + + {mappingMessage && ( + + {mappingMessage} + + )} + + + ); +}; + +const useStyles = makeStyles((theme: Theme) => ({ + alert: { + textAlign: "center", + margin: theme.spacing(2), + display: "flex", + justifyContent: "center", + }, +})); diff --git a/src/presentation/react/core/components/package-import-wizard/steps/PackageSelectionStep.tsx b/src/presentation/react/core/components/package-import-wizard/steps/PackageSelectionStep.tsx new file mode 100644 index 000000000..5d4a4d92a --- /dev/null +++ b/src/presentation/react/core/components/package-import-wizard/steps/PackageSelectionStep.tsx @@ -0,0 +1,37 @@ +import { PaginationOptions } from "d2-ui-components"; +import React from "react"; +import { + isInstance, + isStore, +} from "../../../../../../domain/package-import/entities/PackageSource"; +import { PackagesListTable } from "../../package-list-table/PackageListTable"; +import { PackageImportWizardProps } from "../PackageImportWizard"; + +export const PackageSelectionStep: React.FC = ({ + packageImportRule, + onChange, +}) => { + const handleSelectionChange = (ids: string[]) => { + onChange(packageImportRule.updatePackageIds(ids)); + }; + + return ( + + ); +}; + +const paginationOptions: PaginationOptions = { + pageSizeOptions: [10], + pageSizeInitialValue: 10, +}; diff --git a/src/presentation/react/core/components/package-import-wizard/steps/SummaryStep.tsx b/src/presentation/react/core/components/package-import-wizard/steps/SummaryStep.tsx new file mode 100644 index 000000000..c83b29f27 --- /dev/null +++ b/src/presentation/react/core/components/package-import-wizard/steps/SummaryStep.tsx @@ -0,0 +1,94 @@ +import { useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { ReactNode, useEffect, useState } from "react"; +import { isInstance } from "../../../../../../domain/package-import/entities/PackageSource"; +import { ListPackage } from "../../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../../locales"; +import { isGlobalAdmin } from "../../../../../../utils/permissions"; +import { useAppContext } from "../../../contexts/AppContext"; +import { PackageImportWizardProps } from "../PackageImportWizard"; + +export const SummaryStep: React.FC = ({ packageImportRule }) => { + const { api, compositionRoot } = useAppContext(); + const snackbar = useSnackbar(); + + const getPackagesFromInstance = compositionRoot.packages.list; + const getPackagesFromStore = compositionRoot.packages.listStore; + + const [globalAdmin, setGlobalAdmin] = useState(false); + const [packages, setPackages] = useState([]); + + useEffect(() => { + isGlobalAdmin(api).then(setGlobalAdmin); + }, [api]); + + useEffect(() => { + if (isInstance(packageImportRule.source)) { + getPackagesFromInstance(globalAdmin, packageImportRule.source) + .then(setPackages) + .catch((error: Error) => { + snackbar.error(error.message); + setPackages([]); + }); + } else { + getPackagesFromStore(packageImportRule.source.id).then(result => { + result.match({ + success: setPackages, + error: () => { + snackbar.error(i18n.t("Can't connect to store")); + setPackages([]); + }, + }); + }); + } + }, [getPackagesFromInstance, getPackagesFromStore, packageImportRule, globalAdmin, snackbar]); + + return ( + +
      + + +
        + {packages.length === 0 ? ( + + ) : ( + packageImportRule.packageIds.map(id => { + const instancePackage = packages.find(pkg => pkg.id === id); + return ; + }) + )} +
      +
      +
    +
    + ); +}; + +interface Entry { + label: string; + value?: string | number; + children?: ReactNode; + hide?: boolean; +} + +const LiEntry = ({ label, value, children, hide = false }: Entry) => { + if (hide) return null; + + return ( +
  • + {_.compact([label, value]).join(": ")} + {children} +
  • + ); +}; diff --git a/src/presentation/react/core/components/package-list-table/PackageListTable.tsx b/src/presentation/react/core/components/package-list-table/PackageListTable.tsx new file mode 100644 index 000000000..0537ec66b --- /dev/null +++ b/src/presentation/react/core/components/package-list-table/PackageListTable.tsx @@ -0,0 +1,936 @@ +import { Icon } from "@material-ui/core"; +import { + ConfirmationDialog, + ConfirmationDialogProps, + ObjectsTable, + ObjectsTableDetailField, + RowConfig, + TableAction, + TableColumn, + TableSelection, + TableState, + useLoading, + useSnackbar, +} from "d2-ui-components"; +import _ from "lodash"; +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 { JSONDataSource } from "../../../../../domain/instance/entities/JSONDataSource"; +import { Module } from "../../../../../domain/modules/entities/Module"; +import { ImportedPackage } from "../../../../../domain/package-import/entities/ImportedPackage"; +import { + isInstance, + isStore, + PackageSource, +} from "../../../../../domain/package-import/entities/PackageSource"; +import { mapToImportedPackage } from "../../../../../domain/package-import/mappers/ImportedPackageMapper"; +import { ListPackage, Package } from "../../../../../domain/packages/entities/Package"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { isAppConfigurator, isGlobalAdmin } from "../../../../../utils/permissions"; +import { ModulePackageListPageProps } from "../../../../webapp/core/pages/module-package-list/ModulePackageListPage"; +import { useAppContext } from "../../contexts/AppContext"; +import Dropdown from "../dropdown/Dropdown"; +import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; +import { DiffPackages, PackagesDiffDialog } from "../packages-diff-dialog/PackagesDiffDialog"; +import { + groupPackageByModuleAndVersion as groupPackagesByModuleAndVersion, + InstallStatus, + isPackageItem, + PackageItem, + PackageModuleItem, +} from "./PackageModuleItem"; + +interface PackagesListTableProps extends ModulePackageListPageProps { + isImportDialog?: boolean; + onSelectionChange?: (ids: string[]) => void; + selectedIds?: string[]; +} + +export const PackagesListTable: React.FC = ({ + remoteInstance, + remoteStore, + onActionButtonClick, + presentation = "app", + externalComponents, + openSyncSummary = _.noop, + paginationOptions, + isImportDialog = false, + onSelectionChange, + selectedIds, + actionButtonLabel, +}) => { + const { api, compositionRoot } = useAppContext(); + const snackbar = useSnackbar(); + const loading = useLoading(); + + 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()); + const [stateSelection, updateStateSelection] = useState([]); + const selection = selectedIds?.map(id => ({ id })) ?? stateSelection; + + const [dialogProps, updateDialog] = useState(null); + const [packagesToDiff, setPackagesToDiff] = useState(null); + const [moduleFilter, setModuleFilter] = useState(""); + const [dhis2VersionFilter, setDhis2VersionFilter] = useState(""); + const [localDhis2Version, setLocalDhis2Version] = useState(""); + const [installStatusFilter, setInstallStatusFilter] = useState(""); + + const [globalAdmin, setGlobalAdmin] = useState(false); + const [appConfigurator, setAppConfigurator] = useState(false); + const [loadingTable, setLoadingTable] = useState(true); + const [openImportPackageDialog, setOpenImportPackageDialog] = useState(false); + + const [toImportWizard, setToImportWizard] = useState([]); + + const isRemoteInstance = !!remoteInstance; + + useEffect(() => { + compositionRoot.modules.list(globalAdmin, remoteInstance, true).then(setModules); + }, [compositionRoot, globalAdmin, remoteInstance]); + + const updateSelection = useCallback( + (selection: TableSelection[]) => { + updateStateSelection(selection); + + if (onSelectionChange) { + onSelectionChange(selection.map(selection => selection.id)); + } + }, + [onSelectionChange] + ); + + const deletePackages = useCallback( + async (ids: string[]) => { + loading.show(true, "Deleting packages"); + for (const id of ids) { + await compositionRoot.packages.delete(id); + } + loading.reset(); + setResetKey(Math.random()); + updateSelection([]); + }, + [compositionRoot, loading, updateSelection] + ); + + const updateTable = useCallback( + ({ selection }: TableState) => { + updateSelection(selection); + }, + [updateSelection] + ); + + const downloadPackage = useCallback( + async (ids: string[]) => { + try { + compositionRoot.packages.download(remoteStore?.id, ids[0], remoteInstance); + } catch (error) { + snackbar.error(i18n.t("Invalid package")); + } + }, + [compositionRoot, remoteInstance, snackbar, remoteStore] + ); + + const publishPackage = useCallback( + async (ids: string[]) => { + loading.show(true, i18n.t("Publishing package to Store")); + const validation = await compositionRoot.packages.publish(ids[0]); + validation.match({ + success: () => { + loading.reset(); + snackbar.success(i18n.t("Package published to default store")); + }, + error: code => { + loading.reset(); + switch (code) { + case "BAD_CREDENTIALS": + case "NO_TOKEN": + case "DEFAULT_STORE_NOT_FOUND": + snackbar.error(i18n.t("Default store is not properly configured")); + return; + case "PACKAGE_NOT_FOUND": + snackbar.error(i18n.t("Could not read package")); + return; + case "WRITE_PERMISSIONS": + snackbar.error( + i18n.t("You don't have permissions to create file on GitHub") + ); + return; + case "UNKNOWN": + snackbar.error(i18n.t("Unknown error while creating file on GitHub")); + return; + case "ALREADY_PUBLISHED": + snackbar.warning(i18n.t("Package already published")); + return; + case "BRANCH_NOT_FOUND": + updateDialog({ + title: i18n.t("Branch not found"), + description: i18n.t( + "There are no branches for the department of this module. Do you want to create a new branch for this department?" + ), + onCancel: () => { + updateDialog(null); + }, + onSave: async () => { + updateDialog(null); + loading.show(true, i18n.t("Publishing package to Store")); + const validation = await compositionRoot.packages.publish( + ids[0], + true + ); + validation.match({ + success: () => + snackbar.success( + i18n.t("Package published to store in a new branch") + ), + error: () => + snackbar.error( + i18n.t("Couldn't create new branch on store") + ), + }); + loading.reset(); + }, + cancelText: i18n.t("Cancel"), + saveText: i18n.t("Proceed"), + }); + return; + default: + snackbar.error(i18n.t("Unknown error")); + } + }, + }); + }, + [compositionRoot, snackbar, loading] + ); + + const openPackageDiffDialog = useCallback( + async (ids: string[]) => { + const packageId = _(ids).get(0, null); + const remotePackage = packageId ? rows.find(row => row.id === packageId) : undefined; + if (packageId && remotePackage && isPackageItem(remotePackage)) { + setPackagesToDiff({ merge: remotePackage }); + } + }, + [rows, setPackagesToDiff] + ); + + const openPairPackageDiffDialog = useCallback( + async (ids: string[]) => { + const [packageBase, packageMerge] = ids.map(packageId => { + return rows.find(row => row.id === packageId); + }); + if ( + packageBase && + packageMerge && + isPackageItem(packageBase) && + isPackageItem(packageMerge) + ) { + setPackagesToDiff({ base: packageBase, merge: packageMerge }); + } + }, + [rows, setPackagesToDiff] + ); + + const closePackageDiffDialog = useCallback(() => setPackagesToDiff(null), [setPackagesToDiff]); + + const saveImportedPackage = useCallback( + async ( + pkg: Package, + author: NamedRef, + packageSource: PackageSource, + storePackageUrl?: string + ) => { + const importedPackage = mapToImportedPackage( + pkg, + author, + packageSource, + storePackageUrl + ); + + const result = await compositionRoot.importedPackages.save([importedPackage]); + + result.match({ + success: () => {}, + error: () => { + snackbar.error("An error has ocurred tracking the imported package"); + }, + }); + }, + [compositionRoot, snackbar] + ); + + const getPackage = useCallback( + ( + packageSource: PackageSource, + packageId: string + ): Promise> => { + if (isInstance(packageSource)) { + return compositionRoot.packages.get(packageId, packageSource); + } else { + return compositionRoot.packages.getStore(packageSource.id, packageId); + } + }, + [compositionRoot] + ); + + const getPackageSourceToImport = useCallback(() => { + if (remoteInstance) { + return remoteInstance; + } else if (remoteStore) { + return remoteStore; + } else { + throw new Error("The import action is only available for remote package source"); + } + }, [remoteInstance, remoteStore]); + + const importPackagesFromWizard = useCallback((ids: string[]) => { + setToImportWizard(ids); + 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(); + + const result = await getPackage(packageSource, ids[0]); + + 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 }) + ); + + const mapping = await compositionRoot.mapping.get({ + type: isInstance(packageSource) ? "instance" : "store", + id: packageSource.id, + moduleId: originPackage.module.id, + }); + + const originDataSource = + remoteInstance && isInstance(packageSource) + ? remoteInstance + : JSONDataSource.build( + originPackage.dhisVersion, + originPackage.contents + ); + + const result = await compositionRoot.packages.import( + originPackage, + mapping?.mappingDictionary, + originDataSource + ); + + 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" + : "DONE" + ); + + report.addSyncResult({ + ...result, + originPackage: originPackage.toRef(), + origin: remoteInstance?.toPublicObject(), + }); + await report.save(api); + + if (result.status === "SUCCESS") { + const author = { + id: currentUser.id, + name: currentUser.userCredentials.username, + }; + + await saveImportedPackage( + originPackage, + author, + packageSource, + isStore(packageSource) ? ids[0] : undefined + ); + } + + openSyncSummary(report); + setResetKey(Math.random()); + } catch (error) { + snackbar.error(error.message); + } + loading.reset(); + }, + error: async () => { + snackbar.error(i18n.t("Couldn't load package")); + }, + }); + }, + [ + compositionRoot, + api, + loading, + remoteInstance, + snackbar, + openSyncSummary, + getPackage, + getPackageSourceToImport, + saveImportedPackage, + ] + ); + + const getInstallStatusText = (installStatus: InstallStatus): string => { + switch (installStatus) { + case "Installed": + return i18n.t("Installed"); + case "NotInstalled": + 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)"); + } + }; + + 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: "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: PackageModuleItem) => + isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, + }, + ], + [] + ); + + const details: ObjectsTableDetailField[] = useMemo( + () => [ + { name: "id", text: i18n.t("ID") }, + { name: "name", text: i18n.t("Name") }, + { name: "description", text: i18n.t("Description") }, + { name: "version", text: i18n.t("Version") }, + { name: "dhisVersion", text: i18n.t("DHIS2 Version") }, + { name: "module", text: i18n.t("Module") }, + { name: "created", text: i18n.t("Created") }, + { name: "user", text: i18n.t("Created by") }, + { + name: "installStatus", + text: i18n.t("Status"), + getValue: (row: PackageModuleItem) => + isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, + }, + ], + [] + ); + + const actions: TableAction[] = useMemo( + () => [ + { + name: "details", + text: i18n.t("Details"), + multiple: false, + primary: true, + isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), + }, + { + name: "delete", + text: i18n.t("Delete"), + multiple: true, + onClick: deletePackages, + icon: delete, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + !isImportDialog && + presentation === "app" && + !isRemoteInstance && + !remoteStore && + appConfigurator, + }, + { + name: "download", + text: i18n.t("Download as JSON"), + multiple: false, + onClick: downloadPackage, + icon: cloud_download, + isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), + }, + { + name: "publish", + text: i18n.t("Publish to Store"), + multiple: false, + onClick: publishPackage, + icon: publish, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + !isImportDialog && + presentation === "app" && + !isRemoteInstance && + !remoteStore && + appConfigurator, + }, + { + name: "compare-with-local", + text: i18n.t("Compare with local instance"), + multiple: false, + icon: compare, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + presentation === "app" && + (isRemoteInstance || remoteStore !== undefined) && + appConfigurator, + onClick: openPackageDiffDialog, + }, + { + name: "compare-selected-packages", + text: i18n.t("Compare selected packages"), + multiple: true, + icon: compare_arrows, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + presentation === "app" && + appConfigurator && + (selectedIds ? selectedIds.length === 2 : false), + onClick: openPairPackageDiffDialog, + }, + { + name: "import", + text: i18n.t("Import package"), + multiple: false, + onClick: importPackage, + icon: arrow_downward, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + !isImportDialog && + presentation === "app" && + (isRemoteInstance || remoteStore !== undefined) && + appConfigurator, + }, + { + name: "importFromWizard", + text: i18n.t("Import package (wizard)"), + multiple: true, + onClick: importPackagesFromWizard, + icon: arrow_downward, + isActive: (rows: PackageModuleItem[]) => + _.every(rows, row => isPackageItem(row)) && + !isImportDialog && + presentation === "app" && + (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, + deletePackages, + downloadPackage, + importPackage, + importPackagesFromWizard, + isRemoteInstance, + openPackageDiffDialog, + openPairPackageDiffDialog, + presentation, + publishPackage, + remoteStore, + isImportDialog, + selectedIds, + generateModule, + modules, + ] + ); + + const moduleFilterItems = useMemo(() => { + const packages = remoteStore ? storePackages : instancePackages; + + return _(packages) + .map(pkg => pkg.module) + .uniqBy(({ id }) => id) + .sortBy(({ name }) => name) + .value(); + }, [instancePackages, storePackages, remoteStore]); + + const dhis2VersionFilterItems = useMemo(() => { + const packages = remoteStore ? storePackages : instancePackages; + + return _(packages) + .map(pkg => ({ + id: pkg.dhisVersion, + name: + localDhis2Version === pkg.dhisVersion + ? pkg.dhisVersion + : `${pkg.dhisVersion} (${i18n.t("Not recommended")})`, + })) + .uniqBy(({ id }) => id) + .sortBy(({ name }) => name) + .value(); + }, [instancePackages, storePackages, remoteStore, localDhis2Version]); + + const installStatusFilterItems = useMemo(() => { + const packages = remoteStore ? storePackages : instancePackages; + + return _(packages) + .map(pkg => ({ + id: pkg.installStatus, + name: getInstallStatusText(pkg.installStatus), + })) + .uniqBy(({ id }) => id) + .sortBy(({ name }) => name) + .value(); + }, [instancePackages, storePackages, remoteStore]); + + const filterComponents = useMemo(() => { + const updateFilter = (fn: Function) => (...args: unknown[]) => { + fn(...args); + setResetKey(Math.random()); + }; + + const moduleFilterComponent = ( + + ); + + const dhis2VersionFilterComponent = ( + + ); + + const installStateFilterComponent = ( + + ); + return [ + externalComponents, + moduleFilterComponent, + dhis2VersionFilterComponent, + installStateFilterComponent, + ]; + }, [ + externalComponents, + moduleFilter, + moduleFilterItems, + dhis2VersionFilterItems, + dhis2VersionFilter, + installStatusFilterItems, + installStatusFilter, + ]); + + const rowsFiltered = useMemo(() => { + setLoadingTable(false); + + 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) => { + setOpenImportPackageDialog(false); + setToImportWizard([]); + openSyncSummary(syncReport); + setResetKey(Math.random()); + }; + + const handleCloseImportWizard = () => { + setOpenImportPackageDialog(false); + setToImportWizard([]); + }; + + const showImportFromWizardButton = !isImportDialog && presentation === "app" && appConfigurator; + + const packageSource = remoteInstance ?? remoteStore; + + useEffect(() => { + api.getVersion().then(setLocalDhis2Version); + }, [api]); + + useEffect(() => { + setLoadingTable(true); + compositionRoot.packages + .list(globalAdmin, remoteInstance) + .then(packages => { + setInstancePackages( + mapPackagesToPackageItems(modules, packages, importedPackages, packageSource) + ); + }) + .catch((error: Error) => { + snackbar.error(error.message); + setInstancePackages([]); + }); + }, [ + compositionRoot, + remoteInstance, + resetKey, + snackbar, + globalAdmin, + importedPackages, + remoteStore, + modules, + packageSource, + ]); + + useEffect(() => { + if (remoteStore) { + setLoadingTable(true); + compositionRoot.packages.listStore(remoteStore.id).then(validation => { + validation.match({ + success: packages => { + setStorePackages( + mapPackagesToPackageItems( + modules, + packages, + importedPackages, + packageSource + ) + ); + }, + error: () => { + snackbar.error(i18n.t("Can't connect to store")); + setStorePackages([]); + }, + }); + }); + } else { + setStorePackages([]); + } + }, [ + compositionRoot, + snackbar, + remoteStore, + importedPackages, + remoteInstance, + resetKey, + modules, + packageSource, + ]); + + useEffect(() => { + compositionRoot.importedPackages.list().then(result => + result.match({ + success: setImportedPackages, + error: () => { + snackbar.error(i18n.t("An error has ocurred retrieving imported packages")); + setImportedPackages([]); + }, + }) + ); + }, [compositionRoot, snackbar, resetKey]); + + useEffect(() => { + setModuleFilter(""); + setDhis2VersionFilter(""); + setInstallStatusFilter(""); + setResetKey(Math.random()); + }, [remoteInstance, remoteStore]); + + useEffect(() => { + isAppConfigurator(api).then(setAppConfigurator); + 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} + onActionButtonClick={showImportFromWizardButton ? onActionButtonClick : undefined} + forceSelectionColumn={presentation === "app"} + filterComponents={filterComponents} + selection={selection} + onChange={updateTable} + paginationOptions={paginationOptions} + actionButtonLabel={actionButtonLabel} + loading={loadingTable} + childrenKeys={["packages"]} + /> + + {dialogProps && } + + {packagesToDiff && ( + + )} + + {packageSource && ( + + )} + + ); +}; + +function mapPackagesToPackageItems( + modules: Module[], + packages: ListPackage[], + importedPackages: ImportedPackage[], + packageSource?: PackageSource +): PackageItem[] { + 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 installStatus: InstallStatus = installed + ? "Installed" + : newUpdates + ? "Upgrade" + : "NotInstalled"; + + return { ...pkg, installStatus }; + }); + + return listPackages; + } else { + const listPackages = packages.map(pkg => { + const isPackageImported = verifyIfPackageIsImported(pkg); + + 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; + } +} diff --git a/src/presentation/react/core/components/package-list-table/PackageModuleItem.ts b/src/presentation/react/core/components/package-list-table/PackageModuleItem.ts new file mode 100644 index 000000000..993999346 --- /dev/null +++ b/src/presentation/react/core/components/package-list-table/PackageModuleItem.ts @@ -0,0 +1,47 @@ +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" + | "InstalledLocalPackage" + | "NotInstalledLocalPackage"; +export type PackageItem = Omit & { installStatus: InstallStatus }; + +export const isPackageItem = (item: PackageModuleItem): item is PackageItem => { + return (item as PackageItem).module !== undefined; +}; + +export const groupPackageByModuleAndVersion = (packages: PackageItem[]) => { + return packages.reduce((acc, item) => { + const parentKey = `${item.module.id}-${item.version}`; + + const parent = acc.find(parent => parent.id === parentKey); + + if (parent) { + return acc.map(parentItem => + parentItem.id === parentKey + ? { ...parentItem, packages: [...parentItem.packages, item] } + : parentItem + ); + } else { + const newParent = { + id: parentKey, + name: item.module.name, + version: item.version, + packages: [item], + }; + return [...acc, newParent]; + } + }, [] as ModuleItem[]); +}; diff --git a/src/presentation/react/core/components/packages-diff-dialog/PackagesDiffDialog.tsx b/src/presentation/react/core/components/packages-diff-dialog/PackagesDiffDialog.tsx new file mode 100644 index 000000000..923c27367 --- /dev/null +++ b/src/presentation/react/core/components/packages-diff-dialog/PackagesDiffDialog.tsx @@ -0,0 +1,167 @@ +import { LinearProgress } from "@material-ui/core"; +import { makeStyles } from "@material-ui/styles"; +import { useSnackbar } from "d2-ui-components"; +import { ConfirmationDialog } from "d2-ui-components/confirmation-dialog/ConfirmationDialog"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { + MetadataPackageDiff, + ModelDiff, +} from "../../../../../domain/packages/entities/MetadataPackageDiff"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../contexts/AppContext"; +import SyncSummary from "../sync-summary/SyncSummary"; +import { getChange, getTitle, usePackageImporter } from "./utils"; + +export interface PackagesDiffDialogProps { + onClose(): void; + remoteInstance?: Instance; + remoteStore?: Store; + packages: DiffPackages; +} + +export interface DiffPackages { + base?: PackageToDiff; + merge: PackageToDiff; +} + +export type PackageToDiff = { id: string; name: string; version: string }; + +export const PackagesDiffDialog: React.FC = props => { + const { compositionRoot } = useAppContext(); + const snackbar = useSnackbar(); + const [metadataDiff, setMetadataDiff] = useState(); + const { packages, remoteStore, remoteInstance, onClose } = props; + const { base: packageBase, merge: packageMerge } = packages; + const showImportButton = !packageBase; + + useEffect(() => { + compositionRoot.packages + .diff(packageBase?.id, packageMerge.id, remoteStore?.id, remoteInstance) + .then(res => { + res.match({ + error: msg => { + snackbar.error(i18n.t("Cannot get data from remote instance") + ": " + msg); + onClose(); + }, + success: setMetadataDiff, + }); + }); + }, [ + compositionRoot, + packageBase, + packageMerge, + remoteStore, + remoteInstance, + onClose, + snackbar, + ]); + + const hasChanges = metadataDiff && metadataDiff.hasChanges; + const packageName = `${packageMerge.name} (${remoteInstance?.name ?? "Store"})`; + const { importPackage, syncReport, closeSyncReport } = usePackageImporter( + remoteInstance, + packageName, + metadataDiff, + onClose + ); + + return ( + + + {metadataDiff ? ( + + ) : ( + + )} + + + {!!syncReport && } + + ); +}; + +export const MetadataDiffTable: React.FC<{ + metadataDiff: MetadataPackageDiff["changes"]; +}> = props => { + const { metadataDiff } = props; + const classes = useStyles(); + + return ( +
      + {_.map(metadataDiff, (modelDiff, model) => ( +
    • +

      {model}

      : {modelDiff.total}{" "} + {i18n.t("objects")} ({i18n.t("Unmodified")}: {modelDiff.unmodified.length},{" "} + {i18n.t("New")}: {modelDiff.created.length}, {i18n.t("Updated")}:{" "} + {modelDiff.updates.length}) + +
    • + ))} +
    + ); +}; + +export const ModelDiffList: React.FC<{ modelDiff: ModelDiff }> = props => { + const { modelDiff: diff } = props; + const classes = useStyles(); + + return ( +
      + {diff.created.length > 0 && ( +
    • + + {i18n.t("New")}: {diff.created.length} + + + `${obj.name} (${obj.id})`)} /> +
    • + )} + + {diff.updates.length > 0 && ( +
    • + + {i18n.t("Updated")}: {diff.updates.length} + + + ( + + [{update.obj.id}] {update.obj.name} + + + ))} + /> +
    • + )} +
    + ); +}; + +export const List: React.FC<{ items: React.ReactNode[] }> = props => { + const { items } = props; + return ( +
      + {items.map((item, idx) => ( +
    • {item}
    • + ))} +
    + ); +}; + +const useStyles = makeStyles({ + modelTitle: { display: "inline" }, + added: { color: "green" }, + updated: { color: "orange" }, +}); diff --git a/src/presentation/react/core/components/packages-diff-dialog/utils.tsx b/src/presentation/react/core/components/packages-diff-dialog/utils.tsx new file mode 100644 index 000000000..ce17f01ae --- /dev/null +++ b/src/presentation/react/core/components/packages-diff-dialog/utils.tsx @@ -0,0 +1,80 @@ +import { useLoading, useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import { useCallback, useState } from "react"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { + FieldUpdate, + MetadataPackageDiff, +} from "../../../../../domain/packages/entities/MetadataPackageDiff"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { useAppContext } from "../../contexts/AppContext"; +import { PackageToDiff } from "./PackagesDiffDialog"; + +export function getChange(u: FieldUpdate): string { + return `${u.field}: ${truncate(u.oldValue)} -> ${truncate(u.newValue)}`; +} + +function truncate(s: string) { + return _.truncate(s, { length: 50 }); +} + +export function getTitle( + packageBase: PackageToDiff | undefined, + packageMerge: PackageToDiff | undefined, + metadataDiff: MetadataPackageDiff | undefined +) { + let prefix: string; + if (!metadataDiff) { + prefix = i18n.t("Comparing package contents"); + } else if (metadataDiff.hasChanges) { + prefix = i18n.t("Changes found"); + } else { + prefix = i18n.t("No changes found"); + } + const info = [packageBase, packageMerge] + .map(package_ => (package_ ? `${package_.name} (${package_.version})` : i18n.t("Local"))) + .join(" - > "); + + return `${prefix}: ${info}`; +} + +export function usePackageImporter( + instance: Instance | undefined, + packageName: string, + metadataDiff: MetadataPackageDiff | undefined, + onClose: () => void +) { + const { compositionRoot, api } = useAppContext(); + const loading = useLoading(); + const snackbar = useSnackbar(); + const [syncReport, setSyncReport] = useState(); + + const closeSyncReport = useCallback(() => { + setSyncReport(undefined); + onClose(); + }, [setSyncReport, onClose]); + + const importPackage = useCallback(() => { + async function performImport() { + if (!metadataDiff) return; + loading.show(true, i18n.t("Importing package {{name}}", { name: packageName })); + + const result = await compositionRoot.metadata.import(metadataDiff.mergeableMetadata); + const report = SyncReport.create("metadata"); + report.setStatus( + result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" : "DONE" + ); + report.addSyncResult({ ...result, origin: instance?.toPublicObject() }); + await report.save(api); + + setSyncReport(report); + } + + performImport() + .catch(err => snackbar.error(err.message)) + .finally(() => loading.reset()); + }, [packageName, metadataDiff, compositionRoot, loading, snackbar, api, instance]); + + return { importPackage, syncReport, closeSyncReport }; +} diff --git a/src/presentation/react/core/components/page-header/PageHeader.tsx b/src/presentation/react/core/components/page-header/PageHeader.tsx new file mode 100644 index 000000000..30c315acf --- /dev/null +++ b/src/presentation/react/core/components/page-header/PageHeader.tsx @@ -0,0 +1,74 @@ +import { ButtonProps, Icon, IconButton, Tooltip } from "@material-ui/core"; +import { Variant } from "@material-ui/core/styles/createTypography"; +import Typography from "@material-ui/core/Typography"; +import { DialogButton } from "d2-ui-components"; +import React, { ReactNode } from "react"; +import i18n from "../../../../../locales"; + +const PageHeader: React.FC = ({ + variant = "h5", + title, + onBackClick, + help, + helpSize = "sm", + children, +}) => { + return ( +
    + {!!onBackClick && ( + + arrow_back + + )} + + + {title} + + {help && ( + + )} + {children} +
    + ); +}; + +export interface PageHeaderProps { + variant?: Variant; + title: string; + onBackClick?: () => void; + help?: ReactNode; + helpSize?: "xs" | "sm" | "md" | "lg" | "xl"; +} + +const styles = { + backArrow: { paddingTop: 10, marginBottom: 5 }, + help: { marginBottom: 8 }, + text: { display: "inline-block", fontWeight: 300 }, +}; + +const Button = ({ onClick }: ButtonProps) => ( + + + help + + +); + +export default PageHeader; diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx new file mode 100644 index 000000000..b2a82674f --- /dev/null +++ b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx @@ -0,0 +1,53 @@ +import { Box, makeStyles, Theme } from "@material-ui/core"; +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useState } from "react"; +import i18n from "../../../../../locales"; +import { PeriodFilter } from "../../../../webapp/msf-aggregate-data/pages/MSFHomePage"; +import PeriodSelection from "../period-selection/PeriodSelection"; + +export interface PeriodSelectionDialogProps { + title?: string; + period: PeriodFilter; + onClose(): void; + onSave(value: PeriodFilter): void; +} + +export const PeriodSelectionDialog: React.FC = ({ + title, + onClose, + onSave, + period, +}) => { + const classes = useStyles(); + const [periodState, setPeriodState] = useState(period); + + return ( + onSave(periodState)} + cancelText={i18n.t("Cancel")} + saveText={i18n.t("Save")} + > + + + + + ); +}; + +const useStyles = makeStyles((theme: Theme) => ({ + periodContainer: { + margin: "0 auto", + }, + periodContent: { + margin: theme.spacing(2), + }, +})); diff --git a/src/presentation/react/core/components/period-selection/PeriodSelection.tsx b/src/presentation/react/core/components/period-selection/PeriodSelection.tsx new file mode 100644 index 000000000..2190f67d7 --- /dev/null +++ b/src/presentation/react/core/components/period-selection/PeriodSelection.tsx @@ -0,0 +1,143 @@ +import { makeStyles } from "@material-ui/core"; +import { DatePicker } from "d2-ui-components"; +import _ from "lodash"; +import moment, { Moment } from "moment"; +import React, { useCallback, useMemo } from "react"; +import { DataSyncPeriod } from "../../../../../domain/aggregated/types"; +import i18n from "../../../../../locales"; +import { Maybe } from "../../../../../types/utils"; +import { availablePeriods, PeriodType } from "../../../../../utils/synchronization"; +import Dropdown from "../dropdown/Dropdown"; + +export interface ObjectWithPeriodInput { + period: DataSyncPeriod; + startDate?: Date | string; + endDate?: Date | string; +} + +export interface ObjectWithPeriod { + period: DataSyncPeriod; + startDate?: Date; + endDate?: Date; +} + +export interface PeriodSelectionProps { + periodTitle?: string; + objectWithPeriod: ObjectWithPeriodInput; + onChange?: (obj: ObjectWithPeriod) => void; + onFieldChange?( + field: Field, + value: ObjectWithPeriod[Field] + ): void; + skipPeriods?: Set; + className?: string; +} + +export type OnChange = Required["onChange"]; +export type OnFieldChange = Required["onFieldChange"]; + +const useStyles = makeStyles({ + dropdown: { + marginTop: 20, + marginLeft: 0, + }, + fixedPeriod: { + marginTop: 5, + marginBottom: -20, + marginLeft: 10, + }, + datePicker: { + marginTop: -10, + }, +}); + +const PeriodSelection: React.FC = props => { + const { + objectWithPeriod: obj, + onChange = _.noop as OnChange, + onFieldChange = _.noop as OnFieldChange, + skipPeriods = new Set(), + periodTitle = i18n.t("Period"), + className, + } = props; + + const objectWithPeriod: ObjectWithPeriod = { + period: obj.period, + startDate: obj.startDate ? moment(obj.startDate).toDate() : undefined, + endDate: obj.endDate ? moment(obj.endDate).toDate() : undefined, + }; + const { period, startDate, endDate } = objectWithPeriod; + + const classes = useStyles(); + + const periodItems = useMemo( + () => + _(availablePeriods) + .mapValues((value, key) => ({ ...value, id: key })) + .values() + .filter(period => !skipPeriods.has(period.id as PeriodType)) + .value(), + [skipPeriods] + ); + + const updatePeriod = useCallback( + (period: ObjectWithPeriodInput["period"]) => { + onChange({ ...objectWithPeriod, period }); + onFieldChange("period", period); + }, + [objectWithPeriod, onChange, onFieldChange] + ); + + const updateStartDate = useCallback( + (startDateM: Maybe) => { + const startDate = startDateM?.toDate(); + onChange({ ...objectWithPeriod, startDate }); + onFieldChange("startDate", startDate); + }, + [objectWithPeriod, onChange, onFieldChange] + ); + + const updateEndDate = useCallback( + (endDateM: Maybe) => { + const endDate = endDateM?.toDate(); + onChange({ ...objectWithPeriod, endDate }); + onFieldChange("endDate", endDate); + }, + [objectWithPeriod, onChange, onFieldChange] + ); + + return ( +
    +
    + +
    + + {period === "FIXED" && ( +
    +
    + +
    +
    + +
    +
    + )} +
    + ); +}; + +export default PeriodSelection; diff --git a/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx b/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx new file mode 100644 index 000000000..e2a8a95d7 --- /dev/null +++ b/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx @@ -0,0 +1,207 @@ +import { makeStyles, TextField } from "@material-ui/core"; +import { + ConfirmationDialog, + SearchResult, + ShareUpdate, + Sharing, + SharingRule, + useLoading, + useSnackbar, +} from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useEffect, useState } from "react"; +import { NamedRef } from "../../../../../domain/common/entities/Ref"; +import { Instance } from "../../../../../domain/instance/entities/Instance"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import { SynchronizationBuilder } from "../../../../../types/synchronization"; +import { useAppContext } from "../../contexts/AppContext"; + +export interface PullRequestCreation { + instance: Instance; + builder: SynchronizationBuilder; + type: SynchronizationType; +} + +export interface PullRequestCreationDialogProps extends PullRequestCreation { + onClose: () => void; +} + +interface PullRequestFields { + subject?: string; + description?: string; +} + +export const PullRequestCreationDialog: React.FC = ({ + instance, + type, + builder, + onClose, +}) => { + const { compositionRoot } = useAppContext(); + const classes = useStyles(); + const snackbar = useSnackbar(); + const loading = useLoading(); + + const [fields, updateFields] = useState({}); + const [responsibles, updateResponsibles] = useState>(); + const [notificationUsers, updateNotificationUsers] = useState<{ + users: SharingRule[]; + userGroups: SharingRule[]; + }>({ users: [], userGroups: [] }); + + const save = useCallback(async () => { + const { subject, description } = fields; + + if (!subject) { + snackbar.error(i18n.t("You need to provide a subject")); + return; + } + + try { + loading.show(true, i18n.t("Creating pull request")); + const sync = compositionRoot.sync[type](builder); + const payload = await sync.buildPayload(); + + await compositionRoot.sync.createPullRequest({ + instance, + type, + ids: builder.metadataIds, + payload, + subject, + description, + notificationUsers: { + users: sharingToNamedRef(notificationUsers.users), + userGroups: sharingToNamedRef(notificationUsers.userGroups), + }, + }); + + onClose(); + snackbar.success(i18n.t("Pull request created")); + } catch (err) { + snackbar.error(err.message); + } finally { + loading.reset(); + } + }, [ + compositionRoot, + builder, + fields, + type, + instance, + notificationUsers, + onClose, + snackbar, + loading, + ]); + + const updateTextField = useCallback( + (field: keyof PullRequestFields) => (event: React.ChangeEvent<{ value: unknown }>) => { + const value = event.target.value as string; + updateFields(fields => ({ ...fields, [field]: value })); + }, + [] + ); + + const onSearchRequest = useCallback( + async (key: string) => + compositionRoot.instances + .getApi(instance) + .get("/sharing/search", { key }) + .getData(), + [compositionRoot, instance] + ); + + const onSharingChanged = useCallback(async (updatedAttributes: ShareUpdate) => { + updateNotificationUsers(({ users, userGroups }) => { + const { userAccesses = users, userGroupAccesses = userGroups } = updatedAttributes; + return { users: userAccesses, userGroups: userGroupAccesses }; + }); + }, []); + + useEffect(() => { + compositionRoot.responsibles.get(builder.metadataIds, instance).then(responsibles => { + const users = _.uniqBy( + namedRefToSharing(responsibles.flatMap(({ users }) => users)), + "id" + ); + const userGroups = _.uniqBy( + namedRefToSharing(responsibles.flatMap(({ userGroups }) => userGroups)), + "id" + ); + + updateResponsibles(new Set([...users, ...userGroups].map(({ id }) => id))); + updateNotificationUsers({ users, userGroups }); + }); + }, [compositionRoot, builder, instance]); + + return ( + + + + + + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, +}); + +function namedRefToSharing(namedRefs: NamedRef[]): SharingRule[] { + return namedRefs.map(({ id, name }) => ({ id, displayName: name, access: "------" })); +} + +function sharingToNamedRef(sharings: SharingRule[]): NamedRef[] { + return sharings.map(({ id, displayName }) => ({ id, name: displayName })); +} diff --git a/src/presentation/react/core/components/radio-button-group/RadioButtonGroup.tsx b/src/presentation/react/core/components/radio-button-group/RadioButtonGroup.tsx new file mode 100644 index 000000000..2161ffc30 --- /dev/null +++ b/src/presentation/react/core/components/radio-button-group/RadioButtonGroup.tsx @@ -0,0 +1,59 @@ +import FormControl from "@material-ui/core/FormControl"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import FormLabel from "@material-ui/core/FormLabel"; +import Radio from "@material-ui/core/Radio"; +import RadioGroup from "@material-ui/core/RadioGroup"; +import _ from "lodash"; +import React from "react"; + +interface RadioButtonGroupProps { + items: { + id: string; + name: string; + disabled?: boolean; + }[]; + value: string; + defaultValue?: string; + onChange?: (event: React.ChangeEvent) => void; + onValueChange?: (value: string) => void; + title?: string; + horizontal?: boolean; +} + +export default function RadioButtonGroup({ + items, + value, + defaultValue, + onChange = _.noop, + onValueChange = _.noop, + title, + horizontal = true, +}: RadioButtonGroupProps) { + const handleChange = (event: React.ChangeEvent) => { + onChange(event); + onValueChange(event.target.value as string); + }; + + return ( + + {title && {title}} + + + {items.map(({ id, name, disabled = false }, index) => ( + } + label={name} + disabled={disabled} + /> + ))} + + + ); +} diff --git a/src/presentation/react/core/components/responsible-dialog/ResponsibleDialog.tsx b/src/presentation/react/core/components/responsible-dialog/ResponsibleDialog.tsx new file mode 100644 index 000000000..1cfee0632 --- /dev/null +++ b/src/presentation/react/core/components/responsible-dialog/ResponsibleDialog.tsx @@ -0,0 +1,96 @@ +import { SearchResult, ShareUpdate } from "d2-ui-components"; +import _ from "lodash"; +import React, { useMemo } from "react"; +import { NamedRef } from "../../../../../domain/common/entities/Ref"; +import { MetadataEntities } from "../../../../../domain/metadata/entities/MetadataEntities"; +import { MetadataResponsible } from "../../../../../domain/metadata/entities/MetadataResponsible"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../contexts/AppContext"; +import { SharingDialog } from "../sharing-dialog/SharingDialog"; + +export interface ResponsibleDialogProps { + entity: keyof MetadataEntities; + responsibles: MetadataResponsible[]; + sharingSettingsElement?: NamedRef; + updateResponsibles: (responsibles: MetadataResponsible[]) => void; + onClose: () => void; +} + +export const ResponsibleDialog: React.FC = ({ + entity, + responsibles, + sharingSettingsElement, + updateResponsibles, + onClose, +}) => { + const { compositionRoot, api } = useAppContext(); + + const onSharingChanged = async (update: ShareUpdate) => { + if (!sharingSettingsElement) return; + + const { users: oldUsers = [], userGroups: oldUserGroups = [] } = + responsibles.find(({ id }) => id === sharingSettingsElement.id) ?? {}; + + const users = + update.userAccesses?.map(({ id, displayName }) => ({ id, name: displayName })) ?? + oldUsers; + const userGroups = + update.userGroupAccesses?.map(({ id, displayName }) => ({ id, name: displayName })) ?? + oldUserGroups; + + const newResponsible: MetadataResponsible = { + ...sharingSettingsElement, + entity, + users, + userGroups, + }; + + await compositionRoot.responsibles.set(newResponsible); + updateResponsibles(_.uniqBy([newResponsible, ...responsibles], "id")); + }; + + const onSearchRequest = async (key: string) => + api + .get("/sharing/search", { key }) + .getData(); + + const sharingObject = useMemo(() => { + if (!sharingSettingsElement) return undefined; + + const responsible = responsibles.find(({ id }) => id === sharingSettingsElement?.id); + const { users = [], userGroups = [] } = responsible ?? {}; + + return { + object: { + ...sharingSettingsElement, + userAccesses: users.map(({ id, name }) => ({ id, displayName: name, access: "" })), + userGroupAccesses: userGroups.map(({ id, name }) => ({ + id, + displayName: name, + access: "", + })), + }, + meta: {}, + }; + }, [responsibles, sharingSettingsElement]); + + if (!sharingObject) return null; + + return ( + + ); +}; diff --git a/src/presentation/react/core/components/share/Share.jsx b/src/presentation/react/core/components/share/Share.jsx new file mode 100644 index 000000000..565f85367 --- /dev/null +++ b/src/presentation/react/core/components/share/Share.jsx @@ -0,0 +1,144 @@ +import React from "react"; +import logo from "./logo-eyeseetea.png"; +import PropTypes from "prop-types"; + +class Share extends React.Component { + static propTypes = { + visible: PropTypes.bool.isRequired, + }; + + styles = { + eyeseeteaShare: { + backgroundColor: "rgb(243,243,243)", + position: "fixed", + bottom: "0px", + right: "100px", + borderRadius: "0px", + height: "auto", + opacity: ".85", + paddingBottom: "30px", + width: "65px", + zIndex: 10001, + textAlign: "center", + }, + + eyeseeteaShareButtons: { + width: "35px", + cursor: "pointer", + backgroundColor: "white", + borderradius: 0, + opacity: 1, + color: "white", + boxShadow: "none", + textShadow: "none", + border: "0px", + textAlign: "center", + }, + + eyeseeteaIcon: { + width: "15px", + }, + + twitterIcon: { + color: "#477726", + fontSize: "20px", + }, + + shareTab: { + bottom: "-3px", + right: "100px", + position: "fixed", + zIndex: 10002, + }, + + share: { + textShadow: "none", + backgroundColor: "#ff9800", + color: "white", + width: "65px", + height: "38.5px", + cursor: "pointer", + border: "1px solid rgba(0, 0, 0, 0.1)", + borderRadius: "2px", + backgroundClip: "padding-box", + boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)", + }, + + shareHover: { + border: "2px solid #ff9800", + }, + }; + + state = { + expanded: false, + hover: false, + }; + + toggleExpanded = () => { + this.setState({ expanded: !this.state.expanded }); + }; + + openMainPage = () => { + window.open("http://www.eyeseetea.com/", "_blank"); + }; + + openTwitter = () => { + window.open("https://twitter.com/eyeseetealtd", "_blank"); + }; + + setHover = () => { + this.setState({ hover: true }); + }; + + unsetHover = () => { + this.setState({ hover: false }); + }; + + render() { + const { visible } = this.props; + const { expanded, hover } = this.state; + const { styles } = this; + const shareStyles = hover ? { ...styles.share, ...styles.shareHover } : styles.share; + + if (!visible) return null; + + return ( +
    +
    + +
    + + {expanded && ( +
    +

    + +

    + +

    + +

    +
    + )} +
    + ); + } +} + +export default Share; diff --git a/src/presentation/react/core/components/share/logo-eyeseetea.png b/src/presentation/react/core/components/share/logo-eyeseetea.png new file mode 100644 index 0000000000000000000000000000000000000000..6368320f3edcee7d769c80f3e7ca3ec35e43a060 GIT binary patch literal 16196 zcmZv@by!qi^fyY%08-L0lqj82gEWeRBF)e#-7`pcioh_S2$BNQLzlEj4k+D?bPCea z_wfC__ulut&;4VbnRCwGd&OCMt-U^LO_YWjl!SFryIOra5 zC#+PN6Zj&8D;vDU!V+M=`(S(L$$0`d>D`s|-L+k9+`TN^tg#fVT;Eu;zI3**wbrt> zu<}9lT1#VL$qv7KuAt*RvpX;6%DB}c`1jypQR!$dai7wKC4a>kR#6`b9IdVFmnw?= z`jSN7D6gSyWQr$ojTT-l3QrY{GR`G}tHQ8puyvmXkP~foT1*XmAtHZL;*!<|%PBHb6 zD)M|55+q}Iq^EW?`G~AHYC;i$6F}BwO?z+_@u;1GJi7SVlSZSsT8?%KJ3lVchhvA6 z3JPXgkE7sthK$jE(Y*GL&ne{v!w~7M8RJw)wr%96vu>FBf&e{pYCN9oJIRdSTPL7V>C>#>I| zYHUT(xilQB@?K%WG)K4JBiiWRv=8t_NEL!Zq#~DDoAzD=OnqSK)dv`^*A^nx7FyV! zilY%RB$Hu?L2v3t?BQAVy$Bd>Grv!EIC(aq^wkaT8%bAkEW=7noZlgPWzR#Bq$<~Nk%q_Fhki;o9ZPmZ$_)t?a)EpdJ zby{Y|_pBr;J^vL$IQy{>9p-$t&TyHXOk18*3}0!el;{s(_sfY)`XaA`RkGIP*P^&Q zP;zARnX`kS2C#xvRMb-tbkm%xo1q)jja!Ov+&FjSVx>S0WtgYiTpM@ji(Xbe1S<$t zwa@A(5Gj9qqeu$smVGalo#e|3&d%cqtsD#XPY~C{S_1JGF|gCAe&=4S!+Ed-`a}5Q zml)pN`$pbf`h$_sP)+#}=_EJ#fHpc>>$LY0Vb}TqpFX zF!|F@!cZCq3XB@h)IQW%@_3(G%wN09PN zS2Fvjjpst1FJ9+9owyIYK%;TUz#_AKl$uXEhwd;g@DBc&gV$YMYo6OqKaz*wNZrd} zf?q6EOS2A&KJ(hrjd+s*>Nb5Z7LVJ{3Jyst?nc<8R|+oQ+kA9F1>6v8gY+$apLo#n z=3X%zPaMxuhm4YcSVcJOL|Fs`{cTBsoDeykj2U|?ADLWAB8RmbDRf)uB=)ib+VqD?5W(U4U942*4J z7CAMeMh&f@5y`Dgc7&eCtkpES5JgP!i~3mHUsB=h?v)N;+f%hQl!RWRUIe>4HyuF= zR1@JPtSd7NXzl4+OgSOf`@M0R%1^ixda+;X6^7rOrMIyJ3O9^V@j&f()0DME=Zo7Z z3{AvPA_;J=NMI~roh$@78E~$&Jy;@p%|NAjt`)*RRxIo%#f+BN-iGU)C_gR$S=8AX;rs$vs( zW1FlVJAl44VLAA>W6oa*!`TZNmYxYqZrthKPH`NfD>jnLC^i^Ch0^d6>Q?H>c&hql z1H1&PmHO#K2|bdou6YItMN`O%5975{=*0fbh9-G}poJaEltXY!RbT@=nGhTKx;?C# z4#)Fa{jE=Z$5B6XA#((KJtYCA1Rfq;+?|d7kfa-z#6$>%6OjJGlsT`3vwzaOlP|6Y zkDuDn4HnlbB<@+4c?#HewuZM;LwA%3TlhDu|VCgx7 z#`r1obOCxtoFx$Y#lLuyx8dw_MGk!2!7TYrvKcd^7qfOjuG+!ek8_~o}-A>E#x*>O%p ztYC}M4;()Z;^5(A4R7-YNk_t3C{S>I4{n*E2gm7^&^xa6pNs%HU5BlFfroqT`AqHo!lp}V7=z5_K%Kbu52Qj1AQxFcdPJKf2e>VYSga{i`~(Vl2*%z>f#FjXqP~Qh}$x-#%__`FR-=x%lDSWV@ zx5K$JiS+Ox_m+iE&j2n$5-+a)@hmA*-WSD&NJOyv_q>ML&8PJ%)We=UXII z_YsJcyco%ocboZt=gdF@A-}ISNOq&(;SRL{yPNLWhiT{=LzXY2ByEz$paEHrNzx1N z*C43FXcY-&aAVx8F&K^G$DDqC&_ws?=v8%$Q5hk$aLN?7wcvbr(7uuNOA(#{W$Ka3 z(N=WYcLkz83j%bX>0YJ7Hy#gN6!d&HCbczo)EyzEqEN_?i?j@=(6IO>EOh&L{(d1i zJ1lYKT#FnHDnkE6rj>bT@Zqn+*+kQfv;WvK+TapHxh5()%Uh;op7sl>Y4@w*;=ZlY zu^^?zc{`Q0tp!n3Y3W~xyL}Cds1X$^h++HGElAr@RW?2E7jVfJ&hF8uBJoium(1ea z1!T^~N5K6kk~(rt1ETo0h34I{G@tFXhu*Fg8y^{M`-rX&zOYfEVQyujgv3?tIbJHI z<-3Y;pW0UPqzJXYy@PGQQFNOSv&kc40P0lX$FGycoN6f#bR3qMRJ0flWE&+m?h3*p zDNXV;?DD))T*TFr&_BIpn%5+ys8Jea2|7#zqWC%wl8$<1R&-zkVTb0w1c!_1iyL9s zz5-;wIwC5JC5kqDm@6z3*|7qOcxaozd_RI6tC{Sl zLgqRM1jW)njN-roPD)tq6jc;2HL!tSxUh0oVA?AATLrOOm4PMw+iIEjrmi0291g50NOpT4&?BN?r2L zUQX{{#OG|Mh$d;fjvz-)29%C&|4t`ZmljnOdVuEXD>Y2iVl9ZE zg;MtkxbZg12@E6hVV`~U)x>~9zXgaziaH7K>}*c7Nt}%q5$sNs?<0vV><;e-{Pkr1 zR8*C2|6u98P`dh;C=Jn&du5#e`oa#MzXd@r^q9Jv%Cx3-YtdSek$#~$6$J>HlGNaj zKj{ToqPpB*dwW(=8XQe8ZmRt^Y+MDY&mbdL@?dL@QTfCL0lYlKt0iSz9E4y*wmhB! zH<&GAMJUKG=W*7sg5>ZC58x_)U<7DFt3D_AgaiI}>XA*08jv$g8khxcs{8L@IguaX zny!|&u1nN&o;yAQXY1ptVwHNvw|?)(b>jmoKyYN(!PrydU*LGbWPgaESex`q%893< z*sS2CXMq1DM`5{tAe6+a$UPz>1uj^@*uViZ7@k{s@9yE>Y=Ts*#wE_htC~>}UBHm) zhPOO^{XeGjzfHgcY)U?78V_r48yM}LMV+PXgoA}ojtYj~J^Fvm4Vd%)bL;=63yf7> z?7L&nqRlMugmdZ6W}iCq0Bo)Ox5)p8Bv$(ePpNUdA$nVYDVnP_y`V-a6xI-+s{du( zKoFRCO)1ZrP7EbS{fHE3y1^xa-iiGm#SwiGRjdplY7}q<>>_}cJVYx&1-J*FIAPI% zXr;7XZL8ZFV$W^<&#b#JxVv4-5$w6sK^_Iqb?9co{!hAKfxBJXT*UFe=stDg0cU&L z6yvm0Bu$wCu>#Berw&B%!2RwH%{9R22#^?wQ=+i`E|HYM!#}1w-~gKc-K`60e*rKB z0@(dMM~wo00H=dw2OPV+1-`BaC@iO9i8OBzG~GP-C!)8dxR{qkBJr|ULeAs&_=^7) zj)V!~fcIkEI^Hzq^k{OSeV^srE_Gk9^rC7BYIi0#*^ti~;P3}SEYTks zg_b~%tFX^6qMy2e^S1&fy0G|IS{k2zJ#%aMP~nHIgD! zo7$on=WTMR8*?%T*2zjbHL9MNpZorXLLrZnm$_PXgg~c`2`R1VSw{7|Z3cU-{kb9L zC_+g3!2Wc*J|l9?Vj>6eOSTlDBdJwa$KOaCw|`>AMs6$neL*+rsBCJ5Dhb%;)8l!M zXag?OBiKEjI{wQzQzGgF+_DqsSAu&wm|rI`r^G11&(}+Y3tXg4i4pqRbu3EejbMnAr942%(nul^Cnby9|6)c>h2mW-q@kqTKo0d~& z`bc;#A#HdS>5owzHno~3{L+H&xF4}d{GOE@9L%}?9&oP_R%8^euQ~s+QFU@2U+(%q z==urzq>Fg|@L6Pa$-ljpH3GiKt@d);=}Upr>f*(#(let!{a#zW&knho_B>Un)-&?NfuClre-zLSREFisz2W)Rt(m1Abg*!m)=SNrW37(71jHkKI zmPz)91KybUtExJcTp*vIdVjg4Cfz>7Hxk&S5BfQQ*Mnj^H!!O+p+dpmvJPZs!3&>S zg-rq|m?vYSnRZ;i_o(pvWC|Jxzz=0ee=>6H9eX}O!SO=gXBpq`EvLQt2V+LDFM+P_ zcb?NNE3;~a=oy4zxJIA$B0O7Ce(3o?n2DjA>a3(yXX;PAvy=+k{9S*)q_l0xXYvxb zymDt;QqELF+;pDcmMVmG8va}<=}IF75?0$p$WMlU?jHJ@sxQ2D5PA}?Xi*wLT%^@I zF50k%rKKE4@#%I%OCT?j&L)>ceAN;TPHrw+An3IVWJ7wQ10G*MNXd>@~MB%8hqnAS&4YL^j;&qT$L)ig6d;R9&7uDg@|RY z*=tRyA&H}nY-ZxS=Xl|{@Lbw#NMBh&Ik8f`kuo!V0u{=glWx(DkP8_l)&2p6mmBur zCt>dgkB_tG&nbuGH&U^PXgjD}57P$ix3d)>BTvLl-W^K6Vv~H?q;i*@$Ul%$$ymdd zRJ(k@4z{l^;s}1#Y1(O45D#`BT~ZG7Nn+9w=w>GXQri*#f>bOXv2h@eFuE#n7wzGJgwXWn;3G%Qv$8U2_l$Qex#~gEzse+D+#_;7No$=S zCE|h3&ExF1OaSzO+_OBKUG0rIy9y+n-8pGOYQ~m2+VU4<>HRi^h8Pi$nEv)}q*Rf# z8*IBc#*pgPH`VF2p3i;(Aea#*Kn$JOb_!Xmbi8zEJH_0=UGi4ZHtu>uLne0$;;ola zUm`~pGKV~-1b~1o4im-SR}`o$w{XSuS!yU`MAN>x^CRih{Bt191bh%V#5J59(z~SQ zUPJ_K9MFDhZu{T7NI;jRTh0`yddhn=n_ASU@o$8 z)Ur?hgG=K6E+_?dK*j-#V2(TNEdWJSxfeKms z!>ll_&qL|9xgK(TjHxVN4d7QG>qJ3t`?K8e#@U-C?zs7umqv(r3PgXC>vdP#ls~JR z-}RVPAA{SN#30=Oq%sWHrbR(fB;FEeR%KqbyavzZ1~>%ZD4um4l3>2U!;SRfM*BL! zbH|lAEOt1)tQ`E!L-&pR(pr-+Rm*3yebHcn>%e%QSFh+Z2ZM^D%gLnnjBe?wpMQ6ED>P2 zeI*iYOR-ll$=X8P9~a<|fLH=4nRu@#UIsvenq#PfDc;%Ir`I}W&RJG**>7XFj}Im5x{vjaVwN&0JYf)L=g4Jg~&x-6HD-b~O zcDONrI#?Jdlm`~rqy#)lcZB=I#~5HUbtF_ZzTCgwjSSoMb#gV0vferii)f_@3QUW7 zT|nIWMg5Af`na+AqV;yaBzMr;6oz`L3kw{s6?nS@>Lp zqMP?DzB^elry>qA*V-g`H;e|=tp#=_q$c^&t=*nQ_Weg zP0FBKFbtl+oiv8+!tI-9Vv)>haiGj!XCI^9p{Ym3iUH>z{5%NNQ-czMv3g4#Y84wUo`B*j+{(*CTc~m`9X8wF7 zdB;XgnJ==eA;($J)Q1bDRcpalIW16iu9e2$Xgu1P@$XW}E2{Wz7~0oAO%c&VwDOb+ zlTtSNEu|bPv+(3m*7}+P0>6k@IefS3s-5TEti;8fA5);Q8>oyr z3YK12_GZYBpIb}zPGoUq@1MJ$X@}EOHD==5tiivxNQLy_+S@3%_iWMC67n>ggS7O> z9T_yT&A3Z0;p{csbdl40BBV>s zL_KNf9s~`U{kPv{5XG2K;YQ8Y$p@bdlpcb!W3eT*#kAz9eUb@sw=1km-iDu`M6tc7 zC|ws;B@8jCifbSfH1i7T_Crz@)<#iux;+tqg3s$yYQ*c`b5F972Vm`%5&hw!q%2lj zj!mG16Ad7)m5;z7WW^YRo6}b+aOhyH1dx=cCO2H@iojJWThi$P{Qaq(jA>&%fOqkn?gapWJcNxs0V9gc5wM-(N?#=6(R^{qS1A}ou^iZ@Dd`=Ncf;gQ zdLGOdwNof3y#3-x_~h>AKY3$vLD6UBTWZv(A6h4NZlusg?WV2;w8mKPaWzZ#o+N?c zSFha+v>To{o%Mw5Y3v;%P9GcfFos|CNdIK;0QDB!Cmb94ms4bdM}vN7`o`X5cB%la zJC}9EpX5C^$&lgkg<$175M^;@_2Q~upE-Q)g5(ea{<&UBe%thqz%Oy2>O_#!w>w_u zlXyC&1i`5*Va{o!bBr2*3_6_-F#>hJY0i{>>_%2!tatW;CORm2>=!9};}b#eLNy^c z_^)C*p@v^;-^i>)*=*9zqcz5RPpetf4(k9WVI{UYk~oiJdcn-j#MWm!66X(KUAf*4U1oa|1;)eY$h@olci<7)W z2aC*~Lljk!a^fQmgFv7&+j&q8HgI+WHofEa7rUreT^~u}vfezlL=ELqj@%T(Ds6dyT8$>Y z66_2v=Xbi_1z=s4lnR!+Kik+i0>RHibCB9Yy_V!OA`oEiz#P2Cu2A}E9-4hJ0^X7V4KL#j#e#2WeCF+4b_CJrg z5<4v91_n*UBZYfx@dJA&x#>J|0A1)3ta3G7KF>&Y7a4eVpdSO=D#NpSJr?CMK~VQ^ zQC+(hGWro9=!i|f5}$D8k!1)d&7bZgfCCecKWwM?-4Q&e4qJUT{?Nqs5@OD4FOg<( z8$>&?;5ZHfb;aN-VrDaA^|g(oinfTEbc zl&iM>zz8qS5g*xrN+uNu%Eb#}$2uEztoSoF3rAQi;<{R_}(9ET8)JuVA(3ufc z_^fU+#V5`KErU>axQ9jk=T@eCApHzuzWxUk!XK1>ySLU7bS>>-emWA{XL_-+6y5q% z|H7$`d9vkVUrs%P8vr56-*-gF`xh!L_+%&^=4P`&$~QM)*IDlvQ${0({3W{62nxy^ zmOvSWq$}6obq#pCxiLPVkCrBZ~7jJ+8U&S9t#1HmyLYIBTLEi)F6Y>`w*UP z{pZi}mqBJ!Mf!FUP@N6e026j9HvjBE=vYELx0XdRLDc0504`1{xCDaXEei#uMSc|+ z4Hek*f$RZ&5C{7=IUUZ7CX>S7q=xZ%!P%Sg;9Pm+Brd;JwBSy1RzjrZ+-~#MFI8ot zpM_lNV7EW!mylGt=typIkF+G$WP!O)oW4()F}$tZ@3YbixnQUYXVw=IvmbaW-DrK* zAR3Yrqo?OP5Rn($U<)O#y#fvY5;2D(d*=8=)AWcHsA++N7WE=IfR6A|k!f?ujxf~5 zWTo1@GwdR7R|p&xD(7J5a=zAMAAFm;tpQ#uFVqSYq+_H;!Dc6SdAs7<0ki%U=Jrwi zmHe7A6j!*L5?Z7s`h^SJ@w(cz1`@^DXdLG(s=;rG`ot6s-LiImQ+=(H$e6(RwmEUC z-G%QF3l-60>oGDWY|(xWEyQ_EX}YX|_RrmPx$1F%B zO;UA!3|$@TcV3XmTbmTq)PLSK@N7{#RMrMTU!5MA-Pj2R#U4${Lp+lla-`nO-6~0I%ym}Zl*BACljx1 zS)(I8=7u2;-Em74$J!XX4M&ixZZX`dNdsK6sc3TM71^!{q51K=%(si=ZwetW@x(%6 zVDTo^`4&_Zrh~aub0=$2eho*Htwm7YWGdg zj0fHNtiMAeV7ZmbOH(@<>FV`93G*($>YxW=x>x5qJ5}r^#r^1qmV9m{+~Jmj#a-?S zzMdFCF&#-yb>FIJ_-lqM_nr1DW5I~6$~bX>cn`J zgFBwbREHID*p(#L-Z+~z8AMo~g;n-AAcn)H6l}XPcMPo^Io4_v1t)WeFnjObI9=~&*9Z=*qR`v;TPVhX(I?iOl^PQgRD%-mZJaFGlhbSw0*0()`5?woAo9aedO zRw3AE%h^;+Z9*ru*td`wb5f{+%?+NVjTYYGAe~`k;fL8m(9F|H+uudh=D`U}gC~6R zg@cE|yPfN6?Ug2N5asdZm3IM-XWEaOjAdSk!Iu&oVChd5*R8YLg|-bK-x?>M|V9pEE5@q0CF(if?}-ohb2< zJ|(P*byt|EHEi{esAet%zLmWZRmd})N1VUdz|n$CupldCh0tgM zri)7E(jsPdurtw+@hFF$u#VreG>)N9wYrrDY4{+FZA!@vDCYI$g=h z+OFf?I)}CP-wov1aSwr3yToTXU(AtGD;TC*VY%x+<+nG(FVD`~I5o4S-Ka!WOs5we z$GW!6yLCnFYU=lF{7;5fXdqWjFttUYyVrf{V$z&O6gEGXD@5G-kqX4lnsC zlxZW1>Fv*x=6p25V1-9{N#9wBv}y;D@m)tXuHV=>w)$P58OQo3zh^ZLVFynz3`UfI z(2eHlgUSI@)hq!fiQcP)5q5SR|g##8ppMf zSn1$*1|qh@PzN?4e9dqbFv#Ks zv%gy}6cHbKkUcTwOss5p1dmT!rllh_&WmtUY^A)T5old)huC~kB!B6TR#nRP;g-+Bgawve@m5M@jH=%b zSlbxCge*Aiayak`j^5k*)mHzx_A(`fvq(iBA}&P7zt`H-`)&_r^otOqI&L}le9Lrx zS9(XyYrE=_n^h&PjR)W_L)@dhJ7Tcr`lH|%RkN$0XY$?n3!^3zjLw^CGMhgJX+`u^ zW-U}8MBR18d zYINF-2PsqQ>P<${$lh=P{Aq(reBGc=+zxoZH*QW1iX49Up4QUjGW|kFSMb6+j2{?6 z3ocNlPU1~JKP1&YBW@nu8zIZ_JITl)SwV8j{5vO$1_Ld1p_MNe{`_dQxYG2{NVlI1 z%=+S0{{3~emtKlh|Dsea*UHxkIU;D=l>wotmW;$L8F;kMEE>Ez@a5&>P6c+kJA-wFTTs@v3Os7o13nwm}BoE+6-Z(iVHauk_MQi4T# zW=?8ipZI&8U>4^u4DxsYuBv*gn;xU)+lX?pc8)Oj3WGSj~ToJtqH+Aw42{zCT(j&bjXl$p>0dFV<$45jQDJ z#640e#|M!E9gh@qh*w4>@N<|e#z|d{;H#$Za?G7dZFjyUQ^p%@y%l^p8yBnERxd~w z(<;1c0?F@XqHLQoTqNo-@)=*Zm7vj(G5z*$s-By!fOMZxX3E1&&8!JVfTG6U8ffss zp6(`mI*XUXc>M?I<>Ln3Z$G%xdLD5^@NP;>v#Jj5v>fMVjWw}in|{e<(;WDu)=k0{Ov#gVfNcMj5oIGiV_6b{+ZCob!< zl*l{=VyM?B&Y`c#Vg&|!dM64ZK_|9bpX^HL#X+ZX;+?i#S+|s|7l*+5@>I>2BK{~P zI$)O{PFermx7leZR@6}xDs>!c8aJZp2i zzFOZoO4Lpt=Um~M7s#l$68bBMj_U0yg40e!kjaO0Vy5@X(v;Mu`lke z>Pa94yH$;iW28ONPst`^Dxcl4&DyB`-Ap1u4Btw+^3704T5f&Tj+V2o{0oU~TYK9t zPO3uP{j+@YeVU~ofYndu>m*)}rFc~bjxT3wj!%^YBLyyN=L@;%R+g^bZN!=!p^aFaZG8e5Z>9CnLJ+yghIw zroHm7Ig-UjHfM+H_S#gz%MdA%q` zS2hM4djurM@vBLMzj4obA!ct8L<}4eA}rD5oV=t0#ZOJmINUz;*qi z6H9&1%6D%3itasg#GrUAf4(_VBg1;q(Uk(VHpf=Jh2p9LqV@GmR_e~mh7y2!GjiHrnTKabV~>uKX?`9{STxT|jEx8{S=*E;*~SVgwj4YPS#kD^2b6*o zcu?I35|%pPDt|cWauM@b?_+_z4`F{XGiK{uwCBZ>a%Ob~6Ult%B^Rl85bnQ5l5WlM zo#7c%o&Ht!x;xRYsy3sf(B1ET3YHwCcx`bD28>3#j3-FVA>@8`)W%Eg)YPmrSiZv- zc6{1sh}ro<+d%a(K~(miwddKqr|G(x<2t*YyL@*oZ<*YNVUEYUSW=Ai)bid=*(`4? z@l_J4+P7eLR2s2DGjCh%toQ_f?d3M&!5ktxt%IosgC4IVa^C(RH&DTM$Sr_BWmxi*6F>{w^ zx5(@iTEnkmH+hI9V1Z61JrA6Zf$5xi~DYIus2xd9=GN3j4^}bQ4$f72G+hAZlrg?$NZV4(9M% zNQU!eOLU+wRvUpO=UnO|w84Mi*UJT0Sk|aG@>0>HhWaN>aI7=I( zF*bs-0!~`iUI--g^NyM2)kVIE*JfF<+>J}pH=gR(B|fK&nvzLV8I8Nr<;-WqCCnb> zTH$p?p^yce4W_3CN~tBIdG;q{;y{8K%P>4K z(Xtryzi+SAHC6e%&2$|zp(AqqvTPu-sa?rT!0DG(jAqcYhc z9D6?AGK&d&xIdtMw%xCZ@6$ysgqYgkdn%Iqc<~I5&lj^=lbZ6USwZ|j(F6-%!R^!rm|N7;5 z6|V#E)69v`prQl;$o^{h0=kYA(K_jpZ8YMv_jKgLu2get(RVf8a~iL4x14M5_R(Ek zDDE9@9^R5!L5|UX;=1#>CA!TLY9GE}zI$;x|4>y5#2CDCdZ@KAw3_{$-Bp zeN3Q+*7#IyG#3Z8YSnVVBAIrC^Sq{4z+<|tRPQIsjh;*e72w>R>6`ZAvLDi_i$-`$ z({5f{6B1ZTdMKt623ncHVho~fd?oOL{Ez9dO!=WX*#4fcp~)UU-kK@VU`f_*Q-%#3oZ{W;VU3WXb&Fl>;G?iSml(__S`;!7!1#8INSAdFH#P`mrpE~k=g8#^rS zGfow?TB$YOQhhXW#|aHQ;cIJQuv7CtV`*#L@Ri6z2ky|}3EN51!&8WWFslqfl~0@9 z2RRTX5n1iOY0zjv9zRc$*`a(#4<^>#HADGWmPSOMc;7+AaRbjvBy@PCKps*iHsbH7 z`pR&bjTN6=BgDuVXJ|#~w#ig^g+9LeYik9k&)9m5G+jX|whjv2Ja*pqpWI(`87Xp+`(B1P)pAM+v zsN%(m$dU4Go~iMc4+k<@F4nia^5%@Mu0G2}m(AMS_{5q6g%~g_*6XM~lxXA#;EEM& zL?55G%SVNsBHl(5A4*a95Il;4a4b`;DACXM@rPUa|M{G1AcV7~2atIaVB{wM!sFt( z2=R-oIry>Nms8+;yW865M8irKvfLqD>RbaekitEDRGo%0$9q)-(E3B#Go@-9Z$J$2 zS~^!hAnY7)N~Q|y@484USWNV+RC8oo*d2xejm_iWuj%EFlz7HzL;jUz%jK>JWx{=C$+oGDL2iN zvB4%P1)J!nK5lnNaxwf_!fJcYdlM@Tz?*a-BkhPn9|q2Ef79|gb3gGo8!vRbOmK!$ za6JYe#q6^lJwN9lSWSB(L97qz*))x{%qFQeShVqM!G2W+sIH$j4Ijx1>Y0LZq9Qr= z(IRFYTy)q=%BLQDb=-a&jzqnn<-w|TXH(|I!S7jWn%37bcIX#|DGWb3nZ>Z+taI2QQzfGT;>E0^0vJ>d&!L z7#+mtykIiyBq^@on(5qYltaJN5CYxg`mljqUX$#U_J2j`EJ{xs&B zDR)!6#@*5ElMzDr1nZ=7y$6))6Mc`kyGVUq*p zCak-j$p87#^Ykc6oH8543Xb?XU2r#sZ-Iau^{8As5i(AN)m}OHb;tdqA|hWBE8H?O z!*+5dinE&%J3NQyz;|LRWgTdf4>)}adrzr~tziav5#eU0J+>_k0|q_7PNhH%TW_SG z#$z7Sai;W&Va48uqD#pOQV`FByCZX^khU7Jl&0 z$~v`;kisUaEU85HrpUVHJ0Fl~J{D3ay$FOo#SNAS zx48?Y{pWs#O^&CcErUsBN!nOIo0p3Lf|TaQu70_k|26}r3-i!C=U3wx!6%YuyJ^M3 b#j^OIh?chRB?JBekM&YX?RlA^dFcNIZM>@f literal 0 HcmV?d00001 diff --git a/src/presentation/react/core/components/sharing-dialog/SharingDialog.tsx b/src/presentation/react/core/components/sharing-dialog/SharingDialog.tsx new file mode 100644 index 000000000..5c0eb8702 --- /dev/null +++ b/src/presentation/react/core/components/sharing-dialog/SharingDialog.tsx @@ -0,0 +1,44 @@ +import { makeStyles } from "@material-ui/core"; +import DialogContent from "@material-ui/core/DialogContent"; +import { ConfirmationDialog, Sharing, SharingProps } from "d2-ui-components"; +import React from "react"; +import i18n from "../../../../../locales"; + +export interface SharingDialogProps extends SharingProps { + isOpen: boolean; + onCancel: () => void; + title?: string; +} + +export const SharingDialog: React.FC = ({ + isOpen, + onCancel, + title = i18n.t("Sharing settings"), + ...rest +}) => { + const classes = useStyles(); + + return ( + + + + + + + + ); +}; + +const useStyles = makeStyles({ + content: { + paddingTop: 0, + }, +}); diff --git a/src/presentation/react/core/components/store-creation/StoreCreationDialog.tsx b/src/presentation/react/core/components/store-creation/StoreCreationDialog.tsx new file mode 100644 index 000000000..4be2bd5b1 --- /dev/null +++ b/src/presentation/react/core/components/store-creation/StoreCreationDialog.tsx @@ -0,0 +1,252 @@ +import { + Button, + ButtonProps, + DialogContent, + Icon, + IconButton, + TextField, + Tooltip, +} from "@material-ui/core"; +import { makeStyles } from "@material-ui/styles"; +import { + ConfirmationDialog, + ConfirmationDialogProps, + DialogButton, + useLoading, + useSnackbar, +} from "d2-ui-components"; +import React, { useCallback, useMemo, useState } from "react"; +import { GitHubError } from "../../../../../domain/packages/entities/Errors"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; +import { useAppContext } from "../../contexts/AppContext"; +import Linkify from "react-linkify"; +import helpStoreGithub from "../../../../assets/img/help-store-github.png"; + +interface StoreCreationDialogProps { + isOpen: boolean; + onClose: () => void; + onSaved: (store: Store) => void; +} + +const initialState = { id: "", token: "", account: "", repository: "", default: false }; + +const StoreCreationDialog: React.FC = ({ isOpen, onClose, onSaved }) => { + const { compositionRoot } = useAppContext(); + const classes = useStyles(); + const snackbar = useSnackbar(); + const loading = useLoading(); + + const [state, setState] = useState(initialState); + const [dialogProps, updateDialog] = useState(null); + + const onChangeField = (field: keyof Store) => { + return (event: React.ChangeEvent) => { + const value = event.target.value; + setState(state => ({ ...state, [field]: value })); + }; + }; + + const validateError = useCallback((error?: GitHubError): string => { + switch (error) { + case "NO_TOKEN": + return i18n.t("The token is empty"); + case "NO_ACCOUNT": + return i18n.t("The account is empty"); + case "NO_REPOSITORY": + return i18n.t("The repository is empty"); + case "BAD_CREDENTIALS": + return i18n.t("The token is invalid"); + case "NOT_FOUND": + return i18n.t("Repository not found"); + case "UNKNOWN": + default: + return i18n.t("Unknown error"); + } + }, []); + + const testConnection = useCallback(async () => { + loading.show(true, i18n.t("Testing GitHub connection")); + + const validation = await compositionRoot.store.validate(state as Store); + validation.match({ + error: error => { + snackbar.error(validateError(error)); + }, + success: () => { + snackbar.success(i18n.t("Connected successfully")); + }, + }); + + loading.reset(); + }, [compositionRoot, state, validateError, snackbar, loading]); + + const save = useCallback(async () => { + loading.show(true, i18n.t("Saving store connection")); + + const handleError = (error: GitHubError) => { + switch (error) { + case "NO_TOKEN": + case "NO_ACCOUNT": + case "NO_REPOSITORY": + return snackbar.error(validateError(error)); + default: { + updateDialog({ + title: validateError(error), + description: i18n.t( + "There are issues with the connection details you provided.\nDo you want to proceed?" + ), + onCancel: () => { + updateDialog(null); + }, + onSave: async () => { + const saveResult = await compositionRoot.store.update( + state as Store, + false + ); + + saveResult.match({ + error: error => snackbar.error(validateError(error)), + success: store => { + updateDialog(null); + onSaved(store); + setState(initialState); + }, + }); + }, + cancelText: i18n.t("Cancel"), + saveText: i18n.t("Proceed"), + }); + } + } + }; + + const result = await compositionRoot.store.update(state as Store); + result.match({ + error: error => handleError(error), + success: store => { + onSaved(store); + setState(initialState); + }, + }); + + loading.reset(); + }, [compositionRoot, state, validateError, loading, snackbar, onSaved]); + + return ( + + } + onSave={save} + onCancel={onClose} + saveText={i18n.t("Save")} + maxWidth={"lg"} + fullWidth={true} + > + + + + + + + + + + + + {dialogProps && } + + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, + helpImage: { + width: "75%", + }, + center: { + textAlign: "center", + }, +}); + +export default StoreCreationDialog; + +const HelpButton: React.FC = ({ onClick }) => ( + + + help + + +); + +const DialogTitle: React.FC = () => { + const classes = useStyles(); + + const helpContainer = useMemo( + () => ( + +

    {i18n.t("To connect with a module store you need to:")}

    +

    + {i18n.t("- Create a repository at https://github.com/new", { + nsSeparator: false, + })} +

    +

    + {i18n.t( + "- Create a personal access token at https://github.com/settings/tokens/new", + { nsSeparator: false } + )} +

    +

    + {i18n.t( + "The personal access token requires either 'public_repo' or 'repo' scopes depending if the repository is public or private" + )} +

    +
    + {i18n.t("Create +
    +
    + ), + [classes] + ); + + return ( +
    + {i18n.t("New store")} + +
    + ); +}; diff --git a/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx b/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx new file mode 100644 index 000000000..b74949698 --- /dev/null +++ b/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx @@ -0,0 +1,54 @@ +import DialogContent from "@material-ui/core/DialogContent"; +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useEffect, useState } from "react"; +import i18n from "../../../../../locales"; +import SyncRule from "../../../../../models/syncRule"; +import SyncWizard from "../sync-wizard/SyncWizard"; + +interface SyncDialogProps { + title: string; + isOpen: boolean; + syncRule: SyncRule; + task: (syncRule: SyncRule) => void; + onChange(syncRule: SyncRule): void; + onClose: (importResponse?: any) => void; +} + +const SyncDialog: React.FC = ({ + title, + isOpen, + syncRule, + onChange, + onClose, + task, +}) => { + const [enableSync, updateEnableSync] = useState(false); + + useEffect(() => { + syncRule.isValid().then(updateEnableSync); + }, [syncRule]); + + return ( + task(syncRule)} + onCancel={onClose} + saveText={i18n.t("Synchronize")} + maxWidth={"lg"} + fullWidth={true} + disableSave={!enableSync} + > + + + + + ); +}; + +export default SyncDialog; diff --git a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx new file mode 100644 index 000000000..4e98471bb --- /dev/null +++ b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx @@ -0,0 +1,201 @@ +import { makeStyles, Typography } from "@material-ui/core"; +import React from "react"; +import i18n from "../../../../../locales"; +import SyncRule from "../../../../../models/syncRule"; +import RadioButtonGroup from "../radio-button-group/RadioButtonGroup"; +import { Toggle } from "../toggle/Toggle"; + +interface SyncParamsSelectorProps { + generateNewUidDisabled?: boolean; + syncRule: SyncRule; + onChange(newParams: SyncRule): void; +} + +const useStyles = makeStyles({ + advancedOptionsTitle: { + marginTop: "40px", + fontWeight: 500, + }, +}); + +const SyncParamsSelector: React.FC = ({ + syncRule, + onChange, + generateNewUidDisabled, +}) => { + const classes = useStyles(); + const { syncParams, dataParams } = syncRule; + + const changeSharingSettings = (includeSharingSettings: boolean) => { + onChange( + syncRule.updateSyncParams({ + ...syncParams, + includeSharingSettings, + }) + ); + }; + + const changeOrgUnitReferences = (removeOrgUnitReferences: boolean) => { + onChange(syncRule.updateSyncParams({ ...syncParams, removeOrgUnitReferences })); + }; + + const changeAtomic = (value: boolean) => { + onChange( + syncRule.updateSyncParams({ + ...syncParams, + atomicMode: value ? "NONE" : "ALL", + }) + ); + }; + + const changeReplace = (value: boolean) => { + onChange( + syncRule.updateSyncParams({ + ...syncParams, + mergeMode: value ? "REPLACE" : "MERGE", + }) + ); + }; + + const changeGenerateUID = (value: boolean) => { + onChange( + syncRule.updateDataParams({ + ...dataParams, + generateNewUid: value, + }) + ); + }; + + const changeMetadataStrategy = (importStrategy: string) => { + onChange( + syncRule.updateSyncParams({ + ...syncParams, + importStrategy: importStrategy as "CREATE_AND_UPDATE" | "CREATE" | "UPDATE", + }) + ); + }; + + const changeAggregatedStrategy = (strategy: string) => { + onChange( + syncRule.updateDataParams({ + ...dataParams, + strategy: strategy as "NEW_AND_UPDATES" | "NEW" | "UPDATES", + }) + ); + }; + + const changeDryRun = (dryRun: boolean) => { + if (syncRule.type === "metadata" || syncRule.type === "deleted") { + onChange( + syncRule.updateSyncParams({ + ...syncParams, + importMode: dryRun ? "VALIDATE" : "COMMIT", + }) + ); + } else { + onChange( + syncRule.updateDataParams({ + ...dataParams, + dryRun, + }) + ); + } + }; + + return ( + + + {i18n.t("Advanced options")} + + + {syncRule.type === "metadata" && ( + + )} + + {syncRule.type === "metadata" && ( +
    + +
    + )} + + {syncRule.type === "metadata" && ( +
    + +
    + )} + + {syncRule.type === "metadata" && ( +
    + +
    + )} + + {syncRule.type === "metadata" && ( +
    + +
    + )} + + {syncRule.type === "aggregated" && ( + + )} + + {syncRule.type === "events" && ( +
    + +
    + )} + +
    + +
    +
    + ); +}; + +export default SyncParamsSelector; diff --git a/src/presentation/react/core/components/sync-summary/SyncSummary.tsx b/src/presentation/react/core/components/sync-summary/SyncSummary.tsx new file mode 100644 index 000000000..417ccea97 --- /dev/null +++ b/src/presentation/react/core/components/sync-summary/SyncSummary.tsx @@ -0,0 +1,319 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + DialogContent, + makeStyles, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, + Typography, +} from "@material-ui/core"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import { ConfirmationDialog } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import ReactJson from "react-json-view"; +import { PublicInstance } from "../../../../../domain/instance/entities/Instance"; +import { Store } from "../../../../../domain/stores/entities/Store"; +import { + ErrorMessage, + SynchronizationResult, + SynchronizationStats, +} from "../../../../../domain/synchronization/entities/SynchronizationResult"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; +import SyncReport from "../../../../../models/syncReport"; +import { useAppContext } from "../../contexts/AppContext"; + +const useStyles = makeStyles(theme => ({ + accordionHeading1: { + marginLeft: 30, + fontSize: theme.typography.pxToRem(15), + flexBasis: "55%", + flexShrink: 0, + }, + accordionHeading2: { + fontSize: theme.typography.pxToRem(15), + color: theme.palette.text.secondary, + }, + accordionDetails: { + padding: "4px 24px 4px", + }, + accordion: { + paddingBottom: "10px", + }, + tooltip: { + maxWidth: 650, + fontSize: "0.9em", + }, +})); + +export const formatStatusTag = (value: string) => { + const text = _.startCase(_.toLower(value)); + const color = + value === "ERROR" || value === "FAILURE" || value === "NETWORK ERROR" + ? "#e53935" + : value === "DONE" || value === "SUCCESS" || value === "OK" + ? "#7cb342" + : "#3e2723"; + + return {text}; +}; + +const buildSummaryTable = (stats: SynchronizationStats[]) => { + return ( +
    + + + {i18n.t("Type")} + {i18n.t("Imported")} + {i18n.t("Updated")} + {i18n.t("Deleted")} + {i18n.t("Ignored")} + {i18n.t("Total")} + + + + {stats.map(({ type, imported, updated, deleted, ignored, total }, i) => ( + + {type} + {imported} + {updated} + {deleted} + {ignored} + + {total || _.sum([imported, deleted, ignored, updated])} + + + ))} + +
    + ); +}; + +const buildDataStatsTable = (type: SynchronizationType, stats: any[], classes: any) => { + const elementName = type === "aggregated" ? i18n.t("Data element") : i18n.t("Program"); + + return ( + + + + {elementName} + {i18n.t("Number of entries")} + {type === "events" && {i18n.t("Org units")}} + + + + {stats.map(({ dataElement, program, count, orgUnits }, i) => ( + + {dataElement || program} + {count} + {type === "events" && ( + + {`${_.take(orgUnits, 3).join(", ")} ${ + orgUnits.length > 3 ? "and more" : "" + }`} + + )} + + ))} + +
    + ); +}; + +const buildMessageTable = (messages: ErrorMessage[]) => { + return ( + + + + {i18n.t("Identifier")} + {i18n.t("Type")} + {i18n.t("Property")} + {i18n.t("Message")} + + + + {messages.map(({ id, type, property, message }, i) => ( + + {id} + {type} + {property} + {message} + + ))} + +
    + ); +}; + +const getTypeName = (reportType: SynchronizationType, syncType: string) => { + switch (reportType) { + case "aggregated": + return syncType === "events" ? i18n.t("Program Indicators") : i18n.t("Aggregated"); + case "events": + return i18n.t("Events"); + case "metadata": + return i18n.t("Metadata"); + case "deleted": + return i18n.t("Deleted"); + default: + return i18n.t("Unknown"); + } +}; + +interface SyncSummaryProps { + response: SyncReport; + onClose: () => void; +} + +const getOriginName = (source: PublicInstance | Store) => { + if ((source as Store).token) { + const store = source as Store; + return store.account + " - " + store.repository; + } else { + const instance = source as PublicInstance; + return instance.name; + } +}; + +const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { + const { api } = useAppContext(); + const classes = useStyles(); + const [results, setResults] = useState([]); + + useEffect(() => { + response.loadSyncResults(api).then(setResults); + }, [api, response]); + + if (results.length === 0) return null; + return ( + + + {results.map( + ( + { + origin, + instance, + status, + typeStats = [], + stats, + message, + errors, + type, + originPackage, + }, + i + ) => ( + + }> + + {`Type: ${getTypeName(type, response.syncReport.type)}`} +
    + {origin && `${i18n.t("Origin")}: ${getOriginName(origin)}`} + {origin &&
    } + {originPackage && + `${i18n.t("Origin package")}: ${originPackage.name}`} + {originPackage &&
    } + {`${i18n.t("Destination instance")}: ${instance.name}`} +
    + + {`${i18n.t("Status")}: `} + {formatStatusTag(status)} + +
    + + + {i18n.t("Summary")} + + + {message && ( + + {message} + + )} + + {stats && ( + + {buildSummaryTable([ + ...typeStats, + { ...stats, type: i18n.t("Total") }, + ])} + + )} + + {errors && errors.length > 0 && ( +
    + + + {i18n.t("Messages")} + + + + {buildMessageTable(_.take(errors, 10))} + +
    + )} +
    + ) + )} + + {response.syncReport.dataStats && ( + + }> + + {i18n.t("Data Statistics")} + + + + + {buildDataStatsTable( + response.syncReport.type, + response.syncReport.dataStats, + classes + )} + + + )} + + + }> + + {i18n.t("JSON Response")} + + + + + + + +
    +
    + ); +}; + +export default SyncSummary; diff --git a/src/presentation/react/core/components/sync-wizard/Steps.ts b/src/presentation/react/core/components/sync-wizard/Steps.ts new file mode 100644 index 000000000..dea4c8e4f --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/Steps.ts @@ -0,0 +1,200 @@ +import { WizardStep } from "d2-ui-components"; +import i18n from "../../../../../locales"; +import SyncRule from "../../../../../models/syncRule"; +import GeneralInfoStep from "./common/GeneralInfoStep"; +import InstanceSelectionStep from "./common/InstanceSelectionStep"; +import MetadataSelectionStep from "./common/MetadataSelectionStep"; +import SchedulerStep from "./common/SchedulerStep"; +import SummaryStep from "./common/SummaryStep"; +import AggregationStep from "./data/AggregationStep"; +import CategoryOptionsSelectionStep from "./data/CategoryOptionsSelectionStep"; +import EventsSelectionStep from "./data/EventsSelectionStep"; +import OrganisationUnitsSelectionStep from "./data/OrganisationUnitsSelectionStep"; +import PeriodSelectionStep from "./data/PeriodSelectionStep"; +import MetadataIncludeExcludeStep from "./metadata/MetadataIncludeExcludeStep"; +import MetadataFilterRulesStep from "./common/MetadataFilterRulesStep"; + +export interface SyncWizardStep extends WizardStep { + validationKeys: string[]; + showOnSyncDialog?: boolean; + hidden?: (syncRule: SyncRule) => boolean; +} + +export interface SyncWizardStepProps { + syncRule: SyncRule; + onChange: (syncRule: SyncRule) => void; + onCancel: () => void; +} + +const commonSteps: { + [key: string]: SyncWizardStep; +} = { + generalInfo: { + key: "general-info", + label: i18n.t("General info"), + component: GeneralInfoStep, + validationKeys: ["name"], + }, + instanceSelection: { + key: "instance-selection", + label: i18n.t("Instance Selection"), + component: InstanceSelectionStep, + validationKeys: ["targetInstances"], + showOnSyncDialog: true, + }, + scheduler: { + key: "scheduler", + label: i18n.t("Scheduling"), + component: SchedulerStep, + validationKeys: ["frequency", "enabled"], + description: i18n.t("Configure the scheduling frequency for the synchronization rule"), + warning: i18n.t( + "This step is optional and requires an external server with the metadata synchronization script properly configured" + ), + help: [ + i18n.t( + "This step allows to schedule background metadata synchronization jobs in a remote server." + ), + i18n.t( + "You can either select a pre-defined frequency from the drop-down menu or you enter a custom cron expression." + ), + "A cron expression is a string comprising six fields separated by white space that represents a routine.", + i18n.t("Second (0 - 59)"), + i18n.t("Minute (0 - 59)"), + i18n.t("Hour (0 - 23)"), + i18n.t("Day of the month (1 - 31)"), + i18n.t("Month (1 - 12)"), + i18n.t("Day of the week (1 - 7) (Monday to Sunday)"), + i18n.t( + "An asterisk (*) matches all possibilities. For instance, if we want to run a rule every day we would use asterisks for day of the month, day of the week, and month of the year to match all values." + ), + i18n.t( + "A wildcard (?) means no specific value and only works for day of the month or day of the week. For example, if you want to execute a rule on a particular day (10th) but you don't care about what day of the week that is, you would use ? in the day of the week field." + ), + ].join("\n"), + }, + summary: { + key: "summary", + label: i18n.t("Summary"), + component: SummaryStep, + validationKeys: [], + showOnSyncDialog: true, + }, + aggregation: { + key: "aggregation", + label: i18n.t("Aggregation"), + component: AggregationStep, + validationKeys: ["dataSyncAggregation"], + showOnSyncDialog: true, + }, +}; + +export const metadataSteps: SyncWizardStep[] = [ + commonSteps.generalInfo, + { + key: "metadata", + label: i18n.t("Metadata"), + component: MetadataSelectionStep, + validationKeys: [], + }, + { + key: "filter-rules", + label: i18n.t("Filter rules"), + component: MetadataFilterRulesStep, + validationKeys: ["metadata"], + }, + { + key: "dependencies-selection", + label: i18n.t("Select dependencies"), + component: MetadataIncludeExcludeStep, + validationKeys: ["metadataIncludeExclude"], + showOnSyncDialog: true, + }, + commonSteps.instanceSelection, + commonSteps.scheduler, + commonSteps.summary, +]; + +export const deletedSteps: SyncWizardStep[] = [commonSteps.instanceSelection]; + +export const aggregatedSteps: SyncWizardStep[] = [ + commonSteps.generalInfo, + { + key: "data-elements", + label: i18n.t("Data elements"), + component: MetadataSelectionStep, + validationKeys: ["metadataIds"], + showOnSyncDialog: false, + }, + { + key: "organisations-units", + label: i18n.t("Organisation units"), + component: OrganisationUnitsSelectionStep, + validationKeys: ["dataSyncOrganisationUnits"], + showOnSyncDialog: true, + }, + { + key: "period", + label: i18n.t("Period"), + component: PeriodSelectionStep, + validationKeys: ["dataSyncStartDate", "dataSyncEndDate"], + showOnSyncDialog: true, + }, + { + key: "category-options", + label: i18n.t("Category options"), + component: CategoryOptionsSelectionStep, + validationKeys: ["categoryOptionIds"], + showOnSyncDialog: true, + }, + { + ...commonSteps.aggregation, + warning: i18n.t( + "If aggregation is enabled, the synchronization will use the Analytics endpoint and group data by organisation units children and the chosen time periods." + ), + }, + commonSteps.instanceSelection, + commonSteps.scheduler, + commonSteps.summary, +]; + +export const eventsSteps: SyncWizardStep[] = [ + commonSteps.generalInfo, + { + key: "organisations-units", + label: i18n.t("Organisation units"), + component: OrganisationUnitsSelectionStep, + validationKeys: ["dataSyncOrganisationUnits"], + showOnSyncDialog: true, + }, + { + key: "programs", + label: i18n.t("Programs"), + component: MetadataSelectionStep, + validationKeys: ["metadataIds"], + showOnSyncDialog: false, + }, + { + key: "period", + label: i18n.t("Period"), + component: PeriodSelectionStep, + validationKeys: ["dataSyncStartDate", "dataSyncEndDate"], + showOnSyncDialog: true, + }, + { + key: "events", + label: i18n.t("Events"), + component: EventsSelectionStep, + validationKeys: ["dataSyncEvents"], + showOnSyncDialog: true, + }, + { + ...commonSteps.aggregation, + warning: i18n.t( + "If aggregation is enabled, the synchronization will use the Analytics endpoint and group data by organisation units children and the chosen time periods. Program indicators are only included during a synchronization if aggregation is enabled." + ), + }, + commonSteps.instanceSelection, + commonSteps.scheduler, + commonSteps.summary, +]; diff --git a/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx b/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx new file mode 100644 index 000000000..7d8b66a4e --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx @@ -0,0 +1,86 @@ +import { Wizard, WizardStep } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useRef } from "react"; +import { useLocation } from "react-router-dom"; +import SyncRule from "../../../../../models/syncRule"; +import { getValidationMessages } from "../../../../../utils/old-validations"; +import { getMetadata } from "../../../../../utils/synchronization"; +import { useAppContext } from "../../contexts/AppContext"; +import { aggregatedSteps, deletedSteps, eventsSteps, metadataSteps } from "./Steps"; + +interface SyncWizardProps { + syncRule: SyncRule; + isDialog?: boolean; + onChange?(syncRule: SyncRule): void; + onCancel?(): void; +} + +const config = { + metadata: metadataSteps, + aggregated: aggregatedSteps, + events: eventsSteps, + deleted: deletedSteps, +}; + +const SyncWizard: React.FC = ({ + syncRule, + isDialog = false, + onChange = _.noop, + onCancel = _.noop, +}) => { + const location = useLocation(); + const { api } = useAppContext(); + const memoizedRule = useRef(syncRule); + + const steps = config[syncRule.type] + .filter(({ showOnSyncDialog }) => !isDialog || showOnSyncDialog) + .filter(({ hidden }) => !hidden || !hidden(syncRule)) + .map(step => ({ + ...step, + props: { + syncRule, + onCancel, + onChange, + }, + })); + + const onStepChangeRequest = async (_currentStep: WizardStep, newStep: WizardStep) => { + const index = _(steps).findIndex(step => step.key === newStep.key); + const validationMessages = _.take(steps, index).map(({ validationKeys }) => + getValidationMessages(syncRule, validationKeys) + ); + + return _.flatten(validationMessages); + }; + + // This effect should only run in the first load + useEffect(() => { + getMetadata(api, memoizedRule.current.metadataIds, "id").then(metadata => { + const types = _.keys(metadata); + onChange( + memoizedRule.current + .updateMetadataTypes(types) + .updateDataSyncEnableAggregation( + types.includes("indicators") || types.includes("programIndicators") + ) + ); + }); + }, [api, onChange, memoizedRule]); + + const urlHash = location.hash.slice(1); + const stepExists = steps.find(step => step.key === urlHash); + const firstStepKey = steps.map(step => step.key)[0]; + const initialStepKey = stepExists ? urlHash : firstStepKey; + + return ( + + ); +}; + +export default SyncWizard; diff --git a/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx new file mode 100644 index 000000000..b443dbaa4 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx @@ -0,0 +1,102 @@ +import { makeStyles, TextField } from "@material-ui/core"; +import React, { useCallback, useState } from "react"; +import { Instance } from "../../../../../../domain/instance/entities/Instance"; +import { Store } from "../../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../../locales"; +import SyncRule from "../../../../../../models/syncRule"; +import { Dictionary } from "../../../../../../types/utils"; +import { getValidationMessages } from "../../../../../../utils/old-validations"; +import { + InstanceSelectionDropdown, + InstanceSelectionOption, +} from "../../instance-selection-dropdown/InstanceSelectionDropdown"; +import { SyncWizardStepProps } from "../Steps"; + +export const GeneralInfoStep = ({ syncRule, onChange }: SyncWizardStepProps) => { + const classes = useStyles(); + + const [errors, setErrors] = useState>({}); + + const onChangeField = useCallback( + (field: keyof SyncRule) => { + return (event: React.ChangeEvent<{ value: unknown }>) => { + const newRule = syncRule.update({ [field]: event.target.value }); + const messages = getValidationMessages(newRule, [field]); + + setErrors(errors => ({ ...errors, [field]: messages.join("\n") })); + onChange(newRule); + }; + }, + [syncRule, onChange] + ); + + const onChangeInstance = useCallback( + (_type: InstanceSelectionOption, instance?: Instance | Store) => { + const originInstance = instance?.id ?? "LOCAL"; + const targetInstances = originInstance === "LOCAL" ? [] : ["LOCAL"]; + + onChange( + syncRule + .updateBuilder({ originInstance }) + .updateTargetInstances(targetInstances) + .updateMetadataIds([]) + .updateExcludedIds([]) + ); + }, + [syncRule, onChange] + ); + + return ( + + + + + +
    + +
    + + +
    + ); +}; + +const useStyles = makeStyles({ + row: { + marginBottom: 25, + }, +}); + +export default GeneralInfoStep; diff --git a/src/presentation/react/core/components/sync-wizard/common/InstanceSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/common/InstanceSelectionStep.tsx new file mode 100644 index 000000000..7d3015196 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/InstanceSelectionStep.tsx @@ -0,0 +1,90 @@ +import { makeStyles, Typography } from "@material-ui/core"; +import { MultiSelector } from "d2-ui-components"; +import React, { useEffect, useState } from "react"; +import { Instance } from "../../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../../locales"; +import { useAppContext } from "../../../contexts/AppContext"; +import SyncParamsSelector from "../../sync-params-selector/SyncParamsSelector"; +import { SyncWizardStepProps } from "../Steps"; + +export const buildInstanceOptions = (instances: Instance[]) => { + return instances.map(instance => ({ + value: instance.id, + text: instance.username + ? i18n.t("{{name}} ({{url}}) with user {{username}}") + : i18n.t("{{name}} ({{url}}) with logged user"), + })); +}; + +const InstanceSelectionStep: React.FC = ({ syncRule, onChange }) => { + const { d2, compositionRoot } = useAppContext(); + const classes = useStyles(); + + const [selectedOptions, setSelectedOptions] = useState(syncRule.targetInstances); + const [targetInstances, setTargetInstances] = useState([]); + const instanceOptions = buildInstanceOptions(targetInstances); + + const includeCurrentUrlAndTypeIsEvents = (selectedinstanceIds: string[]) => { + return ( + syncRule.type === "events" && + selectedinstanceIds + .map(id => targetInstances.find(instance => instance.id === id)?.url) + .includes(compositionRoot.instances.getApi().baseUrl) + ); + }; + + const changeInstances = (instances: string[]) => { + setSelectedOptions(instances); + + if (includeCurrentUrlAndTypeIsEvents(instances)) { + onChange( + syncRule.updateTargetInstances(instances).updateDataParams({ + ...syncRule.dataParams, + generateNewUid: true, + }) + ); + } else { + onChange(syncRule.updateTargetInstances(instances)); + } + }; + + useEffect(() => { + compositionRoot.instances.list().then(setTargetInstances); + }, [compositionRoot]); + + return ( + + {syncRule.originInstance === "LOCAL" ? ( + + ) : ( + + {i18n.t("Destination")}: {i18n.t("This instance")} + + )} + + + + ); +}; + +const useStyles = makeStyles({ + advancedOptionsTitle: { + fontWeight: 500, + }, +}); + +export default InstanceSelectionStep; diff --git a/src/presentation/react/core/components/sync-wizard/common/MetadataFilterRulesStep.tsx b/src/presentation/react/core/components/sync-wizard/common/MetadataFilterRulesStep.tsx new file mode 100644 index 000000000..497e26d84 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/MetadataFilterRulesStep.tsx @@ -0,0 +1,17 @@ +import React, { useCallback } from "react"; +import FilterRulesTable, { FilterRulesTableProps } from "../../filter-rules-table/FilterRulesTable"; +import { SyncWizardStepProps } from "../Steps"; + +const MetadataFilterRulesStep: React.FC = props => { + const { syncRule, onChange } = props; + const setFilterRules = useCallback( + filterRules => { + onChange(syncRule.updateFilterRules(filterRules)); + }, + [syncRule, onChange] + ); + + return ; +}; + +export default React.memo(MetadataFilterRulesStep); diff --git a/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx new file mode 100644 index 000000000..6b00d754c --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx @@ -0,0 +1,125 @@ +import { useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { Instance } from "../../../../../../domain/instance/entities/Instance"; +import i18n from "../../../../../../locales"; +import { metadataModels } from "../../../../../../models/dhis/factory"; +import { + AggregatedDataElementModel, + EventProgramWithDataElementsModel, + EventProgramWithIndicatorsModel, +} from "../../../../../../models/dhis/mapping"; +import { + DataElementGroupModel, + DataElementGroupSetModel, + DataSetModel, + IndicatorModel, +} from "../../../../../../models/dhis/metadata"; +import { getMetadata } from "../../../../../../utils/synchronization"; +import { useAppContext } from "../../../contexts/AppContext"; +import MetadataTable from "../../metadata-table/MetadataTable"; +import { SyncWizardStepProps } from "../Steps"; + +const config = { + metadata: { + models: metadataModels, + childrenKeys: undefined, + }, + aggregated: { + models: [ + DataSetModel, + AggregatedDataElementModel, + DataElementGroupModel, + DataElementGroupSetModel, + IndicatorModel, + ], + childrenKeys: ["dataElements", "dataElementGroups"], + }, + events: { + models: [EventProgramWithDataElementsModel, EventProgramWithIndicatorsModel], + childrenKeys: ["dataElements", "programIndicators"], + }, + deleted: { + models: [], + childrenKeys: undefined, + }, +}; + +export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizardStepProps) { + const { api, compositionRoot } = useAppContext(); + const snackbar = useSnackbar(); + + const [metadataIds, updateMetadataIds] = useState([]); + const [remoteInstance, setRemoteInstance] = useState(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + + const { models, childrenKeys } = config[syncRule.type]; + + const changeSelection = (newMetadataIds: string[], newExclusionIds: string[]) => { + const additions = _.difference(newMetadataIds, metadataIds); + if (additions.length > 0) { + snackbar.info( + i18n.t("Selected {{difference}} elements", { difference: additions.length }), + { + autoHideDuration: 1000, + } + ); + } + + const removals = _.difference(metadataIds, newMetadataIds); + if (removals.length > 0) { + snackbar.info( + i18n.t("Removed {{difference}} elements", { + difference: Math.abs(removals.length), + }), + { autoHideDuration: 1000 } + ); + } + + getMetadata(api, newMetadataIds, "id").then(metadata => { + const types = _.keys(metadata); + onChange( + syncRule + .updateMetadataIds(newMetadataIds) + .updateExcludedIds(newExclusionIds) + .updateMetadataTypes(types) + .updateDataSyncEnableAggregation( + types.includes("indicators") || types.includes("programIndicators") + ) + ); + }); + + updateMetadataIds(newMetadataIds); + }; + + useEffect(() => { + compositionRoot.instances.getById(syncRule.originInstance).then(result => { + result.match({ + success: instance => { + setRemoteInstance(instance); + setLoading(false); + }, + error: () => { + snackbar.error(i18n.t("Instance not found")); + setLoading(false); + setError(true); + }, + }); + }); + }, [compositionRoot, snackbar, syncRule]); + + if (loading || error) return null; + + return ( + + ); +} diff --git a/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx b/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx new file mode 100644 index 000000000..6247d9801 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx @@ -0,0 +1,83 @@ +import { DropDown, TextField } from "@dhis2/d2-ui-core"; +import { FormBuilder } from "@dhis2/d2-ui-forms"; +import PropTypes from "prop-types"; +import React from "react"; +import i18n from "../../../../../../locales"; +import isValidCronExpression from "../../../../../../utils/validCronExpression"; +import { Toggle } from "../../toggle/Toggle"; + +const cronExpressions = [ + { displayName: i18n.t("Every day"), id: "0 0 0 ? * *" }, + { displayName: i18n.t("Every month"), id: "0 0 0 1 1/1 ?" }, + { displayName: i18n.t("Every three months"), id: "0 0 0 1 1/3 ?" }, + { displayName: i18n.t("Every six months"), id: "0 0 0 1 1/6 ?" }, + { displayName: i18n.t("Every year"), id: "0 0 0 1 1 ?" }, +]; + +const SchedulerStep = ({ syncRule, onChange }) => { + const selectedCron = cronExpressions.find(({ id }) => id === syncRule.frequency); + + const updateFields = (field, value) => { + if (field === "enabled") { + onChange(syncRule.updateEnabled(value)); + } else if (field === "frequency" || field === "frequencyDropdown") { + const enabled = syncRule.enabled || !!value; + onChange(syncRule.updateFrequency(value || "").updateEnabled(enabled)); + } + }; + + const fields = [ + { + name: "enabled", + value: syncRule.enabled, + component: Toggle, + props: { + label: i18n.t("Enabled"), + style: { width: "100%" }, + }, + validators: [], + }, + { + name: "frequencyDropdown", + value: selectedCron?.value ?? "", + component: DropDown, + props: { + hintText: syncRule.readableFrequency || i18n.t("Select frequency template"), + menuItems: cronExpressions, + includeEmpty: true, + emptyLabel: i18n.t(""), + style: { width: "100%", marginTop: 20 }, + }, + validators: [], + }, + { + name: "frequency", + value: syncRule.frequency, + component: TextField, + props: { + floatingLabelText: i18n.t("Cron expression"), + style: { width: "100%" }, + changeEvent: "onBlur", + }, + validators: [ + { + message: i18n.t("Cron expression must be valid"), + validator(value) { + return !value || isValidCronExpression(value); + }, + }, + ], + }, + ]; + + return ; +}; + +SchedulerStep.propTypes = { + syncRule: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, +}; + +SchedulerStep.defaultProps = {}; + +export default SchedulerStep; diff --git a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx new file mode 100644 index 000000000..89334cedb --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx @@ -0,0 +1,460 @@ +import { Button, LinearProgress, makeStyles } from "@material-ui/core"; +import { ConfirmationDialog, useLoading, useSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import moment from "moment"; +import React, { useEffect, useMemo, useState } from "react"; +import { useHistory } from "react-router-dom"; +import { includeExcludeRulesFriendlyNames } from "../../../../../../domain/metadata/entities/MetadataFriendlyNames"; +import { cleanOrgUnitPaths } from "../../../../../../domain/synchronization/utils"; +import i18n from "../../../../../../locales"; +import { getValidationMessages } from "../../../../../../utils/old-validations"; +import { + availablePeriods, + getMetadata, + requestJSONDownload, +} from "../../../../../../utils/synchronization"; +import { useAppContext } from "../../../contexts/AppContext"; +import { buildAggregationItems } from "../data/AggregationStep"; +import { buildInstanceOptions } from "./InstanceSelectionStep"; +import { filterRuleToString } from "../../../../../../domain/metadata/entities/FilterRule"; + +const LiEntry = ({ label, value, children }) => { + return ( +
  • + {label} + {value || children ? ": " : ""} + {value} + {children} +
  • + ); +}; + +const useStyles = makeStyles({ + saveButton: { + margin: 10, + backgroundColor: "#2b98f0", + color: "white", + }, + buttonContainer: { + display: "flex", + justifyContent: "space-between", + }, +}); + +const SaveStep = ({ syncRule, onCancel }) => { + const { api, compositionRoot } = useAppContext(); + + const snackbar = useSnackbar(); + const loading = useLoading(); + const classes = useStyles(); + const history = useHistory(); + + const [cancelDialogOpen, setCancelDialogOpen] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [metadata, updateMetadata] = useState({}); + const [targetInstances, setTargetInstances] = useState([]); + const instanceOptions = buildInstanceOptions(targetInstances); + + const openCancelDialog = () => setCancelDialogOpen(true); + + const closeCancelDialog = () => setCancelDialogOpen(false); + + const name = syncRule.isOnDemand() + ? `Rule generated on ${moment().format("YYYY-MM-DD HH:mm:ss")}` + : syncRule.name; + + const save = async () => { + setIsSaving(true); + + const errors = getValidationMessages(syncRule); + if (errors.length > 0) { + snackbar.error(errors.join("\n")); + } else { + const newSyncRule = await syncRule.updateName(name).save(api); + history.push(`/sync-rules/${newSyncRule.type}/edit/${newSyncRule.id}`); + onCancel(); + } + + setIsSaving(false); + }; + + const downloadJSON = async () => { + loading.show(true, "Generating JSON file"); + const sync = compositionRoot.sync[syncRule.type](syncRule.toBuilder()); + const payload = await sync.buildPayload(); + requestJSONDownload(payload, syncRule); + loading.reset(); + }; + + useEffect(() => { + const ids = [ + ...syncRule.metadataIds, + ...syncRule.excludedIds, + ...syncRule.dataSyncAttributeCategoryOptions, + ...cleanOrgUnitPaths(syncRule.dataSyncOrgUnitPaths), + ]; + getMetadata(api, ids, "id,name").then(updateMetadata); + compositionRoot.instances.list().then(setTargetInstances); + }, [api, compositionRoot, syncRule]); + + const aggregationItems = useMemo(buildAggregationItems, []); + + const destinationInstances = useMemo( + () => + _.compact( + syncRule.targetInstances.map(id => instanceOptions.find(e => e.value === id)) + ), + [instanceOptions, syncRule.targetInstances] + ); + + const originInstance = useMemo( + () => instanceOptions.find(e => e.value === syncRule.originInstance), + [instanceOptions, syncRule.originInstance] + ); + + return ( + + + +
      + + + + + + + {originInstance && ( + + )} + + +
        + {destinationInstances.map(instanceOption => ( + + ))} +
      +
      + + {_.keys(metadata).map(metadataType => { + const items = metadata[metadataType].filter( + ({ id }) => !syncRule.excludedIds.includes(id) + ); + return ( + items.length > 0 && ( + +
        + {items.map(({ id, name }) => ( + + ))} +
      +
      + ) + ); + })} + + {syncRule.filterRules.length > 0 && ( + +
        + {_.sortBy(syncRule.filterRules, fr => fr.type).map(filterRule => { + return ( + + ); + })} +
      +
      + )} + + {syncRule.excludedIds.length > 0 && ( + +
        + {syncRule.excludedIds.map(id => { + const element = _(metadata).values().flatten().find({ id }); + + return ( + + ); + })} +
      +
      + )} + {syncRule.type === "metadata" && ( + + )} + + {syncRule.type === "metadata" && !syncRule.useDefaultIncludeExclude && ( + +
        + {_.keys(syncRule.metadataIncludeExcludeRules).map(key => { + const { + includeRules, + excludeRules, + } = syncRule.metadataIncludeExcludeRules[key]; + + return ( + +
          + {includeRules.length > 0 && ( + + {includeRules.map((includeRule, idx) => ( +
            + +
          + ))} +
          + )} + + {excludeRules.length > 0 && ( + + {excludeRules.map((excludeRule, idx) => ( +
            + +
          + ))} +
          + )} +
        +
        + ); + })} +
      +
      + )} + + {syncRule.type === "events" && ( + + )} + + {syncRule.dataSyncAllAttributeCategoryOptions && ( + + )} + + {syncRule.type !== "metadata" && ( + + {syncRule.dataSyncPeriod === "FIXED" && ( +
        + +
      + )} + {syncRule.dataSyncPeriod === "FIXED" && ( +
        + +
      + )} +
      + )} + + {syncRule.type !== "metadata" && ( + + )} + + {syncRule.type === "metadata" && ( + +
        + +
      +
        + +
      +
        + +
      +
        + +
      +
        + +
      +
      + )} + {(syncRule.type === "events" || syncRule.type === "aggregated") && ( + + {syncRule.type === "aggregated" && ( +
        + +
      + )} + {syncRule.type === "events" && ( +
        + +
      + )} +
        + +
      +
      + )} + + + + {syncRule.longFrequency && ( + + )} +
    + +
    +
    + {!syncRule.isOnDemand() && ( + + )} + +
    +
    + +
    +
    + + {isSaving && } +
    + ); +}; + +export default SaveStep; diff --git a/src/presentation/react/core/components/sync-wizard/data/AggregationStep.tsx b/src/presentation/react/core/components/sync-wizard/data/AggregationStep.tsx new file mode 100644 index 000000000..5fa7dadda --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/data/AggregationStep.tsx @@ -0,0 +1,83 @@ +import { makeStyles } from "@material-ui/core"; +import { useSnackbar } from "d2-ui-components"; +import React, { useMemo } from "react"; +import { DataSyncAggregation } from "../../../../../../domain/aggregated/types"; +import i18n from "../../../../../../locales"; +import Dropdown from "../../dropdown/Dropdown"; +import { Toggle } from "../../toggle/Toggle"; +import { SyncWizardStepProps } from "../Steps"; + +const useStyles = makeStyles({ + dropdown: { + marginTop: 20, + marginLeft: -10, + }, + fixedPeriod: { + marginTop: 5, + marginBottom: -20, + }, + datePicker: { + marginTop: -10, + }, +}); + +export const buildAggregationItems = () => [ + { id: "DAILY", name: i18n.t("Daily"), format: "YYYYMMDD" }, + { id: "WEEKLY", name: i18n.t("Weekly"), format: "YYYY[W]W" }, + { id: "MONTHLY", name: i18n.t("Monthly"), format: "YYYYMM" }, + { id: "QUARTERLY", name: i18n.t("Quarterly"), format: "YYYY[Q]Q" }, + { id: "YEARLY", name: i18n.t("Yearly"), format: "YYYY" }, +]; + +const AggregationStep: React.FC = ({ syncRule, onChange }) => { + const classes = useStyles(); + const snackbar = useSnackbar(); + + const updateEnableAggregation = (value: boolean) => { + if (syncRule.metadataTypes.includes("indicators") && !value) { + snackbar.warning( + i18n.t( + "Without aggregation, any data value related to an indicator will be ignored" + ) + ); + } else if (syncRule.metadataTypes.includes("programIndicators") && !value) { + snackbar.warning( + i18n.t( + "Without aggregation, program indicators will not be aggregated and synchronized" + ) + ); + } + onChange(syncRule.updateDataSyncEnableAggregation(value).updateDataSyncAggregationType()); + }; + + const updateAggregationType = (value: DataSyncAggregation) => { + onChange( + syncRule.updateDataSyncEnableAggregation(true).updateDataSyncAggregationType(value) + ); + }; + + const aggregationItems = useMemo(buildAggregationItems, []); + + return ( + + + + {syncRule.dataSyncEnableAggregation && ( +
    + +
    + )} +
    + ); +}; + +export default AggregationStep; diff --git a/src/presentation/react/core/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx new file mode 100644 index 000000000..aad64657d --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx @@ -0,0 +1,80 @@ +import { useD2ApiData } from "d2-api"; +import { MultiSelector } from "d2-ui-components"; +import _ from "lodash"; +import React, { useMemo } from "react"; +import i18n from "../../../../../../locales"; +import { useAppContext } from "../../../contexts/AppContext"; +import { Toggle } from "../../toggle/Toggle"; +import { SyncWizardStepProps } from "../Steps"; + +const CategoryOptionsSelectionStep: React.FC = ({ syncRule, onChange }) => { + const { d2, api } = useAppContext(); + + const { data } = useD2ApiData( + api.models.categoryOptionCombos.get({ + paging: false, + fields: { id: true, name: true }, + filter: { + "categoryCombo.dataDimensionType": { eq: "ATTRIBUTE" }, + }, + }) + ); + + const options = useMemo( + () => + _.uniqBy( + _.map(data?.objects ?? [], ({ name }) => ({ value: name, text: name })), + "value" + ), + [data] + ); + + const selected = useMemo( + () => + _(syncRule.dataSyncAttributeCategoryOptions) + .map(id => _.find(data?.objects, { id })?.name) + .uniq() + .compact() + .value(), + [data, syncRule] + ); + + const updateSyncAll = (value: boolean) => { + onChange( + syncRule + .updateDataSyncAllAttributeCategoryOptions(value) + .updateDataSyncAttributeCategoryOptions(undefined) + ); + }; + + const changeAttributeCategoryOptions = (selectedNames: string[]) => { + const attributeCategoryOptions = _(selectedNames) + .map(name => _.filter(data?.objects, { name })) + .flatten() + .map(({ id }) => id) + .value(); + + onChange(syncRule.updateDataSyncAttributeCategoryOptions(attributeCategoryOptions)); + }; + + return ( + + + {!syncRule.dataSyncAllAttributeCategoryOptions && ( + + )} + + ); +}; + +export default CategoryOptionsSelectionStep; diff --git a/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx new file mode 100644 index 000000000..6ba97876a --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx @@ -0,0 +1,198 @@ +import { Typography } from "@material-ui/core"; +import { ObjectsTable, ObjectsTableDetailField, TableColumn, TableState } from "d2-ui-components"; +import _ from "lodash"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { ProgramEvent } from "../../../../../../domain/events/entities/ProgramEvent"; +import { DataElement, Program } from "../../../../../../domain/metadata/entities/MetadataEntities"; +import i18n from "../../../../../../locales"; +import SyncRule from "../../../../../../models/syncRule"; +import { useAppContext } from "../../../contexts/AppContext"; +import Dropdown from "../../dropdown/Dropdown"; +import { Toggle } from "../../toggle/Toggle"; +import { SyncWizardStepProps } from "../Steps"; + +interface ProgramEventObject extends ProgramEvent { + [key: string]: any; +} + +type CustomProgram = Program & { + programStages?: { programStageDataElements: { dataElement: DataElement }[] }[]; +}; + +export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardStepProps) { + const { compositionRoot } = useAppContext(); + + const [memoizedSyncRule] = useState(syncRule); + const [objects, setObjects] = useState(); + const [programs, setPrograms] = useState([]); + const [programFilter, changeProgramFilter] = useState(""); + const [error, setError] = useState(); + + useEffect(() => { + const sync = compositionRoot.sync.events(memoizedSyncRule.toBuilder()); + sync.extractMetadata().then(({ programs = [] }) => setPrograms(programs)); + }, [memoizedSyncRule, compositionRoot]); + + useEffect(() => { + if (programs.length === 0) return; + compositionRoot.events + .list( + { + ...memoizedSyncRule.dataParams, + allEvents: true, + }, + programs.map(({ id }) => id) + ) + .then(setObjects) + .catch(setError); + }, [compositionRoot, memoizedSyncRule, programs]); + + const handleTableChange = useCallback( + (tableState: TableState) => { + const { selection } = tableState; + onChange(syncRule.updateDataSyncEvents(selection.map(({ id }) => id))); + }, + [onChange, syncRule] + ); + + const updateSyncAll = useCallback( + (value: boolean) => { + onChange(syncRule.updateDataSyncAllEvents(value).updateDataSyncEvents(undefined)); + }, + [onChange, syncRule] + ); + + const addToSelection = useCallback( + (ids: string[]) => { + const oldSelection = _.difference(syncRule.dataSyncEvents, ids); + const newSelection = _.difference(ids, syncRule.dataSyncEvents); + + onChange(syncRule.updateDataSyncEvents([...oldSelection, ...newSelection])); + }, + [onChange, syncRule] + ); + + const columns: TableColumn[] = useMemo( + () => [ + { name: "id" as const, text: i18n.t("UID"), sortable: true }, + { + name: "program" as const, + text: i18n.t("Program"), + sortable: true, + getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program, + }, + { name: "orgUnitName" as const, text: i18n.t("Organisation unit"), sortable: true }, + { name: "eventDate" as const, text: i18n.t("Event date"), sortable: true }, + { + name: "lastUpdated" as const, + text: i18n.t("Last updated"), + sortable: true, + hidden: true, + }, + { name: "status" as const, text: i18n.t("Status"), sortable: true }, + { name: "storedBy" as const, text: i18n.t("Stored by"), sortable: true }, + ], + [programs] + ); + + const details: ObjectsTableDetailField[] = useMemo( + () => [ + { name: "id" as const, text: i18n.t("UID") }, + { + name: "program" as const, + text: i18n.t("Program"), + getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program, + }, + { name: "orgUnitName" as const, text: i18n.t("Organisation unit") }, + { name: "created" as const, text: i18n.t("Created") }, + { name: "lastUpdated" as const, text: i18n.t("Last updated") }, + { name: "eventDate" as const, text: i18n.t("Event date") }, + { name: "dueDate" as const, text: i18n.t("Due date") }, + { name: "status" as const, text: i18n.t("Status") }, + { name: "storedBy" as const, text: i18n.t("Stored by") }, + ], + [programs] + ); + + const actions = useMemo( + () => [ + { + name: "select", + text: i18n.t("Select"), + primary: true, + multiple: true, + onClick: addToSelection, + isActive: () => false, + }, + ], + [addToSelection] + ); + + const filterComponents = useMemo( + () => ( + + ), + [programFilter, programs] + ); + + const additionalColumns = useMemo(() => { + const program = _.find(programs, { id: programFilter }); + const dataElements = _(program?.programStages ?? []) + .map(({ programStageDataElements }) => + programStageDataElements.map(({ dataElement }) => dataElement) + ) + .flatten() + .value(); + + return dataElements.map(({ id, displayFormName }) => ({ + name: id, + text: displayFormName, + sortable: true, + hidden: true, + getValue: (row: ProgramEvent) => { + return _.find(row.dataValues, { dataElement: id })?.value ?? "-"; + }, + })); + }, [programFilter, programs]); + + const filteredObjects = + objects?.filter(({ program }) => !programFilter || program === programFilter) ?? []; + + if (error) { + console.error(error); + return ( + + {i18n.t("An error ocurred while trying to access the required events")} + + ); + } + + return ( + + + {!syncRule.dataSyncAllEvents && ( + + rows={filteredObjects} + loading={objects === undefined} + columns={[...columns, ...additionalColumns]} + details={details} + actions={actions} + forceSelectionColumn={true} + onChange={handleTableChange} + selection={syncRule.dataSyncEvents?.map(id => ({ id })) ?? []} + filterComponents={filterComponents} + /> + )} + + ); +} diff --git a/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx new file mode 100644 index 000000000..30eaab3ee --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx @@ -0,0 +1,55 @@ +import { makeStyles, Typography } from "@material-ui/core"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import { OrgUnitsSelector } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import i18n from "../../../../../../locales"; +import { useAppContext } from "../../../contexts/AppContext"; +import { SyncWizardStepProps } from "../Steps"; + +const useStyles = makeStyles({ + loading: { + display: "flex", + justifyContent: "center", + }, +}); + +const OrganisationUnitsSelectionStep: React.FC = ({ syncRule, onChange }) => { + const { api, compositionRoot } = useAppContext(); + const classes = useStyles(); + const [orgUnitRootIds, setOrgUnitRootIds] = useState(); + + useEffect(() => { + compositionRoot.instances + .getOrgUnitRoots() + .then(roots => roots.map(({ id }) => id)) + .then(setOrgUnitRootIds); + }, [compositionRoot]); + + const changeSelection = (orgUnitsPaths: string[]) => { + onChange(syncRule.updateDataSyncOrgUnitPaths(orgUnitsPaths).updateDataSyncEvents([])); + }; + + if (!orgUnitRootIds) { + return ( +
    + +
    + ); + } else if (_.isEmpty(orgUnitRootIds)) { + return {i18n.t("You do not have assigned any organisation unit")}; + } else { + return ( + + ); + } +}; + +export default OrganisationUnitsSelectionStep; diff --git a/src/presentation/react/core/components/sync-wizard/data/PeriodSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/PeriodSelectionStep.tsx new file mode 100644 index 000000000..3c7986572 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/data/PeriodSelectionStep.tsx @@ -0,0 +1,59 @@ +import React, { useCallback, useMemo } from "react"; +import { DataSyncPeriod } from "../../../../../../domain/aggregated/types"; +import PeriodSelection, { ObjectWithPeriod } from "../../period-selection/PeriodSelection"; +import { SyncWizardStepProps } from "../Steps"; + +const PeriodSelectionStep: React.FC = ({ syncRule, onChange }) => { + const updatePeriod = useCallback( + (period: DataSyncPeriod) => { + onChange( + syncRule + .updateDataSyncPeriod(period) + .updateDataSyncStartDate(undefined) + .updateDataSyncEndDate(undefined) + .updateDataSyncEvents([]) + ); + }, + [onChange, syncRule] + ); + + const updateStartDate = useCallback( + (date: Date | null) => { + onChange(syncRule.updateDataSyncStartDate(date ?? undefined).updateDataSyncEvents([])); + }, + [onChange, syncRule] + ); + + const updateEndDate = useCallback( + (date: Date | null) => { + onChange(syncRule.updateDataSyncEndDate(date ?? undefined).updateDataSyncEvents([])); + }, + [onChange, syncRule] + ); + + const onFieldChange = useCallback( + (field: keyof ObjectWithPeriod, value: ObjectWithPeriod[keyof ObjectWithPeriod]) => { + switch (field) { + case "period": + return updatePeriod(value as ObjectWithPeriod["period"]); + case "startDate": + return updateStartDate((value as ObjectWithPeriod["startDate"]) || null); + case "endDate": + return updateEndDate((value as ObjectWithPeriod["endDate"]) || null); + } + }, + [updatePeriod, updateStartDate, updateEndDate] + ); + + const objectWithPeriod = useMemo(() => { + return { + period: syncRule.dataSyncPeriod, + startDate: syncRule.dataSyncStartDate || undefined, + endDate: syncRule.dataSyncEndDate || undefined, + }; + }, [syncRule]); + + return ; +}; + +export default PeriodSelectionStep; diff --git a/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx b/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx new file mode 100644 index 000000000..e5db57680 --- /dev/null +++ b/src/presentation/react/core/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx @@ -0,0 +1,124 @@ +import { makeStyles } from "@material-ui/core"; +import { D2SchemaProperties } from "d2-api/schemas"; +import { MultiSelector, withSnackbar } from "d2-ui-components"; +import _ from "lodash"; +import React, { useEffect, useState } from "react"; +import { MetadataPackage } from "../../../../../../domain/metadata/entities/MetadataEntities"; +import { includeExcludeRulesFriendlyNames } from "../../../../../../domain/metadata/entities/MetadataFriendlyNames"; +import i18n from "../../../../../../locales"; +import { D2Model } from "../../../../../../models/dhis/default"; +import { modelFactory } from "../../../../../../models/dhis/factory"; +import { getMetadata } from "../../../../../../utils/synchronization"; +import { useAppContext } from "../../../contexts/AppContext"; +import Dropdown, { DropdownOption } from "../../dropdown/Dropdown"; +import { Toggle } from "../../toggle/Toggle"; +import { SyncWizardStepProps } from "../Steps"; + +const useStyles = makeStyles({ + includeExcludeContainer: { + display: "flex", + flexDirection: "column", + alignItems: "flex-start", + marginTop: "20px", + }, + multiselectorContainer: { + width: "100%", + }, +}); + +const MetadataIncludeExcludeStep: React.FC = ({ syncRule, onChange }) => { + const classes = useStyles(); + const { d2, api } = useAppContext(); + + const [modelSelectItems, setModelSelectItems] = useState([]); + const [models, setModels] = useState([]); + const [selectedType, setSelectedType] = useState(""); + + useEffect(() => { + getMetadata(api, syncRule.metadataIds, "id,name").then((metadata: MetadataPackage) => { + const models = _.keys(metadata).map((type: string) => { + return modelFactory(type); + }); + + const options = models + .map((model: typeof D2Model) => api.models[model.getCollectionName()].schema) + .map((schema: D2SchemaProperties) => ({ + name: schema.displayName, + id: schema.name, + })); + + setModels(models); + setModelSelectItems(options); + }); + }, [d2, api, syncRule]); + + const { includeRules = [], excludeRules = [] } = + syncRule.metadataIncludeExcludeRules[selectedType] || {}; + const allRules = [...includeRules, ...excludeRules]; + const ruleOptions = allRules.map(rule => ({ + value: rule, + text: includeExcludeRulesFriendlyNames[rule] || rule, + })); + + const changeUseDefaultIncludeExclude = (useDefault: boolean) => { + onChange( + useDefault + ? syncRule.markToUseDefaultIncludeExclude() + : syncRule.markToNotUseDefaultIncludeExclude(models) + ); + }; + + const changeModelName = (modelName: string) => { + setSelectedType(modelName); + }; + + const changeInclude = (currentIncludeRules: any) => { + const type: string = selectedType; + + const oldIncludeRules: string[] = includeRules; + + const ruleToExclude = _.difference(oldIncludeRules, currentIncludeRules); + const ruleToInclude = _.difference(currentIncludeRules, oldIncludeRules); + + if (ruleToInclude.length > 0) { + onChange(syncRule.moveRuleFromExcludeToInclude(type, ruleToInclude)); + } else if (ruleToExclude.length > 0) { + onChange(syncRule.moveRuleFromIncludeToExclude(type, ruleToExclude)); + } + }; + + return ( + + + {!syncRule.useDefaultIncludeExclude && ( +
    + + + {selectedType && ( +
    + +
    + )} +
    + )} +
    + ); +}; + +export default withSnackbar(MetadataIncludeExcludeStep); diff --git a/src/presentation/react/core/components/test-wrapper/TestWrapper.tsx b/src/presentation/react/core/components/test-wrapper/TestWrapper.tsx new file mode 100644 index 000000000..123460e87 --- /dev/null +++ b/src/presentation/react/core/components/test-wrapper/TestWrapper.tsx @@ -0,0 +1,70 @@ +import _ from "lodash"; +import React from "react"; +import { + concatStrings, + generateTestId, + isClassComponent, + recursiveMap, + removeParentheses, + wrapType, +} from "./utils"; + +interface TestWrapperProps { + namespace?: string; + attributeName?: string; + componentParent?: string; +} + +const dataTestDictionary = new Map(); + +/** + * A wrapper that recursively adds `data-test` attributes to children and render components. + * + * Disclaimer: + * - This only "modern" functional components (HOCs and class components might not work as expected) + * + * Implementation based on: + * - https://github.com/dennismorello/react-test-attributes/blob/master/src/components/TestAttribute.tsx + * - https://github.com/ctrlplusb/react-tree-walker/blob/master/src/index.js + */ +export const TestWrapper: React.FC = ({ + children, + namespace, + attributeName, + componentParent, +}) => { + const testAttributeName = attributeName || `data-test`; + + function withTestAttribute(nodes: React.ReactNode, parentId?: string) { + const node = React.Children.only(nodes) as any; + const { type, props } = node; + if (isClassComponent(type)) return node; + + const id = generateTestId(node); + const className = removeParentheses(type.displayName || type.name || type); + const testAttribute = concatStrings([className, namespace, componentParent, parentId, id]); + const children = _.flatten([props.children]); + const element = props["data-test-wrapped"] + ? node + : React.createElement(wrapType(type, parentId), props, ...children); + + const count = dataTestDictionary.get(testAttribute) ?? 0; + const testId = concatStrings([testAttribute, count > 1 ? String(count) : undefined]); + const dataTest = props[testAttributeName] ?? testId; + const isReactFragment = element.type.toString() === "Symbol(react.fragment)"; + // TODO: Disabled for now + // dataTestDictionary.set(testAttribute, count + 1); + + return isReactFragment + ? element + : React.cloneElement(element, { + [testAttributeName]: isReactFragment ? undefined : dataTest, + }); + } + + return ( + + {process.env.REACT_APP_CYPRESS ? recursiveMap(children, withTestAttribute) : children} + + ); +}; diff --git a/src/presentation/react/core/components/test-wrapper/utils.tsx b/src/presentation/react/core/components/test-wrapper/utils.tsx new file mode 100644 index 000000000..6340a857a --- /dev/null +++ b/src/presentation/react/core/components/test-wrapper/utils.tsx @@ -0,0 +1,75 @@ +import _ from "lodash"; +import React from "react"; +import { TestWrapper } from "./TestWrapper"; +import memoize from "nano-memoize"; + +export function recursiveMap(children: React.ReactNode, fn: Function, parentId?: string) { + const result: any[] = []; + React.Children.forEach(children, child => { + const id = concatStrings([parentId, generateTestId(child || {})]); + if (!React.isValidElement(child)) { + result.push(child); + return; + } + + const clone = child.props.children + ? React.cloneElement(child, { + children: recursiveMap(child.props.children, fn, id), + }) + : child; + + result.push(fn(clone, id)); + }); + + return result; +} + +export function concatStrings( + strings: (string | undefined)[], + separator = "-", + duplicates = false +) { + return _(strings) + .map(string => (duplicates ? string : string?.split(separator))) + .flatten() + .compact() + .uniq() + .join(separator); +} + +export function generateTestId({ props = {}, key }: { props?: any; key?: string }) { + const id = _.kebabCase( + _.toLower( + props.id || + props.title || + props.name || + props.label || + props["aria-label"] || + key || + props.value + ) + ); + return id ? id : undefined; +} + +export function removeParentheses(string: string) { + if (typeof string !== "string") return undefined; + const result = string.substring(string.lastIndexOf("(") + 1, string.indexOf(")")); + return result ? result : string; +} + +export function isClassComponent(component: any) { + return typeof component === "function" && !!component.prototype.isReactComponent ? true : false; +} + +export const wrapType = memoize((type: any, parentId?: string) => { + return typeof type === "function" && !isClassComponent(type) + ? (...props: any[]) => { + return ( + + {type(...props)} + + ); + } + : type; +}); diff --git a/src/presentation/react/core/components/text-field-on-blur/TextFieldOnBlur.tsx b/src/presentation/react/core/components/text-field-on-blur/TextFieldOnBlur.tsx new file mode 100644 index 000000000..c60ed2099 --- /dev/null +++ b/src/presentation/react/core/components/text-field-on-blur/TextFieldOnBlur.tsx @@ -0,0 +1,51 @@ +import { TextField, TextFieldProps } from "@material-ui/core"; +import React, { useCallback, useEffect, useRef, useState } from "react"; + +/* Wrap TextField with those two changes: + +- props.onChange is called with the string, not the event. +- props.onChange is called on blur, not on every keystroke, this way the UI is much more responsive. +*/ + +type TextFieldOnBlurProps = Omit & { + value: string; + onChange(newValue: string): void; +}; + +const TextFieldOnBlur: React.FC = props => { + const { onChange } = props; + // Use props.value as initial value for the initial state but also react to changes from the parent + const propValue = props.value; + const prevPropValue = useRef(propValue); + const [value, setValue] = useState(propValue); + + useEffect(() => { + if (propValue !== prevPropValue.current) { + console.log("upchange", { value, propValue, prev: prevPropValue.current }); + setValue(propValue); + prevPropValue.current = propValue; + } + }, [propValue, prevPropValue, value]); + + const callParentOnChange = useCallback(() => { + onChange(value); + }, [value, onChange]); + + const setValueFromEvent = useCallback( + (ev: React.ChangeEvent<{ value: string }>) => { + setValue(ev.target.value); + }, + [setValue] + ); + + return ( + + ); +}; + +export default React.memo(TextFieldOnBlur); diff --git a/src/presentation/react/core/components/toggle/Toggle.tsx b/src/presentation/react/core/components/toggle/Toggle.tsx new file mode 100644 index 000000000..7d341be12 --- /dev/null +++ b/src/presentation/react/core/components/toggle/Toggle.tsx @@ -0,0 +1,34 @@ +import { FormControlLabel, Switch } from "@material-ui/core"; +import _ from "lodash"; +import React from "react"; + +interface InputParameters { + disabled?: boolean; + label: string; + onChange?: Function; + onValueChange?: Function; + value: boolean; +} + +export const Toggle = ({ + label, + onChange = _.noop, + onValueChange = _.noop, + value, + disabled, +}: InputParameters) => ( + { + onChange({ target: { value: e.target.checked } }); + onValueChange(e.target.checked); + }} + checked={value} + color="primary" + /> + } + label={label} + /> +); diff --git a/src/presentation/react/core/contexts/AppContext.ts b/src/presentation/react/core/contexts/AppContext.ts new file mode 100644 index 000000000..245da4626 --- /dev/null +++ b/src/presentation/react/core/contexts/AppContext.ts @@ -0,0 +1,20 @@ +import React, { useContext } from "react"; +import { CompositionRoot } from "../../../CompositionRoot"; +import { D2Api } from "../../../../types/d2-api"; + +export interface AppContext { + api: D2Api; + d2: object; + compositionRoot: CompositionRoot; +} + +export const AppContext = React.createContext(null); + +export function useAppContext() { + const context = useContext(AppContext); + if (context) { + return context; + } else { + throw new Error("Context not found"); + } +} diff --git a/src/presentation/react/core/hooks/useOpenState.ts b/src/presentation/react/core/hooks/useOpenState.ts new file mode 100644 index 000000000..d8f85fb4c --- /dev/null +++ b/src/presentation/react/core/hooks/useOpenState.ts @@ -0,0 +1,10 @@ +import { useCallback, useState } from "react"; + +export function useOpenState(initialValue?: Value) { + const [value, setValue] = useState(initialValue); + const open = useCallback((value: Value) => setValue(value), [setValue]); + const close = useCallback(() => setValue(undefined), [setValue]); + const isOpen = !!value; + + return { isOpen, value, open, close }; +} diff --git a/src/presentation/react/core/hooks/useQueryParams.ts b/src/presentation/react/core/hooks/useQueryParams.ts new file mode 100644 index 000000000..985e47638 --- /dev/null +++ b/src/presentation/react/core/hooks/useQueryParams.ts @@ -0,0 +1,7 @@ +import qs from "qs"; +import { useLocation } from "react-router-dom"; + +export function useQueryParams() { + const location = useLocation(); + return qs.parse(location.search, { ignoreQueryPrefix: true }); +} diff --git a/src/presentation/react/core/themes/dhis2-legacy.theme.js b/src/presentation/react/core/themes/dhis2-legacy.theme.js new file mode 100644 index 000000000..a702c0419 --- /dev/null +++ b/src/presentation/react/core/themes/dhis2-legacy.theme.js @@ -0,0 +1,62 @@ +import { + cyan100, + cyan500, + cyan700, + darkBlack, + grey100, + grey400, + grey500, + orange500, + white, +} from "material-ui/styles/colors"; +import { fade } from "material-ui/utils/colorManipulator"; +import Spacing from "material-ui/styles/spacing"; +import getMuiTheme from "material-ui/styles/getMuiTheme"; + +const theme = { + spacing: Spacing, + fontFamily: "Roboto, sans-serif", + palette: { + primary1Color: cyan500, + primary2Color: cyan700, + primary3Color: cyan100, + accent1Color: orange500, + accent2Color: grey100, + accent3Color: grey500, + textColor: darkBlack, + alternateTextColor: white, + canvasColor: white, + borderColor: grey400, + disabledColor: fade(darkBlack, 0.3), + }, +}; + +function createAppTheme(style) { + return { + sideBar: { + backgroundColor: "#F3F3F3", + backgroundColorItem: "transparent", + backgroundColorItemActive: style.palette.accent2Color, + textColor: style.palette.textColor, + textColorActive: "#276696", + borderStyle: "1px solid #e1e1e1", + }, + forms: { + minWidth: 350, + maxWidth: 900, + }, + formFields: { + secondaryColor: style.palette.accent4Color, + }, + tabs: { + backgroundColor: "#E4E4E4", + inkBarColor: style.palette.accent1Color, + textColor: "#666666", + }, + }; +} + +const muiTheme = getMuiTheme(theme); +const appTheme = createAppTheme(muiTheme); + +export default Object.assign({}, muiTheme, appTheme); diff --git a/src/presentation/react/core/themes/dhis2.theme.js b/src/presentation/react/core/themes/dhis2.theme.js new file mode 100644 index 000000000..0f264f688 --- /dev/null +++ b/src/presentation/react/core/themes/dhis2.theme.js @@ -0,0 +1,91 @@ +import { createMuiTheme } from "@material-ui/core/styles"; + +// Color palette from https://projects.invisionapp.com/share/A7LT4TJYETS#/screens/302550228_Color +export const colors = { + accentPrimary: "#1976d2", + accentPrimaryDark: "#004BA0", + accentPrimaryLight: "#63A4FF", + accentPrimaryLightest: "#EAF4FF", + + accentSecondary: "#fb8c00", + accentSecondaryLight: "#f57c00", + accentSecondaryDark: "#ff9800", + + black: "#000000", + greyBlack: "#494949", + grey: "#9E9E9E", + greyLight: "#E0E0E0", + greyDisabled: "#8E8E8E", + blueGrey: "#ECEFF1", + snow: "#F4F6F8", + white: "#FFFFFF", // Not included in palette! + + negative: "#E53935", + warning: "#F19C02", + positive: "#3D9305", + info: "#EAF4FF", +}; + +export const palette = { + common: { + white: colors.white, + black: colors.black, + }, + action: { + active: colors.greyBlack, + disabled: colors.greyDisabled, + }, + text: { + primary: colors.black, + secondary: colors.greyBlack, + disabled: colors.greyDisabled, + hint: colors.grey, + }, + primary: { + main: colors.accentPrimary, + dark: colors.accentPrimaryDark, + light: colors.accentPrimaryLight, + lightest: colors.accentPrimaryLightest, // Custom extension, not used by default + // contrastText: 'white', + }, + secondary: { + main: colors.accentSecondary, + light: colors.accentSecondaryLight, + dark: colors.accentSecondaryDark, + contrastText: "#fff", + }, + error: { + main: colors.negative, // This is automatically expanded to main/light/dark/contrastText, what do we use here? + }, + status: { + //Custom colors collection, not used by default in MUI + negative: colors.negative, + warning: colors.warning, + positive: colors.positive, + info: colors.info, + }, + background: { + paper: colors.white, + default: colors.snow, + grey: "#FCFCFC", + hover: colors.greyLight, + }, + divider: colors.greyLight, + shadow: colors.grey, +}; + +export const muiTheme = createMuiTheme({ + colors, + palette, + typography: { + fontFamily: "Roboto, Helvetica, Arial, sans-serif", + useNextVariants: true, + }, + overrides: { + MuiDivider: { + light: { + backgroundColor: palette.divider, // No light dividers for now + }, + }, + }, +}); diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx index 763789fdd..ef0b3a233 100644 --- a/src/presentation/webapp/Root.tsx +++ b/src/presentation/webapp/Root.tsx @@ -1,7 +1,7 @@ import React from "react"; import { HashRouter, Switch } from "react-router-dom"; -import RouteWithSession from "../react/components/auth/RouteWithSession"; -import RouteWithSessionAndAuth from "../react/components/auth/RouteWithSessionAndAuth"; +import RouteWithSession from "../react/core/components/auth/RouteWithSession"; +import RouteWithSessionAndAuth from "../react/core/components/auth/RouteWithSessionAndAuth"; import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; import HistoryPage from "./core/pages/history/HistoryPage"; import InstanceListPage from "./core/pages/instance-list/InstanceListPage"; @@ -19,7 +19,7 @@ import SyncRulesCreationPage, { } from "./core/pages/sync-rules-creation/SyncRulesCreationPage"; import SyncRulesPage from "./core/pages/sync-rules-list/SyncRulesListPage"; import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; -import { useAppContext } from "../react/contexts/AppContext"; +import { useAppContext } from "../react/core/contexts/AppContext"; import * as permissions from "../../utils/permissions"; import HomePage from "./core/pages/home/HomePage"; import { MSFHomePage } from "./msf-aggregate-data/pages/MSFHomePage"; diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index 5a34d5427..ab14e06b7 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -14,12 +14,12 @@ import { MigrationsRunner } from "../../migrations"; import { D2Api } from "../../types/d2-api"; import { debug } from "../../utils/debug"; import { initializeAppRoles } from "../../utils/permissions"; -import { AppContext } from "../react/contexts/AppContext"; -import muiThemeLegacy from "../react/themes/dhis2-legacy.theme"; -import { muiTheme } from "../react/themes/dhis2.theme"; +import { AppContext } from "../react/core/contexts/AppContext"; +import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; +import { muiTheme } from "../react/core/themes/dhis2.theme"; import { CompositionRoot } from "../CompositionRoot"; -import Migrations from "../react/components/migrations/Migrations"; -import Share from "../react/components/share/Share"; +import Migrations from "../react/core/components/migrations/Migrations"; +import Share from "../react/core/components/share/Share"; import Root from "./Root"; import "./WebApp.css"; diff --git a/src/presentation/webapp/core/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx index c2e2cf138..8e98b8f82 100644 --- a/src/presentation/webapp/core/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -24,12 +24,12 @@ import SyncReport from "../../../../../models/syncReport"; import SyncRule from "../../../../../models/syncRule"; import { getValueForCollection } from "../../../../../utils/d2-ui-components"; import { isAppConfigurator } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import Dropdown from "../../../../react/components/dropdown/Dropdown"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import SyncSummary, { formatStatusTag, -} from "../../../../react/components/sync-summary/SyncSummary"; +} from "../../../../react/core/components/sync-summary/SyncSummary"; const config = { metadata: { diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index 246e6ec65..57d513bbf 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -8,9 +8,9 @@ import { isAppExecutor, shouldShowDeletedObjects, } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import { Card, Landing } from "../../../../react/components/landing/Landing"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import { Card, Landing } from "../../../../react/core/components/landing/Landing"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; import { AppVariant } from "../../../Root"; const appVariantConfiguration: Record = { diff --git a/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx b/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx index 1215445f3..e7d8313a9 100644 --- a/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx +++ b/src/presentation/webapp/core/pages/instance-creation/GeneralInfoForm.tsx @@ -7,7 +7,7 @@ import { useHistory } from "react-router-dom"; import { ValidationError } from "../../../../../domain/common/entities/Validations"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../locales"; -import { useAppContext } from "../../../../react/contexts/AppContext"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; import SaveButton from "./SaveButton"; export interface GeneralInfoFormProps { diff --git a/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx b/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx index 8cf472822..3aa48b216 100644 --- a/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx +++ b/src/presentation/webapp/core/pages/instance-creation/InstanceCreationPage.tsx @@ -3,9 +3,9 @@ import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../locales"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; import GeneralInfoForm from "./GeneralInfoForm"; const InstanceCreationPage = () => { diff --git a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx index 4b52e8ec6..1f5c248a4 100644 --- a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx @@ -22,9 +22,9 @@ import { Instance } from "../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../locales"; import { executeAnalytics } from "../../../../../utils/analytics"; import { isAppConfigurator } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; const InstanceListPage = () => { const { api, compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx index a9c4ddbd5..8f2564788 100644 --- a/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx +++ b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingLandingPage.tsx @@ -3,9 +3,9 @@ import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../locales"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import { Card, Landing } from "../../../../react/components/landing/Landing"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import { Card, Landing } from "../../../../react/core/components/landing/Landing"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; const InstanceMappingLandingPage: React.FC = () => { const { compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx index e37b9784c..880dfd028 100644 --- a/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx +++ b/src/presentation/webapp/core/pages/instance-mapping/InstanceMappingPage.tsx @@ -21,9 +21,9 @@ import { IndicatorMappedModel, OrganisationUnitMappedModel, } from "../../../../../models/dhis/mapping"; -import MappingTable from "../../../../react/components/mapping-table/MappingTable"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../../react/contexts/AppContext"; +import MappingTable from "../../../../react/core/components/mapping-table/MappingTable"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; export type MappingType = "aggregated" | "tracker" | "orgUnit"; diff --git a/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx b/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx index ccbdae42a..acd449c32 100644 --- a/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/InstancesSelectors.tsx @@ -7,7 +7,7 @@ import { Maybe } from "../../../../../types/utils"; import { InstanceSelectionDropdown, InstanceSelectionDropdownProps, -} from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; +} from "../../../../react/core/components/instance-selection-dropdown/InstanceSelectionDropdown"; interface InstancesSelectorsProps { sourceInstance: Maybe; diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 47b5b4ff0..594ff4c2d 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -29,18 +29,18 @@ import SyncRule from "../../../../../models/syncRule"; import { Ref } from "../../../../../types/d2-api"; import { MetadataType } from "../../../../../utils/d2"; import { isAppConfigurator } from "../../../../../utils/permissions"; -import { InstanceSelectionOption } from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import DeletedObjectsTable from "../../../../react/components/delete-objects-table/DeletedObjectsTable"; -import MetadataTable from "../../../../react/components/metadata-table/MetadataTable"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { InstanceSelectionOption } from "../../../../react/core/components/instance-selection-dropdown/InstanceSelectionDropdown"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import DeletedObjectsTable from "../../../../react/core/components/delete-objects-table/DeletedObjectsTable"; +import MetadataTable from "../../../../react/core/components/metadata-table/MetadataTable"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import { PullRequestCreation, PullRequestCreationDialog, -} from "../../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; -import SyncDialog from "../../../../react/components/sync-dialog/SyncDialog"; -import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +} from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import SyncDialog from "../../../../react/core/components/sync-dialog/SyncDialog"; +import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; import InstancesSelectors from "./InstancesSelectors"; import { Store } from "../../../../../domain/stores/entities/Store"; diff --git a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index 8b5afe1fe..9905c7a9b 100644 --- a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -6,16 +6,16 @@ import { Instance } from "../../../../../domain/instance/entities/Instance"; import { Store } from "../../../../../domain/stores/entities/Store"; import i18n from "../../../../../locales"; import SyncReport from "../../../../../models/syncReport"; -import { CreatePackageFromFileDialog } from "../../../../react/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; +import { CreatePackageFromFileDialog } from "../../../../react/core/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; import { ModulePackageListTable, PresentationOption, ViewOption, -} from "../../../../react/components/module-package-list-table/ModulePackageListTable"; -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"; +} from "../../../../react/core/components/module-package-list-table/ModulePackageListTable"; +import PackageImportDialog from "../../../../react/core/components/package-import-dialog/PackageImportDialog"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; export interface ModulePackageListPageProps { remoteInstance?: Instance; diff --git a/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx b/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx index 9860d78fc..6effb094e 100644 --- a/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx +++ b/src/presentation/webapp/core/pages/modules-creation/ModuleCreationPage.tsx @@ -3,9 +3,9 @@ import React, { useCallback, useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; import { Module } from "../../../../../domain/modules/entities/Module"; import i18n from "../../../../../locales"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import { ModuleWizard } from "../../../../react/components/module-wizard/ModuleWizard"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import { ModuleWizard } from "../../../../react/core/components/module-wizard/ModuleWizard"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; const ModuleCreationPage: React.FC = () => { diff --git a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index 40390efd1..a7fe529ad 100644 --- a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -26,11 +26,11 @@ import { UpdatePullRequestStatusError } from "../../../../../domain/notification import { SynchronizationResult } from "../../../../../domain/synchronization/entities/SynchronizationResult"; import i18n from "../../../../../locales"; import SyncReport from "../../../../../models/syncReport"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import Dropdown from "../../../../react/components/dropdown/Dropdown"; -import { NotificationViewerDialog } from "../../../../react/components/notification-viewer-dialog/NotificationViewerDialog"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; +import { NotificationViewerDialog } from "../../../../react/core/components/notification-viewer-dialog/NotificationViewerDialog"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; export const NotificationsListPage: React.FC = () => { const { api, compositionRoot } = useAppContext(); diff --git a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx index bcc31e3a1..52420fe89 100644 --- a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx +++ b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx @@ -9,10 +9,10 @@ import { isAppConfigurator } from "../../../../../utils/permissions"; import { InstanceSelectionDropdown, InstanceSelectionOption, -} from "../../../../react/components/instance-selection-dropdown/InstanceSelectionDropdown"; -import MetadataTable from "../../../../react/components/metadata-table/MetadataTable"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../../react/contexts/AppContext"; +} from "../../../../react/core/components/instance-selection-dropdown/InstanceSelectionDropdown"; +import MetadataTable from "../../../../react/core/components/metadata-table/MetadataTable"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; export const ResponsiblesListPage: React.FC = () => { const { compositionRoot, api } = useAppContext(); diff --git a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx index 5dae531f4..fe6f0a6fc 100644 --- a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx @@ -12,8 +12,8 @@ import { useHistory, useParams } from "react-router-dom"; import { GitHubError } from "../../../../../domain/packages/entities/Errors"; import { Store } from "../../../../../domain/stores/entities/Store"; import i18n from "../../../../../locales"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import helpStoreGithub from "../../../../assets/img/help-store-github.png"; const StoreCreationPage: React.FC = () => { diff --git a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx index fe7386c2f..df39fbc08 100644 --- a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx @@ -15,8 +15,8 @@ import { useHistory } from "react-router-dom"; import { GitHubError } from "../../../../../domain/packages/entities/Errors"; import { Store } from "../../../../../domain/stores/entities/Store"; import i18n from "../../../../../locales"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import { useAppContext } from "../../../../react/contexts/AppContext"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; import SettingsInputAntenaIcon from "@material-ui/icons/SettingsInputAntenna"; export const StoreListPage: React.FC = () => { diff --git a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx index 94c1cc53c..687f21c78 100644 --- a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx @@ -1,10 +1,10 @@ import { ConfirmationDialog, useLoading } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; -import SyncWizard from "../../../../react/components/sync-wizard/SyncWizard"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; -import { useAppContext } from "../../../../react/contexts/AppContext"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import SyncWizard from "../../../../react/core/components/sync-wizard/SyncWizard"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../../locales"; import SyncRule from "../../../../../models/syncRule"; diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index fdab04e89..eeeeca4a5 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -36,16 +36,16 @@ import { UserInfo, } from "../../../../../utils/permissions"; import { requestJSONDownload } from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../../react/contexts/AppContext"; -import Dropdown from "../../../../react/components/dropdown/Dropdown"; -import PageHeader from "../../../../react/components/page-header/PageHeader"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; +import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import { PullRequestCreation, PullRequestCreationDialog, -} from "../../../../react/components/pull-request-creation-dialog/PullRequestCreationDialog"; -import { SharingDialog } from "../../../../react/components/sharing-dialog/SharingDialog"; -import SyncSummary from "../../../../react/components/sync-summary/SyncSummary"; -import { TestWrapper } from "../../../../react/components/test-wrapper/TestWrapper"; +} from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import { SharingDialog } from "../../../../react/core/components/sharing-dialog/SharingDialog"; +import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; +import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; const config: { [key: string]: { diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 4bf1668a8..34fe7f7dd 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -4,9 +4,9 @@ import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { DataSyncPeriod } from "../../../../domain/aggregated/types"; import { isGlobalAdmin } from "../../../../utils/permissions"; -import PageHeader from "../../../react/components/page-header/PageHeader"; -import { PeriodSelectionDialog } from "../../../react/components/period-selection-dialog/PeriodSelectionDialog"; -import { useAppContext } from "../../../react/contexts/AppContext"; +import PageHeader from "../../../react/core/components/page-header/PageHeader"; +import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; +import { useAppContext } from "../../../react/core/contexts/AppContext"; export interface PeriodFilter { period: DataSyncPeriod; diff --git a/src/presentation/widget/WidgetApp.jsx b/src/presentation/widget/WidgetApp.jsx index 18f0f4195..e0c5a88ee 100644 --- a/src/presentation/widget/WidgetApp.jsx +++ b/src/presentation/widget/WidgetApp.jsx @@ -10,9 +10,9 @@ import i18n from "../../locales"; import { MigrationsRunner } from "../../migrations"; import { D2Api } from "../../types/d2-api"; import { debug } from "../../utils/debug"; -import { AppContext } from "../react/contexts/AppContext"; -import muiThemeLegacy from "../react/themes/dhis2-legacy.theme"; -import { muiTheme } from "../react/themes/dhis2.theme"; +import { AppContext } from "../react/core/contexts/AppContext"; +import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; +import { muiTheme } from "../react/core/themes/dhis2.theme"; import { CompositionRoot } from "../CompositionRoot"; import Root from "./pages/Root"; import "./WidgetApp.css"; diff --git a/src/presentation/widget/pages/module-list-widget/ModuleListWidget.tsx b/src/presentation/widget/pages/module-list-widget/ModuleListWidget.tsx index d84987283..e4bc892ca 100644 --- a/src/presentation/widget/pages/module-list-widget/ModuleListWidget.tsx +++ b/src/presentation/widget/pages/module-list-widget/ModuleListWidget.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ModulePackageListTable } from "../../../react/components/module-package-list-table/ModulePackageListTable"; +import { ModulePackageListTable } from "../../../react/core/components/module-package-list-table/ModulePackageListTable"; const showSelector = { modules: true, diff --git a/src/presentation/widget/pages/package-exporter-widget/PackageExporterWidget.tsx b/src/presentation/widget/pages/package-exporter-widget/PackageExporterWidget.tsx index 650e903ae..517ccbecb 100644 --- a/src/presentation/widget/pages/package-exporter-widget/PackageExporterWidget.tsx +++ b/src/presentation/widget/pages/package-exporter-widget/PackageExporterWidget.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { ModulePackageListTable } from "../../../react/components/module-package-list-table/ModulePackageListTable"; +import { ModulePackageListTable } from "../../../react/core/components/module-package-list-table/ModulePackageListTable"; const showSelector = { modules: false, From 654c290c544ca09be51e36057e05a89cd198519b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 10:59:15 +0100 Subject: [PATCH 047/163] Remove old structure react folder --- .../components/auth/RouteWithSession.tsx | 23 - .../auth/RouteWithSessionAndAuth.tsx | 32 - .../components/auth/WithAuthorization.tsx | 27 - .../react/components/auth/WithSession.tsx | 48 - .../CreatePackageFromFileDialog.tsx | 313 ------ .../DeletedObjectsTable.tsx | 105 -- .../react/components/dropdown/Dropdown.tsx | 121 --- .../filter-rules-table/FilterRuleDialog.tsx | 136 --- .../filter-rules-table/FilterRulesTable.tsx | 163 --- .../components/filter-rules-table/Section.tsx | 38 - .../InstanceSelectionDropdown.tsx | 95 -- .../react/components/landing/Landing.tsx | 66 -- .../react/components/landing/MenuCard.tsx | 117 --- .../mapping-dialog/MappingDialog.tsx | 181 ---- .../components/mapping-table/MappingTable.tsx | 863 ---------------- .../react/components/mapping-table/utils.tsx | 17 - .../mapping-wizard/MappingWizard.tsx | 126 --- .../react/components/mapping-wizard/Steps.tsx | 57 -- .../metadata-drop-zone/MetadataDropZone.tsx | 92 -- .../metadata-table/MetadataTable.tsx | 596 ----------- .../react/components/metadata-table/utils.tsx | 38 - .../components/migrations/Migrations.tsx | 148 --- .../module-list-table/ModuleListTable.tsx | 556 ----------- .../module-list-table/NewPackageDialog.tsx | 168 ---- .../components/module-list-table/utils.ts | 56 -- .../ModulePackageListTable.tsx | 133 --- .../useViewSelector.tsx | 29 - .../components/module-wizard/ModuleWizard.tsx | 49 - .../react/components/module-wizard/Steps.ts | 62 -- .../module-wizard/common/GeneralInfoStep.tsx | 85 -- .../common/MetadataSelectionStep.tsx | 60 -- .../module-wizard/common/SummaryStep.tsx | 150 --- .../metadata/AdvancedMetadataOptionsStep.tsx | 37 - .../metadata/MetadataIncludeExcludeStep.tsx | 127 --- .../NotificationViewerDialog.tsx | 72 -- .../PackageImportDialog.tsx | 235 ----- .../PackageImportWizard.tsx | 95 -- .../steps/InstanceStoreSelectionStep.tsx | 54 - .../steps/PackageMappingStep.tsx | 302 ------ .../steps/PackageSelectionStep.tsx | 34 - .../steps/SummaryStep.tsx | 94 -- .../package-list-table/PackageListTable.tsx | 936 ------------------ .../package-list-table/PackageModuleItem.ts | 47 - .../PackagesDiffDialog.tsx | 167 ---- .../components/packages-diff-dialog/utils.tsx | 80 -- .../components/page-header/PageHeader.tsx | 74 -- .../PeriodSelectionDialog.tsx | 53 - .../period-selection/PeriodSelection.tsx | 143 --- .../PullRequestCreationDialog.tsx | 207 ---- .../radio-button-group/RadioButtonGroup.tsx | 59 -- .../responsible-dialog/ResponsibleDialog.tsx | 96 -- .../react/components/share/Share.jsx | 144 --- .../react/components/share/logo-eyeseetea.png | Bin 16196 -> 0 bytes .../sharing-dialog/SharingDialog.tsx | 44 - .../store-creation/StoreCreationDialog.tsx | 252 ----- .../components/sync-dialog/SyncDialog.tsx | 54 - .../SyncParamsSelector.tsx | 201 ---- .../components/sync-summary/SyncSummary.tsx | 319 ------ .../react/components/sync-wizard/Steps.ts | 200 ---- .../components/sync-wizard/SyncWizard.tsx | 86 -- .../sync-wizard/common/GeneralInfoStep.tsx | 102 -- .../common/InstanceSelectionStep.tsx | 90 -- .../common/MetadataFilterRulesStep.tsx | 17 - .../common/MetadataSelectionStep.tsx | 125 --- .../sync-wizard/common/SchedulerStep.jsx | 83 -- .../sync-wizard/common/SummaryStep.jsx | 460 --------- .../sync-wizard/data/AggregationStep.tsx | 83 -- .../data/CategoryOptionsSelectionStep.tsx | 80 -- .../sync-wizard/data/EventsSelectionStep.tsx | 198 ---- .../data/OrganisationUnitsSelectionStep.tsx | 55 - .../sync-wizard/data/PeriodSelectionStep.tsx | 59 -- .../metadata/MetadataIncludeExcludeStep.tsx | 124 --- .../components/test-wrapper/TestWrapper.tsx | 70 -- .../react/components/test-wrapper/utils.tsx | 75 -- .../text-field-on-blur/TextFieldOnBlur.tsx | 51 - .../react/components/toggle/Toggle.tsx | 34 - src/presentation/react/contexts/AppContext.ts | 20 - src/presentation/react/hooks/useOpenState.ts | 10 - .../react/hooks/useQueryParams.ts | 7 - .../react/themes/dhis2-legacy.theme.js | 62 -- src/presentation/react/themes/dhis2.theme.js | 91 -- 81 files changed, 10858 deletions(-) delete mode 100644 src/presentation/react/components/auth/RouteWithSession.tsx delete mode 100644 src/presentation/react/components/auth/RouteWithSessionAndAuth.tsx delete mode 100644 src/presentation/react/components/auth/WithAuthorization.tsx delete mode 100644 src/presentation/react/components/auth/WithSession.tsx delete mode 100644 src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx delete mode 100644 src/presentation/react/components/delete-objects-table/DeletedObjectsTable.tsx delete mode 100644 src/presentation/react/components/dropdown/Dropdown.tsx delete mode 100644 src/presentation/react/components/filter-rules-table/FilterRuleDialog.tsx delete mode 100644 src/presentation/react/components/filter-rules-table/FilterRulesTable.tsx delete mode 100644 src/presentation/react/components/filter-rules-table/Section.tsx delete mode 100644 src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx delete mode 100644 src/presentation/react/components/landing/Landing.tsx delete mode 100644 src/presentation/react/components/landing/MenuCard.tsx delete mode 100644 src/presentation/react/components/mapping-dialog/MappingDialog.tsx delete mode 100644 src/presentation/react/components/mapping-table/MappingTable.tsx delete mode 100644 src/presentation/react/components/mapping-table/utils.tsx delete mode 100644 src/presentation/react/components/mapping-wizard/MappingWizard.tsx delete mode 100644 src/presentation/react/components/mapping-wizard/Steps.tsx delete mode 100644 src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx delete mode 100644 src/presentation/react/components/metadata-table/MetadataTable.tsx delete mode 100644 src/presentation/react/components/metadata-table/utils.tsx delete mode 100644 src/presentation/react/components/migrations/Migrations.tsx delete mode 100644 src/presentation/react/components/module-list-table/ModuleListTable.tsx delete mode 100644 src/presentation/react/components/module-list-table/NewPackageDialog.tsx delete mode 100644 src/presentation/react/components/module-list-table/utils.ts delete mode 100644 src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx delete mode 100644 src/presentation/react/components/module-package-list-table/useViewSelector.tsx delete mode 100644 src/presentation/react/components/module-wizard/ModuleWizard.tsx delete mode 100644 src/presentation/react/components/module-wizard/Steps.ts delete mode 100644 src/presentation/react/components/module-wizard/common/GeneralInfoStep.tsx delete mode 100644 src/presentation/react/components/module-wizard/common/MetadataSelectionStep.tsx delete mode 100644 src/presentation/react/components/module-wizard/common/SummaryStep.tsx delete mode 100644 src/presentation/react/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx delete mode 100644 src/presentation/react/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx delete mode 100644 src/presentation/react/components/notification-viewer-dialog/NotificationViewerDialog.tsx delete mode 100644 src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx delete mode 100644 src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx delete mode 100644 src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx delete mode 100644 src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx delete mode 100644 src/presentation/react/components/package-import-wizard/steps/PackageSelectionStep.tsx delete mode 100644 src/presentation/react/components/package-import-wizard/steps/SummaryStep.tsx delete mode 100644 src/presentation/react/components/package-list-table/PackageListTable.tsx delete mode 100644 src/presentation/react/components/package-list-table/PackageModuleItem.ts delete mode 100644 src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx delete mode 100644 src/presentation/react/components/packages-diff-dialog/utils.tsx delete mode 100644 src/presentation/react/components/page-header/PageHeader.tsx delete mode 100644 src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx delete mode 100644 src/presentation/react/components/period-selection/PeriodSelection.tsx delete mode 100644 src/presentation/react/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx delete mode 100644 src/presentation/react/components/radio-button-group/RadioButtonGroup.tsx delete mode 100644 src/presentation/react/components/responsible-dialog/ResponsibleDialog.tsx delete mode 100644 src/presentation/react/components/share/Share.jsx delete mode 100644 src/presentation/react/components/share/logo-eyeseetea.png delete mode 100644 src/presentation/react/components/sharing-dialog/SharingDialog.tsx delete mode 100644 src/presentation/react/components/store-creation/StoreCreationDialog.tsx delete mode 100644 src/presentation/react/components/sync-dialog/SyncDialog.tsx delete mode 100644 src/presentation/react/components/sync-params-selector/SyncParamsSelector.tsx delete mode 100644 src/presentation/react/components/sync-summary/SyncSummary.tsx delete mode 100644 src/presentation/react/components/sync-wizard/Steps.ts delete mode 100644 src/presentation/react/components/sync-wizard/SyncWizard.tsx delete mode 100644 src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/common/MetadataFilterRulesStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/common/MetadataSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/common/SchedulerStep.jsx delete mode 100644 src/presentation/react/components/sync-wizard/common/SummaryStep.jsx delete mode 100644 src/presentation/react/components/sync-wizard/data/AggregationStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/data/EventsSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/data/PeriodSelectionStep.tsx delete mode 100644 src/presentation/react/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx delete mode 100644 src/presentation/react/components/test-wrapper/TestWrapper.tsx delete mode 100644 src/presentation/react/components/test-wrapper/utils.tsx delete mode 100644 src/presentation/react/components/text-field-on-blur/TextFieldOnBlur.tsx delete mode 100644 src/presentation/react/components/toggle/Toggle.tsx delete mode 100644 src/presentation/react/contexts/AppContext.ts delete mode 100644 src/presentation/react/hooks/useOpenState.ts delete mode 100644 src/presentation/react/hooks/useQueryParams.ts delete mode 100644 src/presentation/react/themes/dhis2-legacy.theme.js delete mode 100644 src/presentation/react/themes/dhis2.theme.js diff --git a/src/presentation/react/components/auth/RouteWithSession.tsx b/src/presentation/react/components/auth/RouteWithSession.tsx deleted file mode 100644 index b07a7d454..000000000 --- a/src/presentation/react/components/auth/RouteWithSession.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from "react"; -import { Route, RouteComponentProps } from "react-router-dom"; -import WithSession from "./WithSession"; - -export interface RouteWithSessionProps { - render: (props: RouteComponentProps) => React.ReactNode; - path?: string | string[]; - exact?: boolean; -} - -const RouteWithSession: React.FC = ({ path, render, exact }) => { - const key = path?.toString() ?? ""; - - return ( - {render(props)}} - /> - ); -}; - -export default RouteWithSession; diff --git a/src/presentation/react/components/auth/RouteWithSessionAndAuth.tsx b/src/presentation/react/components/auth/RouteWithSessionAndAuth.tsx deleted file mode 100644 index 99fe0b79a..000000000 --- a/src/presentation/react/components/auth/RouteWithSessionAndAuth.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from "react"; -import { Route, RouteComponentProps } from "react-router-dom"; -import WithSession from "./WithSession"; -import WithAuthorization from "./WithAuthorization"; -import { RouteWithSessionProps } from "./RouteWithSession"; - -export interface RouteWithSessionAndAuthProps extends RouteWithSessionProps { - authorize: (props: RouteComponentProps) => Promise; -} - -const RouteWithSessionAndAuth: React.FC = ({ - path, - render, - authorize, -}) => { - const key = path?.toString() ?? ""; - - return ( - ( - - authorize(props)}> - {render(props)} - - - )} - /> - ); -}; - -export default RouteWithSessionAndAuth; diff --git a/src/presentation/react/components/auth/WithAuthorization.tsx b/src/presentation/react/components/auth/WithAuthorization.tsx deleted file mode 100644 index 24099196f..000000000 --- a/src/presentation/react/components/auth/WithAuthorization.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React, { useState, useEffect } from "react"; -import i18n from "../../../../locales"; -import { Typography } from "@material-ui/core"; - -interface AuthorizationProps { - authorize: () => Promise; -} - -const Authorization: React.FC = ({ authorize, children }) => { - const [isAuthorize, setIsAuthorize] = useState(true); - - useEffect(() => { - authorize().then(setIsAuthorize); - }, [authorize]); - - if (isAuthorize) { - return {children}; - } else { - return ( - - {i18n.t("Unauthorized - You do not have permission to view this page.")} - - ); - } -}; - -export default Authorization; diff --git a/src/presentation/react/components/auth/WithSession.tsx b/src/presentation/react/components/auth/WithSession.tsx deleted file mode 100644 index 046cf62ff..000000000 --- a/src/presentation/react/components/auth/WithSession.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { useState, useEffect } from "react"; -import i18n from "../../../../locales"; -import { Typography, CircularProgress, makeStyles } from "@material-ui/core"; -import { useAppContext } from "../../contexts/AppContext"; - -const useStyles = makeStyles({ - loading: { - display: "flex", - justifyContent: "center", - }, -}); - -const WithSession: React.FC = ({ children }) => { - const { api } = useAppContext(); - const classes = useStyles(); - - const [isLoggedIn, setLoggedIn] = useState(undefined); - - useEffect(() => { - api.currentUser - .get({ fields: { id: true } }) - .getData() - .then(() => setLoggedIn(true)) - .catch(() => setLoggedIn(false)); - }, [api]); - - if (isLoggedIn === undefined) { - return ( -
    - -
    - ); - } else if (isLoggedIn === false) { - const { baseUrl } = api; - return ( - - - {i18n.t("Login")} - - {` ${baseUrl}`} - - ); - } else { - return {children}; - } -}; - -export default WithSession; 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 deleted file mode 100644 index 9e8bf1475..000000000 --- a/src/presentation/react/components/create-package-from-file-dialog/CreatePackageFromFileDialog.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import i18n from "../../../../locales"; -import MetadataDropZone from "../metadata-drop-zone/MetadataDropZone"; -import { MetadataPackage } from "../../../../domain/metadata/entities/MetadataEntities"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - makeStyles, - TextField, -} from "@material-ui/core"; -import Autocomplete from "@material-ui/lab/Autocomplete"; -import { useLoading, useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useState } from "react"; -import semver from "semver"; -import { ValidationError } from "../../../../domain/common/entities/Validations"; -import { Package } from "../../../../domain/packages/entities/Package"; -import { Dictionary } from "../../../../types/utils"; -import { useAppContext } from "../../contexts/AppContext"; -import { MetadataModule } from "../../../../domain/modules/entities/MetadataModule"; -import { promiseMap } from "../../../../utils/common"; -import { getValidationsByVersionFeedback } from "../module-list-table/utils"; -import { NamedRef } from "../../../../domain/common/entities/Ref"; -import Dropdown from "../dropdown/Dropdown"; -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(); - const snackbar = useSnackbar(); - const classes = useStyles(); - - const [versions, updateVersions] = useState([]); - const [module, setModule] = useState(MetadataModule.build({ autogenerated: true })); - - const [newPackage, setNewPackage] = useState( - Package.build({ - name: "", - module, - version: "1.0.0", - }) - ); - const [userGroups, setUserGroups] = useState([]); - const [contents, setContents] = useState(); - - const [errors, setErrors] = useState>({}); - - useEffect(() => { - compositionRoot.instances.getVersion().then(version => { - if (versions.length === 0) updateVersions([version]); - }); - }, [compositionRoot, versions, updateVersions]); - - useEffect(() => { - compositionRoot.instances.getUserGroups().then(setUserGroups); - }, [compositionRoot]); - - const updateModel = useCallback( - (field: keyof Package, value: string) => { - const pkg = newPackage.update({ [field]: value }); - const errors = _.keyBy(pkg.validate([field], module), "property"); - - setErrors(errors); - setNewPackage(pkg); - }, - [newPackage, module] - ); - - const onChangeField = useCallback( - (field: keyof Package) => { - return (event: React.ChangeEvent<{ value: unknown }>) => { - updateModel(field, event.target.value as string); - }; - }, - [updateModel] - ); - - const updateVersionNumber = useCallback( - (event: React.ChangeEvent<{ value: unknown }>) => { - const revision = event.target.value as string; - const tag = newPackage.version.split("-")[1]; - const newVersion = [revision, tag].join("-"); - updateModel("version", newVersion); - }, - [newPackage, updateModel] - ); - - const updateVersionTag = useCallback( - (event: React.ChangeEvent<{ value: unknown }>) => { - const revision = newPackage.version.split("-")[0]; - const tag = event.target.value ? (event.target.value as string) : undefined; - const newVersion = semver.parse([revision, tag].join("-"))?.format(); - updateModel("version", newVersion ?? revision); - }, - [newPackage, updateModel] - ); - - const saveModuleAndPackage = async () => { - const moduleErrors = (await compositionRoot.modules.save(module)) - .filter(error => error.property !== "name") - .map(error => - error.property === "metadataIds" - ? { ...error, description: i18n.t("An exported dhis2 file is necessary") } - : error - ); - - if (moduleErrors.length > 0) { - snackbar.error(moduleErrors.map(error => error.description).join("\n")); - } else { - const savedModule = await compositionRoot.modules.get(module.id); - - if (!savedModule) { - i18n.t("An error has ocurred to find the autogenerated module"); - } else { - const validationsByVersion = _.fromPairs( - await promiseMap(versions, async dhisVersion => { - loading.show( - true, - i18n.t("Creating {{dhisVersion}} package for module {{name}}", { - name: module.name, - dhisVersion, - }) - ); - - if (!contents) { - snackbar.error(i18n.t("An exported dhis2 file is necessary")); - } - - const validations = await compositionRoot.packages.create( - "LOCAL", - newPackage.update({ module: savedModule }), - savedModule, - dhisVersion, - contents - ); - - return [dhisVersion, validations]; - }) - ); - - const [level, msg] = getValidationsByVersionFeedback(module, validationsByVersion); - snackbar.openSnackbar(level, msg); - - loading.reset(); - onClose(); - } - } - }; - - const onSave = async (importAfter: boolean) => { - i18n.t("Creating autogenerated module"); - const moduleErrors = module - .validate() - .filter(error => error.property !== "name") - .map(error => - error.property === "metadataIds" - ? { ...error, description: i18n.t("An exported dhis2 file is necessary") } - : error - ); - - const errors = [...moduleErrors, ...newPackage.validate(undefined, module)]; - const messages = _.keyBy(errors, "property"); - - if (errors.length === 0) { - await saveModuleAndPackage(); - - if (importAfter && onImport) onImport(newPackage.id); - if (!importAfter && onSaved) onSaved(); - } else { - snackbar.error(errors.map(error => error.description).join("\n")); - setErrors(messages); - } - }; - - const onChangeDepartment = (id: string) => { - const department = userGroups.find(group => group.id === id); - const updatedModule = module.update({ department }); - setModule(updatedModule); - setNewPackage(newPackage.update({ module: updatedModule })); - }; - - const onFileChange = (fileName: string, metadataPackage: MetadataPackage) => { - const metadataIds: string[] = Object.entries(metadataPackage).reduce( - (acc: string[], [_key, items]) => { - const ids: string[] = items ? items.map(item => item.id) : []; - return [...acc, ...ids]; - }, - [] - ); - - const updatedModule = module.update({ name: fileName, metadataIds }); - setModule(updatedModule); - setNewPackage(newPackage.update({ module: updatedModule })); - setContents(metadataPackage); - }; - - return ( - - {i18n.t("Generate package from File")} - - - - -
    - -
    - -
    - - -
    - - updateVersions(value)} - renderTags={(values: string[]) => values.sort().join(", ")} - renderInput={params => ( - - )} - /> - - - - -
    - - - - - - - - -
    - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, - versionRow: { - width: "100%", - display: "flex", - flex: "1 1 auto", - marginBottom: 25, - }, - marginRight: { - marginRight: 10, - }, -}); diff --git a/src/presentation/react/components/delete-objects-table/DeletedObjectsTable.tsx b/src/presentation/react/components/delete-objects-table/DeletedObjectsTable.tsx deleted file mode 100644 index 2302d5427..000000000 --- a/src/presentation/react/components/delete-objects-table/DeletedObjectsTable.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import SyncIcon from "@material-ui/icons/Sync"; -import { - ObjectsTable, - ObjectsTableDetailField, - ReferenceObject, - TableColumn, - TableState, - DatePicker, -} from "d2-ui-components"; -import React, { useEffect, useState } from "react"; -import DeletedObject from "../../../../models/deletedObjects"; -import SyncRule from "../../../../models/syncRule"; -import { MetadataType } from "../../../../utils/d2"; -import moment from "moment"; -import { useAppContext } from "../../contexts/AppContext"; -import i18n from "../../../../locales"; - -export interface DeletedObjectsTableProps { - openSynchronizationDialog: () => void; - syncRule: SyncRule; - onChange: (syncRule: SyncRule) => void; -} - -const DeletedObjectsTable: React.FC = ({ - openSynchronizationDialog, - syncRule, - onChange, -}) => { - const { api } = useAppContext(); - - const [deletedObjectsRows, setDeletedObjectsRows] = useState([]); - const [search, setSearch] = useState(undefined); - const [dateFilter, setDateFilter] = useState(null); - - const deletedObjectsColumns: TableColumn[] = [ - { name: "id", text: i18n.t("Identifier"), sortable: true }, - { name: "code", text: i18n.t("Code"), sortable: true }, - { name: "klass", text: i18n.t("Metadata type"), sortable: true }, - { name: "deletedAt", text: i18n.t("Deleted date"), sortable: true }, - { name: "deletedBy", text: i18n.t("Deleted by"), sortable: true }, - ]; - - const deletedObjectsDetails: ObjectsTableDetailField[] = [ - { name: "id", text: i18n.t("Identifier") }, - { name: "code", text: i18n.t("Code") }, - { name: "klass", text: i18n.t("Metadata type") }, - { name: "deletedAt", text: i18n.t("Deleted date") }, - { name: "deletedBy", text: i18n.t("Deleted by") }, - ]; - - const deletedObjectsActions = [ - { - name: "details", - text: i18n.t("Details"), - multiple: false, - type: "details", - }, - ]; - - useEffect(() => { - DeletedObject.list( - api, - { - search, - lastUpdatedDate: - dateFilter !== null ? moment(dateFilter).startOf("day") : undefined, - }, - {} - ).then(({ objects }) => setDeletedObjectsRows(objects)); - }, [api, search, dateFilter]); - - const handleTableChange = (tableState: TableState) => { - const { selection } = tableState; - onChange(syncRule.updateMetadataIds(selection.map(({ id }) => id))); - }; - - const filterComponents = ( - - - - ); - - return ( - - rows={deletedObjectsRows} - columns={deletedObjectsColumns} - details={deletedObjectsDetails} - actions={deletedObjectsActions} - forceSelectionColumn={true} - onActionButtonClick={openSynchronizationDialog} - onChange={handleTableChange} - actionButtonLabel={} - onChangeSearch={setSearch} - searchBoxLabel={i18n.t("Search deleted objects")} - filterComponents={filterComponents} - /> - ); -}; - -export default DeletedObjectsTable; diff --git a/src/presentation/react/components/dropdown/Dropdown.tsx b/src/presentation/react/components/dropdown/Dropdown.tsx deleted file mode 100644 index 41bc47a5f..000000000 --- a/src/presentation/react/components/dropdown/Dropdown.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { FormControl, InputLabel, MenuItem, MuiThemeProvider, Select } from "@material-ui/core"; -import { createMuiTheme } from "@material-ui/core/styles"; -import _ from "lodash"; -import React from "react"; -import i18n from "../../../../locales"; - -export interface DropdownOption { - id: string; - name: string; -} - -export type DropdownViewOption = "filter" | "inline" | "full-width"; - -interface DropdownProps { - items: DropdownOption[]; - value: string; - label?: string; - onChange?: Function; - onValueChange?(value: string): void; - hideEmpty?: boolean; - emptyLabel?: string; - view?: DropdownViewOption; - disabled?: boolean; -} - -const getTheme = (view: DropdownViewOption) => { - switch (view) { - case "filter": - return createMuiTheme({ - overrides: { - MuiFormLabel: { - root: { - color: "#aaaaaa", - "&$focused": { - color: "#aaaaaa", - }, - top: "-9px !important", - marginLeft: 10, - }, - }, - MuiInput: { - root: { - marginLeft: 10, - }, - formControl: { - minWidth: 250, - marginTop: "8px !important", - }, - input: { - color: "#565656", - }, - }, - }, - }); - case "inline": - return createMuiTheme({ - overrides: { - MuiFormControl: { - root: { - verticalAlign: "middle", - marginBottom: 5, - }, - }, - }, - }); - default: - return {}; - } -}; - -const Dropdown: React.FC = ({ - items, - value, - onChange = _.noop, - onValueChange = _.noop, - label, - hideEmpty = false, - emptyLabel, - view = "filter", - disabled = false, -}) => { - const inlineStyles = { minWidth: 120, paddingLeft: 25, paddingRight: 25 }; - const styles = view === "inline" ? inlineStyles : {}; - - return ( - - - {view !== "inline" && label && {label}} - - - - ); -}; - -export default Dropdown; diff --git a/src/presentation/react/components/filter-rules-table/FilterRuleDialog.tsx b/src/presentation/react/components/filter-rules-table/FilterRuleDialog.tsx deleted file mode 100644 index 426218b72..000000000 --- a/src/presentation/react/components/filter-rules-table/FilterRuleDialog.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useMemo, useState } from "react"; -import { - FilterRule, - FilterRuleField, - FilterWhere, - updateFilterRule, - updateStringMatch, - validateFilterRule, - whereNames, -} from "../../../../domain/metadata/entities/FilterRule"; -import i18n from "../../../../locales"; -import { metadataModels } from "../../../../models/dhis/factory"; -import Dropdown from "../dropdown/Dropdown"; -import PeriodSelection from "../period-selection/PeriodSelection"; -import TextFieldOnBlur from "../text-field-on-blur/TextFieldOnBlur"; -import { Section } from "./Section"; - -export interface NewFilterRuleDialogProps { - action: "new" | "edit"; - onClose(): void; - onSave(filterRule: FilterRule): void; - initialFilterRule: FilterRule; -} - -export const FilterRuleDialog: React.FC = props => { - const { onClose, onSave, action, initialFilterRule } = props; - const classes = useStyles(); - const snackbar = useSnackbar(); - const [filterRule, setFilterRule] = useState(initialFilterRule); - - const metadataTypeItems = useMemo(() => { - return metadataModels.map(model => ({ - id: model.getMetadataType(), - name: model.getModelName(), - })); - }, []); - - function updateField(field: Field) { - return function (value: FilterRule[Field]) { - setFilterRule(filterRule => updateFilterRule(filterRule, field, value)); - }; - } - - const save = useCallback(() => { - const errors = validateFilterRule(filterRule); - if (_.isEmpty(errors)) { - onSave(filterRule); - } else { - snackbar.error(errors.map(error => error.description).join("\n")); - } - }, [filterRule, onSave, snackbar]); - - function updateStringMatchWhere(where: FilterWhere | "") { - const value = { where: where || null, ...(where ? {} : { value: "" }) }; - setFilterRule(filterRule => updateStringMatch(filterRule, value)); - } - - const title = action === "new" ? i18n.t("Create new filter") : i18n.t("Edit filter"); - const saveText = action === "new" ? i18n.t("Create") : i18n.t("Update"); - - return ( - - -
    - -
    - -
    - -
    - -
    - -
    - -
    -
    - -
    - -
    - - setFilterRule(filterRule => - updateStringMatch(filterRule, { value }) - ) - } - label={i18n.t("String to match (*)")} - value={filterRule.stringMatch?.value || ""} - /> -
    -
    -
    -
    - ); -}; - -const whereItems = _.map(whereNames, (name, key) => ({ id: key, name })); - -const useStyles = makeStyles({ - dropdown: { - marginTop: 20, - }, - textField: { - marginLeft: 10, - }, -}); diff --git a/src/presentation/react/components/filter-rules-table/FilterRulesTable.tsx b/src/presentation/react/components/filter-rules-table/FilterRulesTable.tsx deleted file mode 100644 index 0a064ec89..000000000 --- a/src/presentation/react/components/filter-rules-table/FilterRulesTable.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { Button, Icon } from "@material-ui/core"; -import { - ObjectsTable, - PaginationOptions, - TableAction, - TableColumn, - TableSelection, - TableState, -} from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useMemo, useState } from "react"; -import { updateObject as updateObjectInList } from "../../../../domain/common/entities/Ref"; -import { - FilterRule, - getDateFilterString, - getInitialFilterRule, - getStringMatchString, -} from "../../../../domain/metadata/entities/FilterRule"; -import i18n from "../../../../locales"; -import { metadataModels } from "../../../../models/dhis/factory"; -import { useOpenState } from "../../hooks/useOpenState"; -import { FilterRuleDialog, NewFilterRuleDialogProps } from "./FilterRuleDialog"; - -export interface FilterRulesTableProps { - filterRules: FilterRule[]; - onChange: (filterRules: FilterRule[]) => void; -} - -type Action = { type: "new" | "edit"; filterRule: FilterRule }; - -const FilterRulesTable: React.FC = props => { - const { filterRules, onChange } = props; - const [selection, updateSelection] = useState([]); - const newFilterRuleDialog = useOpenState(); - - const modelNames = useMemo(() => { - return _(metadataModels) - .map(model => [model.getMetadataType(), model.getModelName()] as [string, string]) - .fromPairs() - .value(); - }, []); - - const editRule = useCallback( - (ids: string[]) => { - const filterRule = _.find(filterRules, ({ id }) => id === ids[0]); - if (filterRule) newFilterRuleDialog.open({ type: "edit", filterRule }); - }, - [filterRules, newFilterRuleDialog] - ); - - const deleteRule = useCallback( - async (ids: string[]) => { - const newFilterRules = filterRules.filter(filterRule => !ids.includes(filterRule.id)); - onChange(newFilterRules); - updateSelection([]); - }, - [filterRules, onChange] - ); - - const updateTable = useCallback( - ({ selection }: TableState) => { - updateSelection(selection); - }, - [updateSelection] - ); - - const columns: TableColumn[] = useMemo( - () => [ - { - name: "metadataType", - text: i18n.t("Metadata type"), - getValue: rule => modelNames[rule.metadataType] || "-", - }, - { - name: "created", - text: i18n.t("Created"), - getValue: rule => getDateFilterString(rule.created), - }, - { - name: "lastUpdated", - text: i18n.t("Last updated"), - getValue: rule => getDateFilterString(rule.lastUpdated), - }, - { - name: "stringMatch", - text: i18n.t("Name/code/description"), - getValue: rule => getStringMatchString(rule.stringMatch) || "-", - }, - ], - [modelNames] - ); - - const actions: TableAction[] = useMemo( - () => [ - { - name: "edit", - text: i18n.t("Edit"), - multiple: false, - onClick: editRule, - icon: edit, - }, - { - name: "delete", - text: i18n.t("Delete"), - multiple: true, - onClick: deleteRule, - icon: delete, - }, - ], - [deleteRule, editRule] - ); - - const openNewDialog = useCallback(() => { - const newFilterRule = { type: "new" as const, filterRule: getInitialFilterRule() }; - newFilterRuleDialog.open(newFilterRule); - }, [newFilterRuleDialog]); - - const extraComponents = ( - - ); - - const { close: closeFilterRuleDialog } = newFilterRuleDialog; - const save = useCallback( - filterRule => { - const newFilterRules = updateObjectInList(filterRules, filterRule); - onChange(newFilterRules); - closeFilterRuleDialog(); - }, - [filterRules, onChange, closeFilterRuleDialog] - ); - - return ( - - - rows={filterRules} - columns={columns} - actions={actions} - filterComponents={extraComponents} - selection={selection} - onChange={updateTable} - paginationOptions={paginationOptions} - /> - - {newFilterRuleDialog.value && ( - - )} - - ); -}; - -const paginationOptions: PaginationOptions = { - pageSizeOptions: [10], - pageSizeInitialValue: 10, -}; - -export default React.memo(FilterRulesTable); diff --git a/src/presentation/react/components/filter-rules-table/Section.tsx b/src/presentation/react/components/filter-rules-table/Section.tsx deleted file mode 100644 index 97b8fd20d..000000000 --- a/src/presentation/react/components/filter-rules-table/Section.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from "react"; -import { makeStyles } from "@material-ui/core/styles"; -import Card from "@material-ui/core/Card"; -import CardContent from "@material-ui/core/CardContent"; -import Typography from "@material-ui/core/Typography"; - -const useStyles = makeStyles({ - root: { - minWidth: 275, - marginBottom: 15, - }, - bullet: { - display: "inline-block", - margin: "0 2px", - transform: "scale(0.8)", - }, - title: { - fontSize: 14, - fontWeight: "bold", - }, -}); - -export const Section: React.FC<{ title: string }> = props => { - const { title, children } = props; - const classes = useStyles(); - - return ( - - - - {title} - - - {children} - - - ); -}; diff --git a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx b/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx deleted file mode 100644 index 909314749..000000000 --- a/src/presentation/react/components/instance-selection-dropdown/InstanceSelectionDropdown.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import _ from "lodash"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import { Maybe } from "../../../../types/utils"; -import Dropdown, { DropdownViewOption } from "../dropdown/Dropdown"; -import { useAppContext } from "../../contexts/AppContext"; -import { Store } from "../../../../domain/stores/entities/Store"; - -export type InstanceSelectionOption = "local" | "remote" | "store"; - -export type InstanceSelectionConfig = Partial>; - -export interface InstanceSelectionDropdownProps { - showInstances: InstanceSelectionConfig; - selectedInstance: Maybe; - onChangeSelected: ( - type: T, - instance?: T extends "remote" ? Instance : T extends "store" ? Store : never - ) => void; - view?: DropdownViewOption; - title?: string; - refreshKey?: number; -} - -export const InstanceSelectionDropdown: React.FC = React.memo( - ({ - showInstances, - selectedInstance, - onChangeSelected, - view = "filter", - title = i18n.t("Instances"), - refreshKey, - }) => { - const { compositionRoot } = useAppContext(); - - const [instances, setInstances] = useState([]); - const [stores, setStores] = useState([]); - - const updateSelectedInstance = useCallback( - (id: string) => { - if (id === "LOCAL") { - onChangeSelected("local"); - } else { - const store = stores.find(store => store.id === id); - const instance = instances.find(instance => instance.id === id); - - onChangeSelected(instance ? "remote" : "store", instance ?? store); - } - }, - [instances, stores, onChangeSelected] - ); - - const instanceItems = useMemo(() => { - const localInstance = { id: "LOCAL", name: i18n.t("This instance") }; - const storeInstances = stores.map(store => ({ - id: store.id, - name: `${store.account} - ${store.repository} (${i18n.t("Store")})`, - })); - - return _.compact([ - showInstances.local && localInstance, - ...(showInstances.store ? storeInstances : []), - ...(showInstances.remote ? instances.filter(item => item.type === "dhis") : []), - ]); - }, [showInstances, instances, stores]); - - useEffect(() => { - compositionRoot.instances.list().then(setInstances); - - if (showInstances.store) { - compositionRoot.store.list().then(setStores); - } - }, [compositionRoot, showInstances, refreshKey]); - - useEffect(() => { - // Auto-select first instance - const firstInstanceItem = instanceItems[0]; - if (_.isNil(selectedInstance) && firstInstanceItem) { - updateSelectedInstance(firstInstanceItem.id); - } - }, [instanceItems, selectedInstance, updateSelectedInstance]); - - return ( - - ); - } -); diff --git a/src/presentation/react/components/landing/Landing.tsx b/src/presentation/react/components/landing/Landing.tsx deleted file mode 100644 index ec60d752b..000000000 --- a/src/presentation/react/components/landing/Landing.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import React from "react"; -import _ from "lodash"; -import PageHeader from "../page-header/PageHeader"; -import MenuCard, { MenuCardProps } from "./MenuCard"; - -const useStyles = makeStyles({ - container: { - marginLeft: 30, - }, - title: { - fontSize: 24, - fontWeight: 300, - color: "rgba(0, 0, 0, 0.87)", - padding: "15px 0px 15px", - margin: 0, - }, - clear: { - clear: "both", - }, -}); - -export interface Card { - title?: string; - key: string; - isVisible?: boolean; - children: MenuCardProps[]; -} - -export interface LandingProps { - cards: Card[]; - title?: string; - onBackClick?: () => void; -} - -export const Landing: React.FC = ({ title, cards, onBackClick }) => { - const classes = useStyles(); - - return ( - - {!!title && } - -
    - {cards.map( - ({ key, title, isVisible = true, children }) => - isVisible && - isAnyChildVisible(children) && ( -
    - {!!title &&

    {title}

    } - - {children.map(props => ( - - ))} - -
    -
    - ) - )} -
    - - ); -}; - -function isAnyChildVisible(children: MenuCardProps[]): boolean { - return _.some(children, ({ isVisible = true }) => isVisible); -} diff --git a/src/presentation/react/components/landing/MenuCard.tsx b/src/presentation/react/components/landing/MenuCard.tsx deleted file mode 100644 index ecce7a205..000000000 --- a/src/presentation/react/components/landing/MenuCard.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { makeStyles, Tooltip } from "@material-ui/core"; -import Card from "@material-ui/core/Card"; -import CardActions from "@material-ui/core/CardActions"; -import CardContent from "@material-ui/core/CardContent"; -import CardHeader from "@material-ui/core/CardHeader"; -import IconButton from "@material-ui/core/IconButton"; -import AddIcon from "@material-ui/icons/Add"; -import ViewListIcon from "@material-ui/icons/ViewList"; -import _ from "lodash"; -import React, { ReactNode } from "react"; -import i18n from "../../../../locales"; - -export interface MenuCardProps { - name: string; - description?: string; - icon?: ReactNode; - isVisible?: boolean; - addAction?: () => void; - listAction?: () => void; -} - -export interface MenuCardTitleProps { - text: string; - icon?: ReactNode; -} - -const useStyles = makeStyles({ - card: { - padding: "0", - margin: ".5rem", - float: "left", - width: "230px", - }, - content: { - height: "120px", - padding: ".5rem 1rem", - fontSize: "14px", - }, - actions: { - marginLeft: "auto", - }, - header: { - padding: "1rem", - height: "auto", - borderBottom: "1px solid #ddd", - cursor: "pointer", - }, - headerText: { - fontSize: "15px", - fontWeight: 500, - }, - cardTitle: { - display: "flex", - }, - icon: { - marginLeft: "auto", - display: "inline", - }, -}); - -const MenuCard: React.FC = ({ - name, - icon, - description, - isVisible, - addAction, - listAction, -}) => { - const classes = useStyles(); - - if (isVisible === false) return null; - - return ( - - } - /> - - {description} - - -
    - {addAction && ( - - - - - - )} - - {listAction && ( - - - - - - )} -
    -
    -
    - ); -}; - -const MenuCardTitle: React.FC = ({ text, icon }) => { - const classes = useStyles(); - - return ( - - {text} - {!!icon &&
    {icon}
    } -
    - ); -}; - -export default MenuCard; diff --git a/src/presentation/react/components/mapping-dialog/MappingDialog.tsx b/src/presentation/react/components/mapping-dialog/MappingDialog.tsx deleted file mode 100644 index 6e7e92314..000000000 --- a/src/presentation/react/components/mapping-dialog/MappingDialog.tsx +++ /dev/null @@ -1,181 +0,0 @@ -import { Typography } from "@material-ui/core"; -import DialogContent from "@material-ui/core/DialogContent"; -import { makeStyles } from "@material-ui/styles"; -import { ConfirmationDialog, OrgUnitsSelector } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import { DataSource, isDhisInstance } from "../../../../domain/instance/entities/DataSource"; -import { MetadataMappingDictionary } from "../../../../domain/mapping/entities/MetadataMapping"; -import i18n from "../../../../locales"; -import { modelFactory } from "../../../../models/dhis/factory"; -import { MetadataType } from "../../../../utils/d2"; -import { useAppContext } from "../../contexts/AppContext"; -import { EXCLUDED_KEY } from "../mapping-table/utils"; -import MetadataTable from "../metadata-table/MetadataTable"; - -export interface MappingDialogConfig { - elements: string[]; - mappingType?: string; - mappingPath?: string[]; - firstElement?: MetadataType; -} - -export interface MappingDialogProps { - config: MappingDialogConfig; - instance: DataSource; - mapping: MetadataMappingDictionary; - onUpdateMapping: (items: string[], id?: string) => void; - onClose: () => void; -} - -const useStyles = makeStyles({ - orgUnitSelect: { - margin: "0 auto", - }, -}); - -const MappingDialog: React.FC = ({ - config, - instance, - mapping, - onUpdateMapping, - onClose, -}) => { - const { api: defaultApi, compositionRoot } = useAppContext(); - const classes = useStyles(); - const [connectionSuccess, setConnectionSuccess] = useState(true); - const [filterRows, setFilterRows] = useState(); - const { elements, mappingType, mappingPath, firstElement } = config; - if (!mappingType) { - throw new Error("Attempting to open mapping dialog without a valid mapping type"); - } - - const mappedId = - elements.length === 1 - ? _.last( - _(mapping) - .get([mappingType, elements[0] ?? "", "mappedId"]) - ?.split("-") - ) - : undefined; - const defaultSelection = mappedId !== "DISABLED" ? mappedId : undefined; - const [selected, updateSelected] = useState(defaultSelection); - - const model = modelFactory(mappingType); - const modelName = model.getModelName(); - const api = isDhisInstance(instance) ? compositionRoot.instances.getApi(instance) : defaultApi; - - useEffect(() => { - let mounted = true; - - if (isDhisInstance(instance)) { - compositionRoot.instances.validate(instance).then(result => { - if (result.isError()) console.error(result.value.error); - if (mounted) setConnectionSuccess(result.isSuccess()); - }); - } - - return () => { - mounted = false; - }; - }, [instance, compositionRoot]); - - useEffect(() => { - if (mappingPath) { - const parentMappedId = mappingPath[2]; - compositionRoot.mapping.getValidIds(instance, parentMappedId).then(setFilterRows); - } else if (mappingType === "programDataElements" && elements.length === 1) { - compositionRoot.mapping.getValidIds(instance, elements[0]).then(validIds => { - setFilterRows(buildDataElementFilterForProgram(validIds, elements[0], mapping)); - }); - } - }, [compositionRoot, instance, api, mappingPath, elements, mapping, mappingType]); - - const onUpdateSelection = (selectedIds: string[]) => { - const newSelection = _.last(selectedIds); - onUpdateMapping(elements, newSelection); - updateSelected(newSelection); - }; - - const OrgUnitMapper = ( -
    - -
    - ); - - const MetadataMapper = ( - - ); - - const MapperComponent = - model.getCollectionName() === "organisationUnits" ? OrgUnitMapper : MetadataMapper; - const title = - elements.length > 1 || !firstElement - ? i18n.t( - "Select {{type}} from destination instance {{instance}} to map {{total}} elements", - { - type: modelName, - instance: instance.name, - total: elements.length, - } - ) - : i18n.t( - "Select {{type}} from destination instance {{instance}} to map {{name}} ({{id}})", - { - type: modelName, - instance: instance.name, - name: firstElement.name, - id: firstElement.id, - } - ); - - return ( - 0} - title={title} - onCancel={onClose} - maxWidth={"lg"} - fullWidth={true} - cancelText={i18n.t("Close")} - > - - {connectionSuccess ? ( - MapperComponent - ) : ( - {i18n.t("Could not connect with remote instance")} - )} - - - ); -}; - -const buildDataElementFilterForProgram = ( - validIds: string[], - nestedId: string, - mapping: MetadataMappingDictionary -): string[] | undefined => { - const originProgramId = nestedId.split("-")[0]; - const { mappedId } = _.get(mapping, ["eventPrograms", originProgramId]) ?? {}; - - if (!mappedId || mappedId === EXCLUDED_KEY) return undefined; - return [...validIds, mappedId]; -}; - -export default MappingDialog; diff --git a/src/presentation/react/components/mapping-table/MappingTable.tsx b/src/presentation/react/components/mapping-table/MappingTable.tsx deleted file mode 100644 index 5ecd8c6f9..000000000 --- a/src/presentation/react/components/mapping-table/MappingTable.tsx +++ /dev/null @@ -1,863 +0,0 @@ -import { Icon, IconButton, makeStyles, Tooltip, Typography } from "@material-ui/core"; -import { - ConfirmationDialog, - RowConfig, - TableAction, - TableColumn, - TableGlobalAction, - useLoading, - useSnackbar, -} from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useMemo, useState } from "react"; -import { DataSource } from "../../../../domain/instance/entities/DataSource"; -import { MappingConfig } from "../../../../domain/mapping/entities/MappingConfig"; -import { - MetadataMapping, - MetadataMappingDictionary, -} from "../../../../domain/mapping/entities/MetadataMapping"; -import { cleanOrgUnitPath } from "../../../../domain/synchronization/utils"; -import i18n from "../../../../locales"; -import { D2Model } from "../../../../models/dhis/default"; -import { ProgramDataElementModel } from "../../../../models/dhis/mapping"; -import { DataElementModel, OrganisationUnitModel } from "../../../../models/dhis/metadata"; -import { MetadataType } from "../../../../utils/d2"; -import { useAppContext } from "../../contexts/AppContext"; -import MappingDialog, { MappingDialogConfig } from "../mapping-dialog/MappingDialog"; -import MappingWizard, { MappingWizardConfig, prepareSteps } from "../mapping-wizard/MappingWizard"; -import MetadataTable, { MetadataTableProps } from "../metadata-table/MetadataTable"; -import { cleanNestedMappedId, EXCLUDED_KEY, getChildrenRows } from "./utils"; - -const useStyles = makeStyles({ - iconButton: { - padding: 0, - paddingLeft: 8, - paddingRight: 8, - }, - instanceDropdown: { - order: 0, - }, - actionButtons: { - order: 10, - marginRight: 10, - }, -}); - -interface WarningDialog { - title?: string; - description?: string; - action?: () => void; -} - -export interface MappingTableProps extends MetadataTableProps { - originInstance?: DataSource; - destinationInstance: DataSource; - models: typeof D2Model[]; - filterRows?: string[]; - transformRows?: (rows: MetadataType[]) => MetadataType[]; - mapping: MetadataMappingDictionary; - globalMapping: MetadataMappingDictionary; - onChangeMapping(mapping: MetadataMappingDictionary): Promise; - onApplyGlobalMapping(type: string, id: string, mapping: MetadataMapping): Promise; - isChildrenMapping?: boolean; - mappingPath?: string[]; -} - -export default function MappingTable({ - originInstance, - destinationInstance, - models, - filterRows, - transformRows, - mapping, - globalMapping, - onChangeMapping, - onApplyGlobalMapping, - isChildrenMapping = false, - mappingPath, - ...rest -}: MappingTableProps) { - const { compositionRoot } = useAppContext(); - const classes = useStyles(); - const snackbar = useSnackbar(); - const loading = useLoading(); - - const [model, setModel] = useState(() => models[0] ?? DataElementModel); - - const [rows, setRows] = useState([]); - const [selectedIds, setSelectedIds] = useState([]); - - const [warningDialog, setWarningDialog] = useState(null); - const [mappingConfig, setMappingConfig] = useState(null); - const [wizardConfig, setWizardConfig] = useState(null); - - const getMappedItem = useCallback( - (row?: MetadataType): MetadataMapping => { - const mappingType = row?.model.getMappingType(); - if (!row || !mappingType) return {}; - - const id = cleanNestedMappedId(row.id); - const localItemMapping = _.get(mapping, [mappingType, row.id]); - const globalItemMapping = _.get(globalMapping, [mappingType, id]); - - const isMapped = !!localItemMapping?.mappedId; - const isDifferent = localItemMapping?.mappedId !== globalItemMapping?.mappedId; - const itemMapping = isMapped && isDifferent ? localItemMapping : globalItemMapping; - - return itemMapping ?? {}; - }, - [mapping, globalMapping] - ); - - const applyMapping = useCallback( - async (config: MappingConfig[]) => { - try { - const newMapping = await compositionRoot.mapping.apply( - originInstance ?? compositionRoot.localInstance, - destinationInstance, - mapping, - config, - isChildrenMapping - ); - - await onChangeMapping(newMapping); - setSelectedIds([]); - } catch (e) { - console.error(e); - snackbar.error(i18n.t("Could not apply mapping, please try again.")); - } - loading.reset(); - }, - [ - compositionRoot, - originInstance, - destinationInstance, - snackbar, - loading, - mapping, - isChildrenMapping, - onChangeMapping, - ] - ); - - const makeMappingGlobal = useCallback( - async (selection: string[]) => { - const id = selection[0]; - const firstElement = _.find(rows, ["id", id]); - const mappingType = firstElement?.model.getMappingType(); - const elementMapping = mappingType ? _.get(mapping, [mappingType, id]) : {}; - - if (!firstElement || !mappingType || !elementMapping?.mappedId) { - snackbar.error(i18n.t("You need to map the item before applying a global mapping")); - } else { - await applyMapping([ - { selection, mappingType, global: false, mappedId: undefined }, - ]); - await onApplyGlobalMapping(mappingType, cleanNestedMappedId(id), elementMapping); - snackbar.success(i18n.t("Successfully applied global mapping")); - } - }, - [onApplyGlobalMapping, applyMapping, rows, mapping, snackbar] - ); - - const updateMapping = useCallback( - async (selection: string[], mappedId?: string) => { - const id = selection[0]; - const firstElement = _.find(rows, ["id", id]); - const mappingType = firstElement?.model.getMappingType(); - const global = firstElement?.model.getIsGlobalMapping(); - if (!mappingType) { - snackbar.error(i18n.t("Unable to update mapping")); - } else { - applyMapping([{ selection, mappingType, global, mappedId }]); - } - }, - [applyMapping, rows, snackbar] - ); - - const disableMapping = useCallback( - async (selection: string[]) => { - const id = selection[0]; - const firstElement = _.find(rows, ["id", id]); - const mappingType = firstElement?.model.getMappingType(); - const global = firstElement?.model.getIsGlobalMapping(); - if (selection.length > 0 && mappingType) { - setWarningDialog({ - title: i18n.t("Exclude mapping"), - description: i18n.t( - "Are you sure you want to exclude mapping for {{total}} elements?", - { - total: selection.length, - } - ), - action: () => { - applyMapping([{ selection, mappingType, global, mappedId: EXCLUDED_KEY }]); - }, - }); - } else { - snackbar.error(i18n.t("Please select at least one item to exclude mapping")); - } - }, - [snackbar, applyMapping, rows] - ); - - const resetMapping = useCallback( - async (selection: string[]) => { - const id = selection[0]; - const firstElement = _.find(rows, ["id", id]); - const mappingType = firstElement?.model.getMappingType(); - const global = firstElement?.model.getIsGlobalMapping(); - if (selection.length > 0 && mappingType) { - setWarningDialog({ - title: i18n.t("Reset mapping"), - description: i18n.t( - "Are you sure you want to reset mapping for {{total}} elements?", - { - total: selection.length, - } - ), - action: () => { - applyMapping([{ selection, mappingType, global, mappedId: undefined }]); - }, - }); - } else { - snackbar.error(i18n.t("Please select at least one item to reset mapping")); - } - }, - [snackbar, applyMapping, rows] - ); - - const applyAutoMapping = useCallback( - async (elements: string[]) => { - const types = _(rows) - .filter(({ id }) => elements.includes(id)) - .map(row => row.model.getMappingType()) - .uniq() - .compact() - .value(); - - const id = elements[0]; - const firstElement = _.find(rows, ["id", id]); - const global = firstElement?.model.getIsGlobalMapping(); - - if (types.length === 0) { - snackbar.error(i18n.t("You need to select at least one valid item")); - } else if (types.length > 1) { - snackbar.error(i18n.t("You need to select all items from the same type")); - } - - try { - loading.show( - true, - i18n.t("Preparing auto-mapping for {{total}} elements", { - total: elements.length, - }) - ); - - const { tasks, errors } = await compositionRoot.mapping.autoMap( - originInstance ?? compositionRoot.localInstance, - destinationInstance, - mapping, - types[0], - elements, - global - ); - - await applyMapping(tasks); - - if (errors.length > 0) { - snackbar.error( - errors - .map(id => - i18n.t( - "Could not find a suitable candidate to apply auto-mapping for {{id}}", - { id } - ) - ) - .join("\n") - ); - } else if (elements.length === 1) { - const firstElement = _.find(rows, ["id", elements[0]]); - const mappingType = firstElement?.model.getMappingType(); - if (firstElement && mappingType) { - setMappingConfig({ - elements, - mappingPath, - mappingType, - firstElement, - }); - } - } - } catch (e) { - console.error(e); - snackbar.error(i18n.t("Could not connect with remote instance")); - } - loading.reset(); - }, - [ - compositionRoot, - destinationInstance, - originInstance, - loading, - applyMapping, - rows, - snackbar, - mappingPath, - mapping, - ] - ); - - const openMappingDialog = useCallback( - (elements: string[]) => { - const firstElement = _.find(rows, ["id", elements[0]]); - const types = _(rows) - .filter(({ id }) => elements.includes(id)) - .map(row => row.model.getMappingType()) - .uniq() - .value(); - - if (types.length === 1) { - setMappingConfig({ elements, mappingPath, mappingType: types[0], firstElement }); - setSelectedIds([]); - } else if (types.length > 1) { - snackbar.error(i18n.t("You need to select all items from the same type")); - } else { - snackbar.error(i18n.t("You need to select at least one valid item")); - } - }, - [mappingPath, rows, snackbar] - ); - - const createValidations = useCallback( - async (dict: MetadataMappingDictionary) => { - const result = _.cloneDeep(dict); - - for (const type of _.keys(dict)) { - for (const id of _.keys(dict[type])) { - const { mappedId, mapping = {}, ...rest } = dict[type][id]; - const innerMapping = await createValidations(mapping); - - const { - mappedName, - mappedCode, - mappedLevel, - } = await compositionRoot.mapping.buildMapping({ - originInstance: originInstance ?? compositionRoot.localInstance, - destinationInstance, - originalId: id, - mappedId, - }); - - result[type][id] = _.omitBy( - { - ...rest, - mappedId, - mappedName, - mappedCode, - mappedLevel, - mapping: innerMapping, - }, - _.isUndefined - ); - } - } - - return result; - }, - [compositionRoot, destinationInstance, originInstance] - ); - - const applyValidateMapping = useCallback( - async (selection: string[]) => { - loading.show( - true, - i18n.t("Validating mapping for {{total}} elements", { total: selection.length }) - ); - - const tasks = []; - const selectedRows = _.compact(selection.map(id => _.find(rows, ["id", id]))); - const allRows = [...selectedRows, ...getChildrenRows(selectedRows, model)]; - - for (const row of allRows) { - const mappingType = row.model.getMappingType(); - const global = row.model.getIsGlobalMapping(); - if (mappingType) { - const newMapping = await createValidations({ - [mappingType]: { - [row.id]: getMappedItem(row), - }, - }); - const { mappedId, ...overrides } = newMapping[mappingType][row.id]; - tasks.push({ selection: [row.id], mappingType, global, mappedId, overrides }); - } - } - - applyMapping(tasks); - loading.reset(); - }, - [applyMapping, getMappedItem, loading, rows, createValidations, model] - ); - - const validateMapping = useCallback( - async (selection: string[]) => { - if (selection.length > 0) { - setWarningDialog({ - title: i18n.t("Validate mapping"), - description: i18n.t( - "Are you sure you want to validate mapping for {{total}} elements?", - { - total: selection.length, - } - ), - action: () => applyValidateMapping(selection), - }); - } else { - snackbar.error(i18n.t("Please select at least one item to validate mapping")); - } - }, - [snackbar, applyValidateMapping] - ); - - const openRelatedMapping = useCallback( - (selection: string[]) => { - const id = _.first(selection); - const element = _.find(rows, ["id", id]); - if (!id || !element) return; - - const mappingType = element.model.getMappingType(); - const { mapping: rowMapping = undefined } = mappingType - ? _.get(mapping, [mappingType, id]) - : {}; - - if (!rowMapping || !mappingType) { - snackbar.error( - i18n.t( - "You need to map this element before accessing its related metadata mapping" - ) - ); - } else { - setWizardConfig({ mappingPath: [mappingType, id], type: mappingType, element }); - } - }, - [mapping, rows, snackbar] - ); - - const updateSelection = (selection: string[]) => { - setSelectedIds(prevSelection => { - const removedRows = _(prevSelection) - .difference(selection) - .map(id => _.find(rows, ["id", id])) - .compact() - .value(); - const childrenRemovals = getChildrenRows(removedRows, model).map(({ id }) => id); - - return _.difference(selection, childrenRemovals); - }); - }; - - const rowConfig = useCallback( - (row: MetadataType): RowConfig => { - const mappingType = row.model.getMappingType(); - - if (!mappingType) { - return { selectable: false }; - } else if (mappingType === ProgramDataElementModel.getMappingType()) { - const parentId = _.first(row.id.split("-")) ?? row.id; - const parentMapping = _.get(mapping, ["eventPrograms", parentId, "mappedId"]); - const isParentMapped = !!parentMapping && parentMapping !== EXCLUDED_KEY; - const { mappedId } = getMappedItem(row); - - const hasErrors = isParentMapped && !mappedId; - return { - style: hasErrors ? { backgroundColor: "#ffcdd2" } : undefined, - }; - } else { - return {}; - } - }, - [getMappedItem, mapping] - ); - - const columns: TableColumn[] = useMemo( - () => - _.compact([ - { name: "lastUpdated", text: i18n.t("Last updated"), sortable: true, hidden: true }, - { - name: "id", - text: i18n.t("ID"), - getValue: (row: MetadataType) => { - return cleanNestedMappedId(row.id); - }, - }, - { - name: "metadata-type", - text: i18n.t("Metadata type"), - hidden: model.getChildrenKeys() === undefined, - getValue: (row: MetadataType) => { - return row.model.getModelName(); - }, - }, - { - name: "mapped-id", - text: i18n.t("Mapped ID"), - sortable: false, - getValue: (row: MetadataType) => { - const { mappedId } = getMappedItem(row); - const mappingType = row.model.getMappingType(); - const text = - !!mappedId && mappedId !== EXCLUDED_KEY - ? cleanOrgUnitPath(mappedId) - : "-"; - - return ( - - - {text} - - {!!mappingType && ( - - { - event.stopPropagation(); - openMappingDialog([row.id]); - }} - > - open_in_new - - - )} - - ); - }, - }, - { - name: "mapped-name", - text: i18n.t("Mapped Name"), - sortable: false, - getValue: (row: MetadataType) => { - const { - mappedName, - conflicts = false, - mapping: childrenMapping, - } = getMappedItem(row); - - const childrenConflicts = _(childrenMapping) - .values() - .map(Object.values) - .flatten() - .some(["conflicts", true]); - const showConflicts = conflicts || childrenConflicts; - - return ( - - - {mappedName ?? "-"} - - {showConflicts && ( - - { - event.stopPropagation(); - if (!isChildrenMapping) - openRelatedMapping([row.id]); - else openMappingDialog([row.id]); - }} - > - warning - - - )} - - ); - }, - }, - model === OrganisationUnitModel - ? { - name: "mapped-level", - text: i18n.t("Mapped Level"), - sortable: false, - getValue: (row: MetadataType) => { - const { mappedLevel } = getMappedItem(row); - - return ( - - - {mappedLevel ?? "-"} - - - ); - }, - } - : undefined, - { - name: "mapping-status", - text: i18n.t("Mapping Status"), - sortable: false, - getValue: (row: MetadataType) => { - const { mappedId, global = false } = getMappedItem(row); - - const notMappedStatus = !mappedId ? i18n.t("Not mapped") : undefined; - const disabledStatus = - mappedId === EXCLUDED_KEY ? i18n.t("Excluded") : undefined; - const globalStatus = global ? i18n.t("Mapped (Global)") : i18n.t("Mapped"); - - return ( - - - {notMappedStatus ?? disabledStatus ?? globalStatus} - - - ); - }, - }, - ]), - [classes, model, openMappingDialog, isChildrenMapping, openRelatedMapping, getMappedItem] - ); - - const addToSelection = useCallback( - (selection: string[]) => { - const ids = _(selection) - .map(id => _.find(rows, ["id", id])) - .compact() - .filter(row => !!row.model.getMappingType()) - .map(({ id }) => id) - .value(); - - setSelectedIds(prevSelection => { - const oldSelection = _.difference(prevSelection, ids); - const newSelection = _.difference(ids, prevSelection); - return _.uniq([...oldSelection, ...newSelection]); - }); - }, - [rows] - ); - - const actions: TableAction[] = useMemo( - () => [ - { - name: "select", - text: "Select", - onClick: addToSelection, - isActive: () => false, - primary: true, - }, - { - name: "set-mapping", - text: i18n.t("Set mapping"), - multiple: true, - onClick: openMappingDialog, - icon: open_in_new, - isActive: (selected: MetadataType[]) => { - return _.every(selected, row => row.model.getMappingType()); - }, - }, - { - name: "select-children-rows", - text: i18n.t("Select children"), - multiple: true, - onClick: (selection: string[]) => { - const selectedRows = _.compact(selection.map(id => _.find(rows, ["id", id]))); - const children = getChildrenRows(selectedRows, model).map(({ id }) => id); - setSelectedIds(prevSelection => _.uniq([...prevSelection, ...children])); - }, - icon: done_all, - isActive: (selection: MetadataType[]) => { - const children = getChildrenRows(selection, model); - return children.length > 0; - }, - }, - { - name: "global-mapping", - text: i18n.t("Make this mapping global"), - multiple: false, - onClick: makeMappingGlobal, - icon: add_circle_outline, - isActive: (selected: MetadataType[]) => { - const isRowMappedAndNotGlobal = _(selected) - .map(getMappedItem) - .every(({ mappedId, global }) => !!mappedId && !global); - const isRowCompatible = - isChildrenMapping || - _.every(selected, row => row.model.getIsGlobalMapping()); - - return isRowMappedAndNotGlobal && isRowCompatible; - }, - }, - { - name: "validate-mapping", - text: i18n.t("Validate mapping"), - multiple: true, - onClick: validateMapping, - icon: find_replace, - isActive: (selected: MetadataType[]) => { - const isGlobalMapping = _.some(selected, row => row.model.getIsGlobalMapping()); - return _(selected) - .map(getMappedItem) - .some(({ mappedId, global }) => !!mappedId && (isGlobalMapping || !global)); - }, - }, - { - name: "auto-mapping", - text: i18n.t("Auto-map element"), - multiple: true, - onClick: applyAutoMapping, - icon: compare_arrows, - isActive: (selected: MetadataType[]) => { - return _.every(selected, row => row.model.getMappingType()); - }, - }, - { - name: "disable-mapping", - text: i18n.t("Exclude mapping"), - multiple: true, - onClick: disableMapping, - icon: sync_disabled, - isActive: (selected: MetadataType[]) => { - return _.every(selected, row => row.model.getMappingType()); - }, - }, - { - name: "reset-mapping", - text: i18n.t("Reset mapping to default values"), - multiple: true, - onClick: resetMapping, - icon: clear, - isActive: (selected: MetadataType[]) => { - return _.every(selected, row => row.model.getMappingType()); - }, - }, - { - name: "related-mapping", - text: i18n.t("Related metadata mapping"), - multiple: false, - onClick: openRelatedMapping, - icon: assignment, - isActive: (selected: MetadataType[]) => { - const element = selected[0]; - const mappingType = element.model.getMappingType(); - const steps = prepareSteps(mappingType, element); - const { mappedId } = getMappedItem(element); - - return !!mappedId && !isChildrenMapping && steps.length > 0; - }, - }, - ], - [ - addToSelection, - disableMapping, - openMappingDialog, - resetMapping, - applyAutoMapping, - makeMappingGlobal, - validateMapping, - openRelatedMapping, - getMappedItem, - isChildrenMapping, - model, - rows, - ] - ); - - const globalActions: TableGlobalAction[] = _.compact([ - model !== OrganisationUnitModel - ? { - name: "validate-mapping", - text: i18n.t("Validate mapping"), - onClick: validateMapping, - icon: find_replace, - } - : undefined, - model !== OrganisationUnitModel - ? { - name: "reset-mapping", - text: i18n.t("Reset mapping"), - onClick: resetMapping, - icon: clear, - } - : undefined, - model !== OrganisationUnitModel - ? { - name: "disable-mapping", - text: i18n.t("Exclude mapping"), - onClick: disableMapping, - icon: sync_disabled, - } - : undefined, - ]); - - const notifyNewModel = useCallback(model => { - setRows([]); - setSelectedIds([]); - setModel(() => model); - }, []); - - const updateRows = useCallback( - (rows: MetadataType[]) => { - setRows([...rows, ...getChildrenRows(rows, model)]); - }, - [model] - ); - - const closeWarningDialog = () => setWarningDialog(null); - const closeMappingDialog = () => setMappingConfig(null); - const closeWizard = () => setWizardConfig(null); - - return ( - - {!!warningDialog && ( - { - if (warningDialog.action) warningDialog.action(); - setWarningDialog(null); - }} - onCancel={closeWarningDialog} - /> - )} - - {!!mappingConfig && ( - - )} - - {!!wizardConfig && ( - - )} - - - - ); -} diff --git a/src/presentation/react/components/mapping-table/utils.tsx b/src/presentation/react/components/mapping-table/utils.tsx deleted file mode 100644 index 37c9b8600..000000000 --- a/src/presentation/react/components/mapping-table/utils.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import _ from "lodash"; -import { D2Model } from "../../../../models/dhis/default"; -import { MetadataType } from "../../../../utils/d2"; - -export const EXCLUDED_KEY = "DISABLED"; - -export const cleanNestedMappedId = (id: string): string => { - return _(id).split("-").last() ?? ""; -}; - -export const getChildrenRows = (rows: MetadataType[], model: typeof D2Model): MetadataType[] => { - const childrenKeys = model.getChildrenKeys() ?? []; - - return _.flattenDeep( - rows.map(row => Object.values(_.pick(row, childrenKeys)) as MetadataType[]) - ); -}; diff --git a/src/presentation/react/components/mapping-wizard/MappingWizard.tsx b/src/presentation/react/components/mapping-wizard/MappingWizard.tsx deleted file mode 100644 index 973bb19df..000000000 --- a/src/presentation/react/components/mapping-wizard/MappingWizard.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { DialogContent } from "@material-ui/core"; -import { ConfirmationDialog, Wizard, WizardStep } from "d2-ui-components"; -import _ from "lodash"; -import React, { useState } from "react"; -import { DataSource } from "../../../../domain/instance/entities/DataSource"; -import { - MetadataMapping, - MetadataMappingDictionary, -} from "../../../../domain/mapping/entities/MetadataMapping"; -import i18n from "../../../../locales"; -import { MetadataType } from "../../../../utils/d2"; -import { MappingTableProps } from "../mapping-table/MappingTable"; -import { cleanNestedMappedId } from "../mapping-table/utils"; -import { buildModelSteps } from "./Steps"; - -export interface MappingWizardStep extends WizardStep { - showOnSyncDialog?: boolean; - props: MappingTableProps; -} - -export interface MappingWizardConfig { - mappingPath: string[]; - type: string; - element: MetadataType; -} - -export interface MappingWizardProps { - originInstance?: DataSource; - destinationInstance: DataSource; - mapping: MetadataMappingDictionary; - config: MappingWizardConfig; - onUpdateMapping: (mapping: MetadataMappingDictionary) => Promise; - onApplyGlobalMapping(type: string, id: string, mapping: MetadataMapping): Promise; - onCancel?(): void; -} - -export const prepareSteps = (type: string | undefined, element: MetadataType) => { - if (!type) return []; - return buildModelSteps(type).filter(({ isVisible = _.noop }) => isVisible(type, element)); -}; - -const MappingWizard: React.FC = ({ - originInstance, - destinationInstance, - mapping: instanceMapping, - config, - onUpdateMapping, - onApplyGlobalMapping, - onCancel = _.noop, -}) => { - const { mappingPath, type, element } = config; - - const { mappedId = "", mapping = {} }: MetadataMapping = _.get( - instanceMapping, - mappingPath, - {} - ); - - const mappingKeys = _(mapping).mapValues(Object.keys).values().flatten().value(); - - const filterRows = mappingKeys.map(cleanNestedMappedId); - - const transformRows = (rows: MetadataType[]) => { - return rows.filter(({ id }) => mappingKeys.includes(id)); - }; - - const onChangeMapping = async (subMapping: MetadataMappingDictionary) => { - const newMapping = _.clone(instanceMapping); - _.set(newMapping, [...mappingPath, "mapping"], subMapping); - await onUpdateMapping(newMapping); - }; - - const steps: MappingWizardStep[] = - prepareSteps(type, element).map(({ models, ...step }) => ({ - ...step, - props: { - models, - globalMapping: instanceMapping, - mapping, - onChangeMapping, - onApplyGlobalMapping, - originInstance, - destinationInstance, - filterRows, - transformRows, - mappingPath: [...mappingPath, mappedId], - isChildrenMapping: true, - }, - })) ?? []; - - const [stepName, updateStepName] = useState(steps[0]?.label); - - const onStepChangeRequest = async (_prev: WizardStep, next: WizardStep) => { - updateStepName(next.label); - return undefined; - }; - - if (steps.length === 0) return null; - - const initialStepKey = steps.map(step => step.key)[0]; - const mainTitle = i18n.t(`Related metadata mapping for {{name}} ({{id}})`, element); - const title = _.compact([mainTitle, stepName]).join(" - "); - - return ( - - - - - - ); -}; - -export default MappingWizard; diff --git a/src/presentation/react/components/mapping-wizard/Steps.tsx b/src/presentation/react/components/mapping-wizard/Steps.tsx deleted file mode 100644 index 5b30dfdfd..000000000 --- a/src/presentation/react/components/mapping-wizard/Steps.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import React from "react"; -import i18n from "../../../../locales"; -import { D2Model } from "../../../../models/dhis/default"; -import { - CategoryOptionMappedModel, - OptionMappedModel, - ProgramStageMappedModel, -} from "../../../../models/dhis/mapping"; -import { Dictionary } from "../../../../types/utils"; -import { MetadataType } from "../../../../utils/d2"; -import MappingTable, { MappingTableProps } from "../mapping-table/MappingTable"; -import { MappingWizardStep } from "./MappingWizard"; - -type MappingWizardStepBuilder = Omit & { - models: typeof D2Model[]; - isVisible?: (type: string, element: MetadataType) => boolean; -}; - -export const buildModelSteps = (type: string): MappingWizardStepBuilder[] => { - const availableSteps: { [key: string]: MappingWizardStepBuilder } = { - categoryOptions: { - key: "category-options", - label: i18n.t("Category Options"), - component: (props: MappingTableProps) => , - models: [CategoryOptionMappedModel], - isVisible: (_type: string, element: MetadataType) => { - return !!element.categoryCombo?.id; - }, - }, - options: { - key: "options", - label: i18n.t("Options"), - component: (props: MappingTableProps) => , - models: [OptionMappedModel], - isVisible: (_type: string, element: MetadataType) => { - return !!element.optionSet?.id; - }, - }, - programStages: { - key: "programStages", - label: i18n.t("Program Stages"), - component: (props: MappingTableProps) => , - models: [ProgramStageMappedModel], - isVisible: (type: string, element: MetadataType) => { - return type === "programs" && element.programType === "WITH_REGISTRATION"; - }, - }, - }; - - const modelSteps: Dictionary = { - aggregatedDataElements: [availableSteps.categoryOptions, availableSteps.options], - programDataElements: [availableSteps.options], - eventPrograms: [availableSteps.categoryOptions, availableSteps.programStages], - }; - - return modelSteps[type] ?? []; -}; diff --git a/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx b/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx deleted file mode 100644 index 7046d9599..000000000 --- a/src/presentation/react/components/metadata-drop-zone/MetadataDropZone.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useSnackbar } from "d2-ui-components"; -import React, { useState } from "react"; -import Dropzone from "react-dropzone"; -import i18n from "../../../../locales"; -import CloudUploadIcon from "@material-ui/icons/CloudUpload"; -import CloudDoneIcon from "@material-ui/icons/CloudDone"; -import { makeStyles } from "@material-ui/core"; -import { MetadataPackage } from "../../../../domain/metadata/entities/MetadataEntities"; - -interface MetadataDropZoneProps { - onChange: (fileName: string, metadataPackage: MetadataPackage) => void; -} - -const MetadataDropZone: React.FC = ({ onChange }) => { - const classes = useStyles(); - const [file, setFile] = useState(); - const snackbar = useSnackbar(); - - const onDrop = async (files: File[]) => { - const file = files[0]; - if (!file) { - snackbar.error(i18n.t("Cannot read file")); - return; - } - - const contentsFile = await file.text(); - const contentsJson = JSON.parse(contentsFile); - delete contentsJson.date; - delete contentsJson.package; - - onChange(file.name, contentsJson as MetadataPackage); - setFile(file); - }; - - return ( - - {({ getRootProps, getInputProps }) => ( -
    -
    - - - -
    -
    - )} -
    - ); -}; - -export default MetadataDropZone; - -const useStyles = makeStyles({ - dropzoneTextStyle: { textAlign: "center", top: "15%", position: "relative" }, - dropzoneParagraph: { fontSize: 20 }, - uploadIconSize: { width: 50, height: 50, color: "#909090" }, - dropzone: { - position: "relative", - width: "100%", - height: 270, - backgroundColor: "#f0f0f0", - border: "dashed", - borderColor: "#c8c8c8", - cursor: "pointer", - }, - stripes: { - width: "100%", - height: 270, - cursor: "pointer", - border: "solid", - borderColor: "#c8c8c8", - "-webkit-animation": "progress 2s linear infinite !important", - "-moz-animation": "progress 2s linear infinite !important", - animation: "progress 2s linear infinite !important", - backgroundSize: "150% 100%", - }, -}); diff --git a/src/presentation/react/components/metadata-table/MetadataTable.tsx b/src/presentation/react/components/metadata-table/MetadataTable.tsx deleted file mode 100644 index 3356149ea..000000000 --- a/src/presentation/react/components/metadata-table/MetadataTable.tsx +++ /dev/null @@ -1,596 +0,0 @@ -import { Checkbox, FormControlLabel, Icon, makeStyles } from "@material-ui/core"; -import DoneAllIcon from "@material-ui/icons/DoneAll"; -import { isCancel } from "d2-api"; -import { - DatePicker, - ObjectsTable, - ObjectsTableDetailField, - ObjectsTableProps, - OrgUnitsSelector, - ReferenceObject, - TableAction, - TableColumn, - TablePagination, - TableSelection, - TableState, - useSnackbar, -} from "d2-ui-components"; -import _ from "lodash"; -import React, { ChangeEvent, ReactNode, useCallback, useEffect, useState } from "react"; -import { NamedRef } from "../../../../domain/common/entities/Ref"; -import { - DataSource, - isDhisInstance, - isJSONDataSource, -} from "../../../../domain/instance/entities/DataSource"; -import { MetadataResponsible } from "../../../../domain/metadata/entities/MetadataResponsible"; -import { ListMetadataParams } from "../../../../domain/metadata/repositories/MetadataRepository"; -import i18n from "../../../../locales"; -import { D2Model } from "../../../../models/dhis/default"; -import { DataElementModel } from "../../../../models/dhis/metadata"; -import { MetadataType } from "../../../../utils/d2"; -import { useAppContext } from "../../contexts/AppContext"; -import Dropdown from "../dropdown/Dropdown"; -import { ResponsibleDialog } from "../responsible-dialog/ResponsibleDialog"; -import { getFilterData, getOrgUnitSubtree } from "./utils"; - -export type MetadataTableFilters = "group" | "level" | "orgUnit" | "lastUpdated" | "onlySelected"; - -export interface MetadataTableProps - extends Omit, "rows" | "columns"> { - remoteInstance?: DataSource; - filterRows?: string[]; - transformRows?: (rows: MetadataType[]) => MetadataType[]; - models: typeof D2Model[]; - selectedIds?: string[]; - excludedIds?: string[]; - childrenKeys?: string[]; - initialShowOnlySelected?: boolean; - additionalColumns?: TableColumn[]; - additionalActions?: TableAction[]; - showIndeterminateSelection?: boolean; - notifyNewSelection?(selectedIds: string[], excludedIds: string[]): void; - notifyNewModel?(model: typeof D2Model): void; - notifyRowsChange?(rows: MetadataType[]): void; - allowChangingResponsible?: boolean; - showResponsible?: boolean; - externalFilterComponents?: ReactNode; - viewFilters?: MetadataTableFilters[]; -} - -const useStyles = makeStyles({ - checkbox: { - paddingLeft: 10, - marginTop: 8, - }, - orgUnitFilter: { - order: -1, - marginRight: "1rem", - }, - metadataFilter: { - order: 1, - }, - dateFilter: { - order: 2, - }, - groupFilter: { - order: 3, - }, - levelFilter: { - order: 4, - }, - onlySelectedFilter: { - order: 5, - }, -}); - -const initialState = { - sorting: { - field: "displayName" as const, - order: "asc" as const, - }, - pagination: { - page: 1, - pageSize: 25, - }, -}; - -const uniqCombine = (items: any[]) => { - return _(items).compact().reverse().uniqBy("name").reverse().value(); -}; - -const MetadataTable: React.FC = ({ - remoteInstance, - filterRows, - transformRows = rows => rows, - models, - selectedIds = [], - excludedIds = [], - notifyNewSelection = _.noop, - notifyNewModel = _.noop, - notifyRowsChange = _.noop, - childrenKeys = [], - additionalColumns = [], - additionalActions = [], - loading: providedLoading, - initialShowOnlySelected = false, - showIndeterminateSelection = false, - allowChangingResponsible = false, - showResponsible = true, - externalFilterComponents, - viewFilters = ["group", "level", "orgUnit", "lastUpdated", "onlySelected"], - ...rest -}) => { - const { compositionRoot, api: defaultApi } = useAppContext(); - const classes = useStyles(); - - const snackbar = useSnackbar(); - - const [model, updateModel] = useState(() => models[0] ?? DataElementModel); - const [ids, updateIds] = useState([]); - const [responsibles, updateResponsibles] = useState([]); - const [sharingSettingsElement, setSharingSettingsElement] = useState(); - - const [selectedRows, setSelectedRows] = useState(selectedIds); - const [filters, setFilters] = useState({ - type: model.getCollectionName(), - showOnlySelected: initialShowOnlySelected, - order: initialState.sorting, - page: initialState.pagination.page, - pageSize: initialState.pagination.pageSize, - }); - - const updateFilters = useCallback( - (partialFilters: Partial) => { - setFilters(state => ({ ...state, page: 1, ...partialFilters })); - }, - [setFilters] - ); - - const api = - remoteInstance && isDhisInstance(remoteInstance) - ? compositionRoot.instances.getApi(remoteInstance) - : defaultApi; - - const [expandOrgUnits, updateExpandOrgUnits] = useState(); - const [groupFilterData, setGroupFilterData] = useState([]); - const [levelFilterData, setLevelFilterData] = useState([]); - - const [rows, setRows] = useState([]); - const [pager, setPager] = useState>({}); - const [loading, setLoading] = useState(true); - - const showResponsibles = - showResponsible && - (model.getCollectionName() === "dataSets" || model.getCollectionName() === "programs"); - - const changeModelFilter = (modelName: string) => { - if (models.length === 0) throw new Error("You need to provide at least one model"); - const model = _.find(models, model => model.getMetadataType() === modelName) ?? models[0]; - updateModel(() => model); - notifyNewModel(model); - updateFilters({ type: model.getCollectionName() }); - }; - - const changeSearchFilter = (value: string) => { - const hasSearch = value.trim() !== ""; - const { field, operator } = model.getSearchFilter(); - updateFilters({ - search: hasSearch ? { field, operator, value } : undefined, - }); - }; - - const changeLastUpdatedFilter = (date: Date | null) => { - updateFilters({ lastUpdated: date ?? undefined }); - }; - - const changeGroupFilter = (value: string) => { - updateFilters({ - group: { type: model.getGroupFilterName(), value }, - }); - }; - - const changeLevelFilter = (level: string) => { - updateFilters({ level, parents: [] }); - }; - - const changeOnlySelectedFilter = (event: ChangeEvent) => { - const showOnlySelected = event.target?.checked; - updateFilters({ - selectedIds: showOnlySelected ? selectedRows : undefined, - showOnlySelected, - }); - }; - - const changeParentOrgUnitFilter = useCallback( - (parents: string[]) => { - updateFilters({ parents, level: "" }); - }, - [updateFilters] - ); - - const selectOrgUnitChildren = async (selectedOUs: string[]) => { - const ids = new Set(); - for (const selectedOU of selectedOUs) { - const subtree = await getOrgUnitSubtree(api, selectedOU); - subtree.forEach(id => ids.add(id)); - } - const includedIds = _.uniq([...selectedIds, ...Array.from(ids)]); - notifyNewSelection(includedIds, excludedIds); - - const orgUnitPaths = _(rows) - .intersectionBy( - selectedOUs.map(id => ({ id })), - "id" - ) - .map(({ path }) => path) - .compact() - .value(); - updateExpandOrgUnits(orgUnitPaths); - changeParentOrgUnitFilter(orgUnitPaths); - }; - - const addToSelection = (ids: string[]) => { - const oldSelection = _.difference(selectedIds, ids); - const newSelection = _.difference(ids, selectedIds); - - notifyNewSelection([...oldSelection, ...newSelection], excludedIds); - }; - - const openResponsibleDialog = (ids: string[]) => { - const { id, name } = rows.find(({ id }) => ids[0] === id) ?? {}; - if (!id || !name) return; - - setSharingSettingsElement({ id, name }); - }; - - const filterComponents = ( - - {externalFilterComponents} - - {models.length > 1 && ( -
    - ({ - id: model.getMetadataType(), - name: model.getModelName(), - }))} - onValueChange={changeModelFilter} - value={model.getMetadataType()} - label={i18n.t("Metadata type")} - hideEmpty={true} - /> -
    - )} - - {viewFilters.includes("lastUpdated") && ( -
    - -
    - )} - - {viewFilters.includes("group") && model.getGroupFilterName() && ( -
    - -
    - )} - - {viewFilters.includes("level") && model.getLevelFilterName() && ( -
    - -
    - )} - - {viewFilters.includes("onlySelected") && ( -
    - - } - label={i18n.t("Only selected items")} - /> -
    - )} -
    - ); - - const orgUnitTreeFilter = viewFilters.includes("orgUnit") && - model.getCollectionName() === "organisationUnits" && ( -
    - -
    - ); - - const handleError = useCallback( - (error: Error) => { - if (!isCancel(error)) { - snackbar.error(error.message); - setRows([]); - setPager({}); - setLoading(false); - } - }, - [snackbar] - ); - - const tableActions = [ - { - name: "details", - text: i18n.t("Details"), - multiple: false, - type: "details", - }, - { - name: "select-children", - text: i18n.t("Select with children subtree"), - multiple: true, - onClick: selectOrgUnitChildren, - icon: , - isActive: () => { - return model.getMetadataType() === "organisationUnit"; - }, - }, - { - name: "select", - text: i18n.t("Select"), - primary: true, - multiple: true, - onClick: addToSelection, - isActive: () => false, - }, - { - name: "set-responsible", - text: i18n.t("Set metadata custodian"), - multiple: false, - icon: supervisor_account, - onClick: openResponsibleDialog, - isActive: () => { - return allowChangingResponsible && !remoteInstance && showResponsibles; - }, - }, - ]; - - useEffect(() => { - updateFilters({ - page: initialState.pagination.page, - }); - }, [updateFilters, remoteInstance]); - - useEffect(() => { - if (model.getCollectionName() === "organisationUnits") return; - if (remoteInstance && isJSONDataSource(remoteInstance)) return; - - compositionRoot.metadata - .listAll({ ...filters, filterRows, fields: { id: true } }, remoteInstance) - .then(objects => { - updateIds(objects.map(({ id }) => id)); - }); - }, [filters, filterRows, model, compositionRoot, remoteInstance]); - - useEffect(() => { - if (model.getCollectionName() !== "organisationUnits") return; - if (remoteInstance && isJSONDataSource(remoteInstance)) { - changeParentOrgUnitFilter([]); - return; - } - - compositionRoot.instances - .getOrgUnitRoots(remoteInstance) - .then(roots => changeParentOrgUnitFilter(roots.map(({ path }) => path))) - .catch(handleError); - }, [compositionRoot, remoteInstance, model, handleError, changeParentOrgUnitFilter]); - - useEffect(() => { - if (model.getCollectionName() === "organisationUnits" && !filters.parents) return; - const fields = model.getFields(); - const includeParents = model.getCollectionName() === "organisationUnits"; - - setLoading(true); - compositionRoot.metadata - .list({ ...filters, filterRows, fields, includeParents }, remoteInstance) - .then(({ objects, pager }) => { - const rows = model.getApiModelTransform()((objects as unknown) as MetadataType[]); - notifyRowsChange(rows); - - setRows(rows); - setPager(pager); - setLoading(false); - }) - .catch(handleError); - }, [ - compositionRoot, - notifyRowsChange, - remoteInstance, - filters, - filterRows, - model, - handleError, - ]); - - useEffect(() => { - if (model && model.getGroupFilterName()) { - getFilterData( - model.getGroupFilterName(), - "group", - api.apiPath, - api - ).then(({ objects }) => setGroupFilterData(objects)); - } - - if (model && model.getLevelFilterName()) { - getFilterData(model.getLevelFilterName(), "level", api.apiPath, api).then( - ({ objects }) => { - setLevelFilterData( - objects.map(({ name, level }) => ({ - id: String(level), - name: `${level}. ${name}`, - })) - ); - } - ); - } - }, [api, model]); - - useEffect(() => { - if (remoteInstance && isJSONDataSource(remoteInstance)) return; - - compositionRoot.responsibles.list(remoteInstance).then(updateResponsibles); - }, [compositionRoot, remoteInstance]); - - const handleTableChange = (tableState: TableState) => { - const { sorting, pagination, selection } = tableState; - - const included = _.reject(selection, { indeterminate: true }).map(({ id }) => id); - const newlySelectedIds = _.difference(included, selectedIds); - const newlyUnselectedIds = _.difference(selectedIds, included); - - const parseChildren = (ids: string[]) => - _(rows) - .filter(({ id }) => !!ids.includes(id)) - .map(row => (_.values(_.pick(row, childrenKeys)) as unknown) as MetadataType) - .flattenDeep() - .map(({ id }) => id) - .value(); - - const excluded = _(excludedIds) - .union(newlyUnselectedIds) - .difference(parseChildren(newlyUnselectedIds)) - .difference(newlySelectedIds) - .difference(parseChildren(newlySelectedIds)) - .filter(id => !_.find(rows, { id })) - .value(); - - notifyNewSelection(included, excluded); - setSelectedRows(included); - updateFilters({ - order: sorting, - page: pagination.page, - pageSize: pagination.pageSize, - }); - }; - - const exclusion = excludedIds.map(id => ({ id })); - const selection = selectedIds.map(id => ({ - id, - checked: true, - indeterminate: false, - })); - - const childrenSelection: TableSelection[] = showIndeterminateSelection - ? _(rows) - .intersectionBy(selection, "id") - .map(row => (_.values(_.pick(row, childrenKeys)) as unknown) as MetadataType[]) - .flattenDeep() - .differenceBy(selection, "id") - .differenceBy(exclusion, "id") - .map(({ id }) => { - return { - id, - checked: true, - indeterminate: !_.find(selection, { id }), - }; - }) - .value() - : []; - - const responsibleField = showResponsibles - ? { - name: "responsible", - text: i18n.t("Custodian"), - getValue: (row: MetadataType) => { - const { users = [], userGroups = [] } = - responsibles.find(({ id }) => row.id === id) ?? {}; - - const results = [...users, ...userGroups].map(({ name }) => name); - return results.length === 0 ? "-" : results.join(", "); - }, - } - : undefined; - - const columns: TableColumn[] = uniqCombine([ - ...model.getColumns(), - ...additionalColumns, - { ...responsibleField, sortable: false }, - ]); - - const details: ObjectsTableDetailField[] = uniqCombine([ - ...model.getDetails(), - responsibleField, - ]); - - const actions: TableAction[] = uniqCombine([ - ...tableActions, - ...additionalActions, - ]); - - return ( - - setSharingSettingsElement(undefined)} - /> - - - rows={transformRows(rows)} - columns={columns} - details={details} - onChangeSearch={changeSearchFilter} - initialState={initialState} - searchBoxLabel={i18n.t(`Search by `) + model.getSearchFilter().field} - pagination={pager} - onChange={handleTableChange} - ids={ids} - loading={providedLoading || loading} - selection={[...selection, ...childrenSelection]} - childrenKeys={childrenKeys} - filterComponents={filterComponents} - forceSelectionColumn={true} - actions={actions} - sideComponents={orgUnitTreeFilter} - {...rest} - /> - - ); -}; - -export default MetadataTable; diff --git a/src/presentation/react/components/metadata-table/utils.tsx b/src/presentation/react/components/metadata-table/utils.tsx deleted file mode 100644 index bcce28288..000000000 --- a/src/presentation/react/components/metadata-table/utils.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import memoize from "nano-memoize"; -import { MetadataEntities } from "../../../../domain/metadata/entities/MetadataEntities"; -import { modelFactory } from "../../../../models/dhis/factory"; -import { D2Api } from "../../../../types/d2-api"; - -/** - * Load memoized filter data from an instance (This should be removed with a cache on d2-api) - * Note: _baseUrl is used as cacheKey to avoid memoizing values between instances - */ -export const getFilterData = memoize( - (modelName: keyof MetadataEntities, type: "group" | "level", _baseUrl: string, api: D2Api) => - modelFactory(modelName) - .getApiModel(api) - .get({ - paging: false, - fields: - type === "group" - ? { - id: true as const, - name: true as const, - } - : { - name: true as const, - level: true as const, - }, - order: type === "group" ? undefined : `level:iasc`, - }) - .getData(), - { maxArgs: 3 } -); - -export async function getOrgUnitSubtree(api: D2Api, orgUnitId: string): Promise { - const { organisationUnits } = (await api - .get(`/organisationUnits/${orgUnitId}`, { fields: "id", includeDescendants: true }) - .getData()) as { organisationUnits: { id: string }[] }; - - return organisationUnits.map(({ id }) => id); -} diff --git a/src/presentation/react/components/migrations/Migrations.tsx b/src/presentation/react/components/migrations/Migrations.tsx deleted file mode 100644 index dfc430e8f..000000000 --- a/src/presentation/react/components/migrations/Migrations.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { ConfirmationDialog } from "d2-ui-components"; -import React, { useCallback, useEffect, useState } from "react"; -import i18n from "../../../../locales"; -import { MigrationsRunner } from "../../../../migrations"; - -export interface MigrationsProps { - runner: MigrationsRunner; - onFinish: () => void; -} - -type State = - | { type: "show-info" } - | { type: "app-out-of-date" } - | { type: "migrating" } - | { type: "success" }; - -const Migrations: React.FC = props => { - const { runner, onFinish } = props; - const [messages, setMessages] = useState([]); - const [state, setState] = useState(getInitialState(runner)); - useEffect(followContents, [messages]); - - const debug = useCallback((message: string) => { - setMessages(messages => [...messages, message]); - }, []); - - const startMigration = useCallback(() => { - runMigrations(runner, debug, setState).then(setState); - }, [runner, debug]); - - const actionText = getActionText(state); - - if (state.type === "app-out-of-date") { - return ; - } - - return ( - (state.type === "success" ? onFinish() : startMigration())} - saveText={actionText} - onCancel={undefined} - disableSave={state.type === "migrating" || !actionText} - maxWidth="md" - fullWidth={true} - > -
    -

    {getPendingMigrationsText(runner)}

    - -

    - {messages.map((msg, idx) => ( - - {msg} -
    -
    - ))} -

    - -

    - {state.type === "success" && - i18n.t("Migrations finished successfully, you may now continue to the app")} -

    -
    -
    - ); -}; - -function runMigrations( - runner: MigrationsRunner, - debug: (message: string) => void, - setState: React.Dispatch> -): Promise { - setState({ type: "migrating" }); - - return runner - .setDebug(debug) - .execute() - .then(() => ({ type: "success" as const })) - .catch(() => { - debug("---"); - debug( - i18n.t( - "There has been an error. You can either retry or contact your administrator if you think there has been an un recoverable error" - ) - ); - return { type: "show-info" as const }; - }); -} - -function followContents() { - const contentsEl = document.getElementById("migrations-contents"); - const divEl = contentsEl ? contentsEl.parentElement : null; - if (divEl) divEl.scrollTop = divEl.scrollHeight; -} - -function getActionText(state: State): string | undefined { - switch (state.type) { - case "show-info": - return i18n.t("Migrate instance"); - case "migrating": - return i18n.t("Migrating..."); - case "success": - return i18n.t("Continue to the App"); - case "app-out-of-date": - return; - } -} - -function getInitialState(runner: MigrationsRunner): State { - if (runner.instanceVersion === runner.appVersion) { - return { type: "success" }; - } else if (runner.instanceVersion > runner.appVersion) { - return { type: "app-out-of-date" }; - } else { - return { type: "show-info" }; - } -} - -function getPendingMigrationsText(runner: MigrationsRunner): string { - return i18n.t( - "The app needs to run all pending migrations (v{{instanceVersion}} -> v{{appVersion}}) in order to continue. This may take a long time, make sure the process is not interrupted.", - runner - ); -} - -const isDebug = process.env.NODE_ENV === "development"; - -const MigrationsError: React.FC<{ runner: MigrationsRunner; onFinish: () => void }> = ({ - runner, - onFinish, -}) => ( - - {i18n.t( - "The database version (v{{instanceVersion}}) is greater than the app version (v{{appVersion}}), we cannot continue. Please contact the administrator to update the app.", - runner - )} - -); - -export default Migrations; diff --git a/src/presentation/react/components/module-list-table/ModuleListTable.tsx b/src/presentation/react/components/module-list-table/ModuleListTable.tsx deleted file mode 100644 index 20d573743..000000000 --- a/src/presentation/react/components/module-list-table/ModuleListTable.tsx +++ /dev/null @@ -1,556 +0,0 @@ -import { Icon } from "@material-ui/core"; -import { - ConfirmationDialog, - ConfirmationDialogProps, - MetaObject, - ObjectsTable, - ObjectsTableDetailField, - SearchResult, - ShareUpdate, - TableAction, - TableColumn, - TableSelection, - TableState, - useLoading, - useSnackbar, -} from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { Module } from "../../../../domain/modules/entities/Module"; -import { Package } from "../../../../domain/packages/entities/Package"; -import i18n from "../../../../locales"; -import { promiseMap } from "../../../../utils/common"; -import { getUserInfo, isGlobalAdmin, UserInfo } from "../../../../utils/permissions"; -import Dropdown from "../dropdown/Dropdown"; -import { - PullRequestCreation, - PullRequestCreationDialog, -} from "../pull-request-creation-dialog/PullRequestCreationDialog"; -import { SharingDialog } from "../sharing-dialog/SharingDialog"; -import { ModulePackageListPageProps } from "../../../webapp/core/pages/module-package-list/ModulePackageListPage"; -import { useAppContext } from "../../contexts/AppContext"; -import { NewPackageDialog } from "./NewPackageDialog"; -import { getValidationsByVersionFeedback } from "./utils"; - -export const ModulesListTable: React.FC = ({ - remoteInstance, - onActionButtonClick, - presentation = "app", - externalComponents, - openSyncSummary = _.noop, - paginationOptions, -}) => { - const { compositionRoot, api } = useAppContext(); - const snackbar = useSnackbar(); - const loading = useLoading(); - const history = useHistory(); - - const [rows, setRows] = useState([]); - const [selection, updateSelection] = useState([]); - - const [resetKey, setResetKey] = useState(Math.random()); - const [isTableLoading, setIsTableLoading] = useState(false); - const [newPackageModule, setNewPackageModule] = useState(); - const [sharingSettingsObject, setSharingSettingsObject] = useState(null); - const [pullRequestProps, setPullRequestProps] = useState(); - const [dialogProps, updateDialog] = useState(null); - const [departmentFilter, setDepartmentFilter] = useState(""); - - const [globalAdmin, setGlobalAdmin] = useState(false); - const [userInfo, setUserInfo] = useState(); - - const editModule = useCallback( - (ids: string[]) => { - const item = _.find(rows, ({ id }) => id === ids[0]); - if (!item) snackbar.error(i18n.t("Invalid module")); - else history.push({ pathname: `/modules/edit/${item.id}`, state: { module: item } }); - }, - [rows, history, snackbar] - ); - - const downloadSnapshot = useCallback( - async (ids: string[]) => { - const module = _.find(rows, ({ id }) => id === ids[0]); - if (!module) snackbar.error(i18n.t("Invalid module")); - else { - loading.show(true, i18n.t("Downloading snapshot for module {{name}}", module)); - - const originInstance = remoteInstance?.id ?? "LOCAL"; - const contents = await compositionRoot.sync[module.type]({ - ...module.toSyncBuilder(), - originInstance, - targetInstances: [], - }).buildPayload(); - - await compositionRoot.modules.download(module, contents); - loading.reset(); - } - }, - [compositionRoot, remoteInstance, rows, snackbar, loading] - ); - - const createPackage = useCallback( - async (ids: string[]) => { - const module = _.find(rows, ({ id }) => id === ids[0]); - if (!module) snackbar.error(i18n.t("Invalid module")); - else setNewPackageModule(module); - }, - [rows, snackbar] - ); - - const savePackage = useCallback( - async (item: Package, versions: string[]) => { - setNewPackageModule(undefined); - const module = _.find(rows, ({ id }) => id === item.module.id); - if (!module) snackbar.error(i18n.t("Invalid module")); - else { - const validationsByVersion = _.fromPairs( - await promiseMap(versions, async dhisVersion => { - loading.show( - true, - i18n.t("Creating {{dhisVersion}} package for module {{name}}", { - name: module.name, - dhisVersion, - }) - ); - - const validations = await compositionRoot.packages.create( - remoteInstance?.id ?? "LOCAL", - item, - module, - dhisVersion - ); - - return [dhisVersion, validations]; - }) - ); - - const [level, msg] = getValidationsByVersionFeedback(module, validationsByVersion); - snackbar.openSnackbar(level, msg); - - loading.reset(); - setResetKey(Math.random()); - } - }, - [compositionRoot, remoteInstance, rows, snackbar, loading] - ); - - const pullModule = useCallback( - async (ids: string[]) => { - const module = _.find(rows, ({ id }) => id === ids[0]); - if (!module) snackbar.error(i18n.t("Invalid module")); - else { - loading.show(true, i18n.t("Pulling metadata from module {{name}}", module)); - - const originInstance = remoteInstance?.id ?? "LOCAL"; - const builder = { - ...module.toSyncBuilder(), - originInstance, - targetInstances: ["LOCAL"], - }; - - const result = await compositionRoot.sync.prepare(module.type, builder); - const sync = compositionRoot.sync[module.type](builder); - - const createPullRequest = () => { - if (!remoteInstance) { - snackbar.error(i18n.t("Unable to create pull request")); - } else { - setPullRequestProps({ - instance: remoteInstance, - builder, - type: module.type, - }); - } - }; - - const synchronize = async () => { - for await (const { message, syncReport, done } of sync.execute()) { - if (message) loading.show(true, message); - if (syncReport) await syncReport.save(api); - if (done) { - openSyncSummary(syncReport); - return; - } - } - }; - - await result.match({ - success: async () => { - await synchronize(); - }, - error: async code => { - switch (code) { - case "PULL_REQUEST": - createPullRequest(); - break; - case "PULL_REQUEST_RESPONSIBLE": - updateDialog({ - title: i18n.t("Pull metadata"), - description: i18n.t( - "You are one of the reponsibles for the selected items.\nDo you want to directly pull the metadata?" - ), - onCancel: () => { - updateDialog(null); - }, - onSave: async () => { - updateDialog(null); - await synchronize(); - }, - onInfoAction: () => { - updateDialog(null); - createPullRequest(); - }, - cancelText: i18n.t("Cancel"), - saveText: i18n.t("Proceed"), - infoActionText: i18n.t("Create pull request"), - }); - break; - case "INSTANCE_NOT_FOUND": - snackbar.warning(i18n.t("Couldn't connect with instance")); - break; - default: - snackbar.error(i18n.t("Unknown synchronization error")); - } - }, - }); - - loading.reset(); - } - }, - [compositionRoot, openSyncSummary, remoteInstance, loading, rows, snackbar, api] - ); - - const replicateModule = useCallback( - async (ids: string[]) => { - const item = _.find(rows, ({ id }) => id === ids[0]); - if (!item) { - snackbar.error(i18n.t("Invalid module")); - return; - } - - history.push({ - pathname: `/modules/new`, - state: { module: item.replicate() }, - }); - }, - [history, rows, snackbar] - ); - - const deleteModule = useCallback( - async (ids: string[]) => { - loading.show(true, "Deleting modules"); - for (const id of ids) { - await compositionRoot.modules.delete(id); - } - loading.reset(); - setResetKey(Math.random()); - updateSelection([]); - }, - [compositionRoot, loading] - ); - - const openSharingSettings = useCallback( - async (ids: string[]) => { - const module = _.find(rows, ({ id }) => id === ids[0]); - if (!module) { - snackbar.error(i18n.t("Invalid module")); - return; - } - - setSharingSettingsObject({ - object: module, - meta: { allowPublicAccess: true, allowExternalAccess: false }, - }); - }, - [rows, snackbar] - ); - - const updateTable = useCallback( - ({ selection }: TableState) => { - updateSelection(selection); - }, - [updateSelection] - ); - - const verifyUserHasWritePermissions = useCallback( - (modules: Module[]) => { - if (globalAdmin) return true; - - for (const module of modules) { - if (!!userInfo && !module.hasPermissions("write", userInfo.id, userInfo.userGroups)) - return false; - } - - return true; - }, - [globalAdmin, userInfo] - ); - - const columns: TableColumn[] = useMemo( - () => [ - { name: "name", text: i18n.t("Name"), sortable: true }, - { - name: "department", - text: i18n.t("Department"), - sortable: true, - getValue: ({ department }) => { - return department.name; - }, - }, - { name: "description", text: i18n.t("Description"), sortable: true, hidden: true }, - { - name: "metadataIds", - text: "Selected metadata", - getValue: module => `${module.metadataIds.length} elements`, - }, - { name: "lastUpdated", text: i18n.t("Last updated"), hidden: true }, - { name: "lastUpdatedBy", text: i18n.t("Last updated by"), hidden: true }, - { name: "created", text: i18n.t("Created"), hidden: true }, - { name: "user", text: i18n.t("Created by"), hidden: true }, - ], - [] - ); - - const details: ObjectsTableDetailField[] = useMemo( - () => [ - { name: "name", text: i18n.t("Name") }, - { - name: "department", - text: i18n.t("Department"), - getValue: ({ department }) => { - return department.name; - }, - }, - { name: "description", text: i18n.t("Description") }, - { - name: "metadataIds", - text: i18n.t("Selected metadata"), - getValue: module => `${module.metadataIds.length} elements`, - }, - { name: "lastUpdated", text: i18n.t("Last updated") }, - { name: "lastUpdatedBy", text: i18n.t("Last updated by") }, - { name: "created", text: i18n.t("Created") }, - { name: "user", text: i18n.t("Created by") }, - ], - [] - ); - - const actions: TableAction[] = useMemo( - () => [ - { - name: "details", - text: i18n.t("Details"), - multiple: false, - primary: presentation !== "app" && !remoteInstance, - }, - { - name: "edit", - text: i18n.t("Edit"), - multiple: false, - isActive: modules => - presentation === "app" && - !remoteInstance && - verifyUserHasWritePermissions(modules), - onClick: editModule, - primary: presentation === "app" && !remoteInstance, - icon: edit, - }, - { - name: "delete", - text: i18n.t("Delete"), - multiple: true, - isActive: modules => - presentation === "app" && - !remoteInstance && - verifyUserHasWritePermissions(modules), - onClick: deleteModule, - icon: delete, - }, - { - name: "replicate", - text: i18n.t("Replicate"), - multiple: false, - onClick: replicateModule, - icon: content_copy, - isActive: () => presentation === "app" && !remoteInstance, - }, - { - name: "download", - text: i18n.t("Download metadata package"), - multiple: false, - onClick: downloadSnapshot, - icon: cloud_download, - }, - { - name: "package-data-store", - text: i18n.t("Generate package from module"), - multiple: false, - icon: description, - isActive: () => presentation === "app" && !remoteInstance, - onClick: createPackage, - }, - { - name: "pull-metadata", - text: i18n.t("Pull metadata"), - multiple: false, - icon: arrow_downward, - isActive: () => presentation === "app" && !!remoteInstance, - onClick: pullModule, - }, - { - name: "sharingSettings", - text: i18n.t("Sharing settings"), - multiple: false, - isActive: verifyUserHasWritePermissions, - onClick: openSharingSettings, - icon: share, - }, - ], - [ - createPackage, - deleteModule, - downloadSnapshot, - editModule, - openSharingSettings, - presentation, - pullModule, - remoteInstance, - replicateModule, - verifyUserHasWritePermissions, - ] - ); - - const departmentFilterItems = useMemo(() => { - return _(rows) - .map(({ department }) => department) - .uniqBy(({ id }) => id) - .sortBy(({ name }) => name) - .value(); - }, [rows]); - - const filterComponents = useMemo(() => { - const departmentFilterComponent = ( - - ); - - return [externalComponents, departmentFilterComponent]; - }, [externalComponents, departmentFilter, departmentFilterItems]); - - const rowsFiltered = useMemo(() => { - return departmentFilter - ? rows.filter(({ department }) => department.id === departmentFilter) - : rows; - }, [departmentFilter, rows]); - - const onSearchRequest = useCallback( - async (key: string) => - api - .get("/sharing/search", { key }) - .getData(), - [api] - ); - - const onSharingChanged = useCallback( - async (updatedAttributes: ShareUpdate) => { - if (!sharingSettingsObject) return; - - const module = (sharingSettingsObject.object as Module).update(updatedAttributes); - - await compositionRoot.modules.save(module); - setSharingSettingsObject({ - meta: sharingSettingsObject.meta, - object: module, - }); - }, - [sharingSettingsObject, compositionRoot] - ); - - const closeSharingSettingsDialog = useCallback(() => { - setSharingSettingsObject(null); - setResetKey(Math.random()); - }, []); - - useEffect(() => { - setIsTableLoading(true); - compositionRoot.modules - .list(globalAdmin, remoteInstance) - .then(rows => { - setRows(rows); - setIsTableLoading(false); - }) - .catch((error: Error) => { - snackbar.error(error.message); - setRows([]); - setIsTableLoading(false); - }); - }, [compositionRoot, remoteInstance, resetKey, snackbar, setIsTableLoading, globalAdmin]); - - useEffect(() => { - setDepartmentFilter(""); - }, [remoteInstance]); - - useEffect(() => { - isGlobalAdmin(api).then(setGlobalAdmin); - getUserInfo(api).then(setUserInfo); - }, [api]); - - return ( - - - rows={rowsFiltered} - loading={isTableLoading} - columns={columns} - details={details} - actions={actions} - onActionButtonClick={onActionButtonClick} - forceSelectionColumn={presentation === "app"} - filterComponents={filterComponents} - selection={selection} - onChange={updateTable} - paginationOptions={paginationOptions} - /> - - {!!newPackageModule && ( - setNewPackageModule(undefined)} - module={newPackageModule} - /> - )} - - {!!pullRequestProps && ( - setPullRequestProps(undefined)} - /> - )} - - {!!sharingSettingsObject && ( - - )} - - {dialogProps && } - - ); -}; diff --git a/src/presentation/react/components/module-list-table/NewPackageDialog.tsx b/src/presentation/react/components/module-list-table/NewPackageDialog.tsx deleted file mode 100644 index e7d9ff043..000000000 --- a/src/presentation/react/components/module-list-table/NewPackageDialog.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { makeStyles, TextField } from "@material-ui/core"; -import Autocomplete from "@material-ui/lab/Autocomplete"; -import { ConfirmationDialog } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useState } from "react"; -import semver from "semver"; -import { ValidationError } from "../../../../domain/common/entities/Validations"; -import { Module } from "../../../../domain/modules/entities/Module"; -import { Package } from "../../../../domain/packages/entities/Package"; -import i18n from "../../../../locales"; -import { Dictionary } from "../../../../types/utils"; -import { useAppContext } from "../../contexts/AppContext"; - -export const NewPackageDialog: React.FC = ({ module, save, close }) => { - const { compositionRoot } = useAppContext(); - const classes = useStyles(); - - const [versions, updateVersions] = useState([]); - const [item, updateItem] = useState( - Package.build({ - name: i18n.t("Package of {{name}}", module), - module, - version: - semver.parse(module.lastPackageVersion.split("-")[0])?.inc("patch").format() ?? - "1.0.0", - }) - ); - - const [errors, setErrors] = useState>({}); - - const updateModel = useCallback( - (field: keyof Package, value: string) => { - const newPackage = item.update({ [field]: value }); - const errors = _.keyBy(newPackage.validate([field], module), "property"); - - setErrors(errors); - updateItem(newPackage); - }, - [item, module] - ); - - const onChangeField = useCallback( - (field: keyof Package) => { - return (event: React.ChangeEvent<{ value: unknown }>) => { - updateModel(field, event.target.value as string); - }; - }, - [updateModel] - ); - - const updateVersionNumber = useCallback( - (event: React.ChangeEvent<{ value: unknown }>) => { - const revision = event.target.value as string; - const tag = item.version.split("-")[1]; - const newVersion = [revision, tag].join("-"); - updateModel("version", newVersion); - }, - [item, updateModel] - ); - - const updateVersionTag = useCallback( - (event: React.ChangeEvent<{ value: unknown }>) => { - const revision = item.version.split("-")[0]; - const tag = event.target.value ? (event.target.value as string) : undefined; - const newVersion = semver.parse([revision, tag].join("-"))?.format(); - updateModel("version", newVersion ?? revision); - }, - [item, updateModel] - ); - - const onSave = useCallback(() => { - const errors = item.validate(undefined, module); - const messages = _.keyBy(errors, "property"); - - if (errors.length === 0) save(item, versions); - else setErrors(messages); - }, [item, save, module, versions]); - - useEffect(() => { - compositionRoot.instances.getVersion().then(version => { - if (versions.length === 0) updateVersions([version]); - }); - }, [compositionRoot, versions, updateVersions]); - - return ( - - - -
    - - -
    - - updateVersions(value)} - renderTags={(values: string[]) => values.sort().join(", ")} - renderInput={params => ( - - )} - /> - - -
    - ); -}; - -export interface NewPackageDialogProps { - module: Module; - save: (item: Package, versions: string[]) => void; - close: () => void; -} - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, - versionRow: { - width: "100%", - display: "flex", - flex: "1 1 auto", - marginBottom: 25, - }, - marginRight: { - marginRight: 10, - }, -}); diff --git a/src/presentation/react/components/module-list-table/utils.ts b/src/presentation/react/components/module-list-table/utils.ts deleted file mode 100644 index e42e76108..000000000 --- a/src/presentation/react/components/module-list-table/utils.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { MetadataModule } from "../../../../domain/modules/entities/MetadataModule"; -import _ from "lodash"; -import { ValidationError } from "../../../../domain/common/entities/Validations"; -import { SnackbarLevel } from "d2-ui-components"; -import i18n from "../../../../locales"; - -export function getValidationsByVersionFeedback( - module: MetadataModule, - validationsByVersion: _.Dictionary -): [SnackbarLevel, string] { - const successVersions = _(validationsByVersion) - .pickBy(validations => _.isEmpty(validations)) - .keys() - .value(); - - const errorVersions = _(validationsByVersion) - .pickBy(validations => !_.isEmpty(validations)) - .keys() - .value(); - - const msg = _.compact([ - i18n.t("Module: {{module}}", { - module: module.name, - nsSeparator: false, - }), - successVersions.length > 0 - ? i18n.t("{{n}} package(s) created successfully: {{list}}", { - n: successVersions.length, - list: successVersions.join(", "), - nsSeparator: false, - }) - : null, - errorVersions.length > 0 - ? i18n.t("{{n}} package(s) could not be created: {{list}}", { - n: errorVersions.length, - list: errorVersions.join(", "), - nsSeparator: false, - }) - : null, - ..._(validationsByVersion) - .toPairs() - .sortBy(([version, _validations]) => version) - .flatMap(([version, validations]) => - validations.map(v => `[${version}] ${v.description}`) - ) - .value(), - ]).join("\n"); - - const level = _.isEmpty(errorVersions) - ? "success" - : _.isEmpty(successVersions) - ? "error" - : "warning"; - - return [level, msg]; -} diff --git a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx b/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx deleted file mode 100644 index 9b2ee092c..000000000 --- a/src/presentation/react/components/module-package-list-table/ModulePackageListTable.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { PaginationOptions } from "d2-ui-components"; -import React, { ReactNode, useCallback, useMemo, useState } from "react"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { ModulesListTable } from "../module-list-table/ModuleListTable"; -import { PackagesListTable } from "../package-list-table/PackageListTable"; -import Dropdown from "../dropdown/Dropdown"; -import { - InstanceSelectionConfig, - InstanceSelectionDropdown, - InstanceSelectionOption, -} from "../instance-selection-dropdown/InstanceSelectionDropdown"; -import { useViewSelector, ViewSelectorConfig } from "./useViewSelector"; -import { Store } from "../../../../domain/stores/entities/Store"; - -export interface ModulePackageListTableProps { - onCreate?(): void; - onViewChange?(option: ViewOption): void; - viewValue?: ViewOption; - presentation: PresentationOption; - showSelector: ViewSelectorConfig; - showInstances: InstanceSelectionConfig; - openSyncSummary?: (syncReport: SyncReport) => void; - onInstanceChange?: (instance?: Instance | Store) => void; - actionButtonLabel?: ReactNode; -} - -export type ViewOption = "modules" | "packages"; -export type PresentationOption = "app" | "widget"; - -export const ModulePackageListTable: React.FC = ({ - onCreate, - onViewChange, - viewValue: propsViewValue, - presentation, - showSelector, - showInstances, - openSyncSummary, - onInstanceChange, - actionButtonLabel, -}) => { - const [selectedInstance, setSelectedInstance] = useState(); - const [selectedStore, setSelectedStore] = useState(); - const [selection, setSelection] = useState([]); - - const viewSelector = useViewSelector(showSelector, propsViewValue); - - const setValue = useCallback( - (value: ViewOption) => { - viewSelector.setValue(value); - if (onViewChange) onViewChange(value); - }, - [viewSelector, onViewChange] - ); - - const updateSelectedInstance = useCallback( - (type: InstanceSelectionOption, source?: Instance | Store) => { - setSelection([]); - setSelectedStore(type === "store" ? (source as Store) : undefined); - setSelectedInstance(type === "remote" ? (source as Instance) : undefined); - - if (onInstanceChange) { - onInstanceChange(source); - } - }, - [onInstanceChange] - ); - - const filters = useMemo( - () => ( - - - - {viewSelector.items.length > 1 && viewSelector.value && ( - - )} - - ), - [ - showInstances, - selectedInstance, - setValue, - viewSelector, - updateSelectedInstance, - selectedStore, - ] - ); - - const Table = viewSelector.value === "packages" ? PackagesListTable : ModulesListTable; - - return ( - - ); -}; - -const paginationOptions: PaginationOptions = { - pageSizeOptions: [10], - pageSizeInitialValue: 10, -}; diff --git a/src/presentation/react/components/module-package-list-table/useViewSelector.tsx b/src/presentation/react/components/module-package-list-table/useViewSelector.tsx deleted file mode 100644 index 683a90b28..000000000 --- a/src/presentation/react/components/module-package-list-table/useViewSelector.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import _ from "lodash"; -import { useMemo, useState } from "react"; -import i18n from "../../../../locales"; -import { ViewOption } from "./ModulePackageListTable"; - -export interface ViewSelectorConfig { - modules?: boolean; - packages?: boolean; -} - -export function useViewSelector( - { modules = true, packages = true }: ViewSelectorConfig, - initialValue?: ViewOption -) { - const items = useMemo( - () => - _.compact([ - modules && { id: "modules" as const, name: i18n.t("Modules") }, - packages && { id: "packages" as const, name: i18n.t("Packages") }, - ]), - [modules, packages] - ); - - const [value, setValue] = useState( - () => initialValue ?? _.first(items.map(item => item.id)) - ); - - return useMemo(() => ({ items, value, setValue }), [items, value, setValue]); -} diff --git a/src/presentation/react/components/module-wizard/ModuleWizard.tsx b/src/presentation/react/components/module-wizard/ModuleWizard.tsx deleted file mode 100644 index 6005eee0c..000000000 --- a/src/presentation/react/components/module-wizard/ModuleWizard.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Wizard, WizardStep } from "d2-ui-components"; -import _ from "lodash"; -import React from "react"; -import { useLocation } from "react-router-dom"; -import { Module } from "../../../../domain/modules/entities/Module"; -import { metadataModuleSteps, ModuleWizardStepProps } from "./Steps"; - -export interface ModuleWizardProps { - isEdit: boolean; - onCancel: () => void; - onClose: () => void; - module: Module; - onChange: (module: Module) => void; -} - -export const ModuleWizard: React.FC = ({ - isEdit, - onCancel, - onClose, - module, - onChange, -}) => { - const location = useLocation(); - - const props: ModuleWizardStepProps = { module, onChange, onCancel, onClose, isEdit }; - const steps = metadataModuleSteps.map(step => ({ ...step, props })); - - const onStepChangeRequest = async (_currentStep: WizardStep, newStep: WizardStep) => { - const index = _(steps).findIndex(step => step.key === newStep.key); - return _.take(steps, index).flatMap(({ validationKeys }) => - module.validate(validationKeys).map(({ description }) => description) - ); - }; - - const urlHash = location.hash.slice(1); - const stepExists = steps.find(step => step.key === urlHash); - const firstStepKey = steps.map(step => step.key)[0]; - const initialStepKey = stepExists ? urlHash : firstStepKey; - - return ( - - ); -}; diff --git a/src/presentation/react/components/module-wizard/Steps.ts b/src/presentation/react/components/module-wizard/Steps.ts deleted file mode 100644 index df543a3f7..000000000 --- a/src/presentation/react/components/module-wizard/Steps.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { WizardStep } from "d2-ui-components"; -import { Module } from "../../../../domain/modules/entities/Module"; -import i18n from "../../../../locales"; -import { GeneralInfoStep } from "./common/GeneralInfoStep"; -import { MetadataSelectionStep } from "./common/MetadataSelectionStep"; -import { SummaryStep } from "./common/SummaryStep"; -import { AdvancedMetadataOptionsStep } from "./metadata/AdvancedMetadataOptionsStep"; -import { MetadataIncludeExcludeStep } from "./metadata/MetadataIncludeExcludeStep"; - -export interface SyncWizardStep extends WizardStep { - validationKeys: string[]; - showOnSyncDialog?: boolean; -} - -export interface ModuleWizardStepProps { - module: T; - onChange: (module: T) => void; - onCancel: () => void; - onClose: () => void; - isEdit: boolean; -} - -const commonSteps: { - [key: string]: SyncWizardStep; -} = { - generalInfo: { - key: "general-info", - label: i18n.t("General info"), - component: GeneralInfoStep, - validationKeys: ["name", "department"], - }, - summary: { - key: "summary", - label: i18n.t("Summary"), - component: SummaryStep, - validationKeys: [], - showOnSyncDialog: true, - }, -}; - -export const metadataModuleSteps: SyncWizardStep[] = [ - commonSteps.generalInfo, - { - key: "metadata", - label: i18n.t("Metadata"), - component: MetadataSelectionStep, - validationKeys: ["metadataIds"], - }, - { - key: "dependencies-selection", - label: i18n.t("Select dependencies"), - component: MetadataIncludeExcludeStep, - validationKeys: ["metadataIncludeExclude"], - }, - { - key: "advanced-metadata-options", - label: i18n.t("Advanced options"), - component: AdvancedMetadataOptionsStep, - validationKeys: [], - }, - commonSteps.summary, -]; diff --git a/src/presentation/react/components/module-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/components/module-wizard/common/GeneralInfoStep.tsx deleted file mode 100644 index 90a1d5fe2..000000000 --- a/src/presentation/react/components/module-wizard/common/GeneralInfoStep.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { makeStyles, TextField } from "@material-ui/core"; -import _ from "lodash"; -import React, { useCallback, useEffect, useState } from "react"; -import { NamedRef } from "../../../../../domain/common/entities/Ref"; -import { ValidationError } from "../../../../../domain/common/entities/Validations"; -import { Module } from "../../../../../domain/modules/entities/Module"; -import i18n from "../../../../../locales"; -import { Dictionary } from "../../../../../types/utils"; -import { useAppContext } from "../../../contexts/AppContext"; -import Dropdown from "../../dropdown/Dropdown"; -import { ModuleWizardStepProps } from "../Steps"; - -export const GeneralInfoStep = ({ module, onChange, isEdit }: ModuleWizardStepProps) => { - const { compositionRoot } = useAppContext(); - const classes = useStyles(); - - const [errors, setErrors] = useState>({}); - const [userGroups, setUserGroups] = useState([]); - - const onChangeField = useCallback( - (field: keyof Module) => { - return (event: React.ChangeEvent) => { - const newModule = module.update({ [field]: event.target.value }); - const errors = _.keyBy(newModule.validate([field]), "property"); - - setErrors(errors); - onChange(newModule); - }; - }, - [module, onChange] - ); - - const onChangeDepartment = useCallback( - (id: string) => { - const department = userGroups.find(group => group.id === id); - onChange(module.update({ department })); - }, - [module, onChange, userGroups] - ); - - useEffect(() => { - compositionRoot.instances.getUserGroups().then(setUserGroups); - }, [compositionRoot]); - - return ( - - - - - - - - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, -}); diff --git a/src/presentation/react/components/module-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/components/module-wizard/common/MetadataSelectionStep.tsx deleted file mode 100644 index dae0fafe9..000000000 --- a/src/presentation/react/components/module-wizard/common/MetadataSelectionStep.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback } from "react"; -import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; -import i18n from "../../../../../locales"; -import { DashboardModel, DataSetModel, ProgramModel } from "../../../../../models/dhis/metadata"; -import MetadataTable from "../../metadata-table/MetadataTable"; -import { ModuleWizardStepProps } from "../Steps"; - -const config = { - module: { - metadata: { models: [DataSetModel, ProgramModel, DashboardModel], childrenKeys: [] }, - }, -}; - -export const MetadataSelectionStep = ({ - module, - onChange, -}: ModuleWizardStepProps) => { - const snackbar = useSnackbar(); - const { models, childrenKeys } = config["module"][module.type]; - - const changeSelection = useCallback( - (newMetadataIds: string[], newExcludedIds: string[]) => { - const additions = _.difference(newMetadataIds, module.metadataIds); - if (additions.length > 0) { - snackbar.info( - i18n.t("Selected {{difference}} elements", { difference: additions.length }), - { - autoHideDuration: 1000, - } - ); - } - - const removals = _.difference(module.metadataIds, newMetadataIds); - if (removals.length > 0) { - snackbar.info( - i18n.t("Removed {{difference}} elements", { - difference: Math.abs(removals.length), - }), - { autoHideDuration: 1000 } - ); - } - - onChange(module.update({ metadataIds: newMetadataIds, excludedIds: newExcludedIds })); - }, - [module, onChange, snackbar] - ); - - return ( - - ); -}; diff --git a/src/presentation/react/components/module-wizard/common/SummaryStep.tsx b/src/presentation/react/components/module-wizard/common/SummaryStep.tsx deleted file mode 100644 index b40d050a9..000000000 --- a/src/presentation/react/components/module-wizard/common/SummaryStep.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { Button, LinearProgress, makeStyles } from "@material-ui/core"; -import { useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { ReactNode, useEffect, useState } from "react"; -import { NamedRef } from "../../../../../domain/common/entities/Ref"; -import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; -import { Module } from "../../../../../domain/modules/entities/Module"; -import i18n from "../../../../../locales"; -import { Dictionary } from "../../../../../types/utils"; -import { getMetadata } from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../contexts/AppContext"; -import { ModuleWizardStepProps } from "../Steps"; -import { MetadataEntities } from "../../../../../domain/metadata/entities/MetadataEntities"; - -export const SummaryStep = ({ module, onCancel, onClose }: ModuleWizardStepProps) => { - const classes = useStyles(); - const snackbar = useSnackbar(); - const { api, compositionRoot } = useAppContext(); - - const [isSaving, setIsSaving] = useState(false); - const [metadata, updateMetadata] = useState>({}); - - const save = async () => { - setIsSaving(true); - - const errors = await compositionRoot.modules.save(module); - - if (errors.length > 0) { - snackbar.error(errors.join("\n")); - } else { - onClose(); - } - - setIsSaving(false); - }; - - useEffect(() => { - getMetadata(api, module.metadataIds, "id,name").then(updateMetadata); - }, [api, module]); - - return ( - -
      - {getEntries(module).map(LiEntry)} - - {module.type === "metadata" && - _.keys(metadata).map(metadataType => { - const items = metadata[metadataType]; - - return ( - items.length > 0 && ( - -
        - {items.map(({ id, name }) => ( - - ))} -
      -
      - ) - ); - })} -
    -
    -
    - - -
    -
    - {isSaving && } -
    - ); -}; - -const useStyles = makeStyles({ - saveButton: { - margin: 10, - backgroundColor: "#2b98f0", - color: "white", - }, - buttonContainer: { - display: "flex", - justifyContent: "space-between", - }, -}); - -interface Entry { - label: string; - value?: string | number; - children?: ReactNode; - hide?: boolean; -} - -const LiEntry = ({ label, value, children, hide = false }: Entry) => { - if (hide) return null; - - return ( -
  • - {_.compact([label, value]).join(": ")} - {children} -
  • - ); -}; - -const getEntries = (module: Module): Entry[] => { - switch (module.type) { - case "metadata": - return buildMetadataEntries(module as MetadataModule); - default: - return buildCommonEntries(module); - } -}; - -const buildCommonEntries = ({ name, description }: Module): Entry[] => { - return [ - { label: i18n.t("Name"), value: name }, - { - label: i18n.t("Description"), - value: description, - }, - ]; -}; - -const buildMetadataEntries = (module: MetadataModule): Entry[] => { - return [ - ...buildCommonEntries(module), - { - label: i18n.t("Department"), - value: module.department.name, - }, - { - label: i18n.t("Selected metadata"), - value: `${module.metadataIds.length} elements`, - }, - { - label: i18n.t("Metadata exclusions"), - value: `${module.excludedIds.length} elements`, - hide: module.excludedIds.length === 0, - }, - ]; -}; diff --git a/src/presentation/react/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx b/src/presentation/react/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx deleted file mode 100644 index 6227c9c79..000000000 --- a/src/presentation/react/components/module-wizard/metadata/AdvancedMetadataOptionsStep.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from "react"; -import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; -import i18n from "../../../../../locales"; -import { Toggle } from "../../toggle/Toggle"; -import { ModuleWizardStepProps } from "../Steps"; - -export const AdvancedMetadataOptionsStep: React.FC> = ({ - module, - onChange, -}) => { - const changeSharingSettings = (includeUserInformation: boolean) => { - onChange(module.update({ includeUserInformation })); - }; - - const changeOrgUnitReferences = (removeOrgUnitReferences: boolean) => { - onChange(module.update({ removeOrgUnitReferences })); - }; - - return ( - -
    - -
    -
    - -
    -
    - ); -}; diff --git a/src/presentation/react/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx b/src/presentation/react/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx deleted file mode 100644 index abd5e8d3c..000000000 --- a/src/presentation/react/components/module-wizard/metadata/MetadataIncludeExcludeStep.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { D2SchemaProperties } from "d2-api/schemas"; -import { MultiSelector } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import { MetadataPackage } from "../../../../../domain/metadata/entities/MetadataEntities"; -import { includeExcludeRulesFriendlyNames } from "../../../../../domain/metadata/entities/MetadataFriendlyNames"; -import { MetadataModule } from "../../../../../domain/modules/entities/MetadataModule"; -import i18n from "../../../../../locales"; -import { D2Model } from "../../../../../models/dhis/default"; -import { modelFactory } from "../../../../../models/dhis/factory"; -import { getMetadata } from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../contexts/AppContext"; -import Dropdown, { DropdownOption } from "../../dropdown/Dropdown"; -import { Toggle } from "../../toggle/Toggle"; -import { ModuleWizardStepProps } from "../Steps"; - -export const MetadataIncludeExcludeStep: React.FC> = ({ - module, - onChange, -}) => { - const classes = useStyles(); - const { d2, api } = useAppContext(); - - const [modelSelectItems, setModelSelectItems] = useState([]); - const [models, setModels] = useState([]); - const [selectedType, setSelectedType] = useState(""); - - useEffect(() => { - getMetadata(api, module.metadataIds, "id,name").then((metadata: MetadataPackage) => { - const models = _.keys(metadata).map((type: string) => { - return modelFactory(type); - }); - - const options = models - .map((model: typeof D2Model) => api.models[model.getCollectionName()].schema) - .map((schema: D2SchemaProperties) => ({ - name: schema.displayName, - id: schema.name, - })); - - setModels(models); - setModelSelectItems(options); - }); - }, [d2, api, module]); - - const { includeRules = [], excludeRules = [] } = - module.metadataIncludeExcludeRules[selectedType] ?? {}; - const allRules = [...includeRules, ...excludeRules]; - const ruleOptions = allRules.map(rule => ({ - value: rule, - text: includeExcludeRulesFriendlyNames[rule] ?? rule, - })); - - const changeUseDefaultIncludeExclude = (useDefault: boolean) => { - onChange( - useDefault - ? module.markToUseDefaultIncludeExclude() - : module.markToNotUseDefaultIncludeExclude(models) - ); - }; - - const changeModelName = (modelName: string) => { - setSelectedType(modelName); - }; - - const changeInclude = (currentIncludeRules: any) => { - const type: string = selectedType; - - const oldIncludeRules: string[] = includeRules; - - const ruleToExclude = _.difference(oldIncludeRules, currentIncludeRules); - const ruleToInclude = _.difference(currentIncludeRules, oldIncludeRules); - - if (ruleToInclude.length > 0) { - onChange(module.moveRuleFromExcludeToInclude(type, ruleToInclude)); - } else if (ruleToExclude.length > 0) { - onChange(module.moveRuleFromIncludeToExclude(type, ruleToExclude)); - } - }; - - return ( - - - - {!module.useDefaultIncludeExclude && ( -
    - - - {selectedType && ( -
    - -
    - )} -
    - )} -
    - ); -}; - -const useStyles = makeStyles({ - includeExcludeContainer: { - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - marginTop: "20px", - }, - multiselectorContainer: { - width: "100%", - }, -}); diff --git a/src/presentation/react/components/notification-viewer-dialog/NotificationViewerDialog.tsx b/src/presentation/react/components/notification-viewer-dialog/NotificationViewerDialog.tsx deleted file mode 100644 index ef32093ca..000000000 --- a/src/presentation/react/components/notification-viewer-dialog/NotificationViewerDialog.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { ConfirmationDialog } from "d2-ui-components"; -import React, { useEffect, useState } from "react"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { AppNotification } from "../../../../domain/notifications/entities/Notification"; -import i18n from "../../../../locales"; -import { DashboardModel, DataSetModel, ProgramModel } from "../../../../models/dhis/metadata"; -import { useAppContext } from "../../contexts/AppContext"; -import MetadataTable from "../metadata-table/MetadataTable"; - -export interface NotificationViewerDialogProps { - notification: AppNotification; - onClose: () => void; -} - -export const NotificationViewerDialog: React.FC = ({ - notification, - onClose, -}) => { - const classes = useStyles(); - const { compositionRoot } = useAppContext(); - - const [remoteInstance, setRemoteInstance] = useState(); - const [error, setError] = useState(false); - - useEffect(() => { - if (notification.type === "sent-pull-request") { - compositionRoot.instances.getById(notification.instance.id).then(result => - result.match({ - success: setRemoteInstance, - error: () => setError(true), - }) - ); - } - }, [compositionRoot, notification]); - - return ( - - {notification.text &&

    {notification.text}

    } - - {error && i18n.t("Could not connect with remote instance")} - - {!error && - (notification.type === "received-pull-request" || - notification.type === "sent-pull-request") && ( - - )} -
    - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, -}); diff --git a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx deleted file mode 100644 index f4c08047c..000000000 --- a/src/presentation/react/components/package-import-dialog/PackageImportDialog.tsx +++ /dev/null @@ -1,235 +0,0 @@ -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"; -import { JSONDataSource } from "../../../../domain/instance/entities/JSONDataSource"; -import { PackageImportRule } from "../../../../domain/package-import/entities/PackageImportRule"; -import { - isInstance, - isStore, - PackageSource, -} from "../../../../domain/package-import/entities/PackageSource"; -import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; -import { Package } from "../../../../domain/packages/entities/Package"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { useAppContext } from "../../contexts/AppContext"; -import { PackageImportWizard } from "../package-import-wizard/PackageImportWizard"; - -interface PackageImportDialogProps { - isOpen: boolean; - instance: PackageSource; - selectedPackagesId?: string[]; - onClose: () => void; - openSyncSummary?: (result: SyncReport) => void; - disablePackageSelection?: boolean; -} - -const PackageImportDialog: React.FC = ({ - isOpen, - instance, - selectedPackagesId, - onClose, - openSyncSummary, - disablePackageSelection, -}) => { - const [enableImport, setEnableImport] = useState(false); - const snackbar = useSnackbar(); - const loading = useLoading(); - const { compositionRoot, api } = useAppContext(); - - const [packageImportRule, setPackageImportRule] = useState( - PackageImportRule.create(instance, selectedPackagesId) - ); - - useEffect(() => { - const rule = PackageImportRule.create(instance, selectedPackagesId); - setPackageImportRule(rule); - setEnableImport(rule.validate().length === 0); - }, [instance, selectedPackagesId]); - - const handlePackageImportRuleChange = (packageImportRule: PackageImportRule) => { - setEnableImport(packageImportRule.validate().length === 0); - setPackageImportRule(packageImportRule); - }; - - const saveImportedPackages = async ( - packages: Package[], - author: NamedRef, - packageSource: PackageSource, - storePackageUrls: Record - ) => { - const importedPackages = packages.map(pkg => - mapToImportedPackage(pkg, author, packageSource, storePackageUrls[pkg.id]) - ); - - const result = await compositionRoot.importedPackages.save(importedPackages); - - result.match({ - success: () => {}, - error: () => { - snackbar.error("An error has ocurred tracking the imported packages"); - }, - }); - }; - - const getPackage = (packageId: string): Promise> => { - if (isInstance(packageImportRule.source)) { - return compositionRoot.packages.get(packageId, packageImportRule.source); - } else { - return compositionRoot.packages.getStore(packageImportRule.source.id, packageId); - } - }; - - const handleExecuteImport = async () => { - // TODO: this steps coordination to import several packages, save the result - // and save the imported package should be in the domain layer, - // may be a new use case? ImportPackagesUseCase.execute (packageIds:string[]) - // Steps: - // - Retrieve current user - // - for each packageId - // 1 - retrieve package (store or instance) (using PackageRepository) - // 2 - Import (using MetadataRepository) - // 3 - Save Result (using ResultRepository) - // 4 - Save ImportedPackage (using ImportedPackageRepository) - const importedPackages: Package[] = []; - - 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 author = { id: currentUser.id, name: currentUser.userCredentials.username }; - - const executePackageImport = async (packageId: string) => { - const getPackageResult = await getPackage(packageId); - - await getPackageResult.match({ - success: async originPackage => { - loading.show( - true, - i18n.t("Importing package {{name}}", { name: originPackage.name }) - ); - - if (isStore(packageImportRule.source)) { - storePackageUrls[originPackage.id] = packageId; - } - - 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) - : undefined; - - const originDataSource = - originInstance?.value.data ?? - JSONDataSource.build(originPackage.dhisVersion, originPackage.contents); - - const result = await compositionRoot.packages.import( - originPackage, - mapping?.mappingDictionary, - originDataSource - ); - - report.setTypes( - _.uniq([...report.syncReport.types, ..._.keys(originPackage.contents)]) - ); - - report.setStatus( - result.status === "ERROR" || result.status === "NETWORK ERROR" - ? "FAILURE" - : "DONE" - ); - - const origin = isInstance(packageImportRule.source) - ? packageImportRule.source.toPublicObject() - : packageImportRule.source; - - report.addSyncResult({ - ...result, - originPackage: originPackage.toRef(), - origin: origin, - }); - - if (result.status === "SUCCESS") { - importedPackages.push(originPackage); - } - }, - error: async () => { - loading.reset(); - snackbar.error(i18n.t("Couldn't load package")); - }, - }); - }; - - for (const id of packageImportRule.packageIds) { - await executePackageImport(id); - } - - loading.show(true, i18n.t("Saving imported packages")); - - await report.save(api); - - await saveImportedPackages( - importedPackages, - author, - packageImportRule.source, - storePackageUrls - ); - - loading.reset(); - - if (openSyncSummary) { - openSyncSummary(report); - } - } catch (error) { - loading.reset(); - snackbar.error(i18n.t("An error has ocurred importing packages")); - } - }; - - return ( - handleExecuteImport()} - onCancel={onClose} - saveText={i18n.t("Import")} - maxWidth={"lg"} - fullWidth={true} - disableSave={!enableImport} - > - - - - - ); -}; - -export default PackageImportDialog; diff --git a/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx b/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx deleted file mode 100644 index 1bd41e4ab..000000000 --- a/src/presentation/react/components/package-import-wizard/PackageImportWizard.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { Wizard, WizardStep } from "d2-ui-components"; -import _ from "lodash"; -import React from "react"; -import { useLocation } from "react-router-dom"; -import { PackageImportRule } from "../../../../domain/package-import/entities/PackageImportRule"; -import i18n from "../../../../locales"; -import { InstanceStoreSelectionStep } from "./steps/InstanceStoreSelectionStep"; -import { PackageMappingStep } from "./steps/PackageMappingStep"; -import { PackageSelectionStep } from "./steps/PackageSelectionStep"; -import { SummaryStep } from "./steps/SummaryStep"; - -export interface PackageImportWizardStep extends WizardStep { - validationKeys: string[]; -} - -export interface PackageImportWizardStepProps { - packageImportRule: PackageImportRule; - onChange: (packageImportRule: PackageImportRule) => void; - onCancel: () => void; - onClose: () => void; -} - -export const stepsBaseInfo = [ - { - key: "instance-playstore", - label: i18n.t("Instances & Play Stores"), - component: InstanceStoreSelectionStep, - validationKeys: [], - }, - { - key: "packages", - label: i18n.t("Packages"), - component: PackageSelectionStep, - validationKeys: ["packageIds"], - }, - { - key: "package-mapping", - label: i18n.t("Packages mapping"), - component: PackageMappingStep, - validationKeys: [], - }, - { - key: "summary", - label: i18n.t("Summary"), - component: SummaryStep, - validationKeys: [], - }, -]; - -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 - .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); - const validationMessages = _.take(steps, index).map(({ validationKeys }) => - props.packageImportRule.validate(validationKeys).map(({ description }) => description) - ); - - return _.flatten(validationMessages); - }; - - const urlHash = location.hash.slice(1); - const stepExists = steps.find(step => step.key === urlHash); - const firstStepKey = steps.map(step => step.key)[0]; - const initialStepKey = stepExists ? urlHash : firstStepKey; - - return ( - - ); -}; diff --git a/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx b/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx deleted file mode 100644 index 0bb2dc537..000000000 --- a/src/presentation/react/components/package-import-wizard/steps/InstanceStoreSelectionStep.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Box, Icon, IconButton } from "@material-ui/core"; -import React, { useState } from "react"; -import { PackageSource } from "../../../../../domain/package-import/entities/PackageSource"; -import { Store } from "../../../../../domain/stores/entities/Store"; -import i18n from "../../../../../locales"; -import { - InstanceSelectionDropdown, - InstanceSelectionOption, -} from "../../instance-selection-dropdown/InstanceSelectionDropdown"; -import StoreCreationDialog from "../../store-creation/StoreCreationDialog"; -import { PackageImportWizardProps } from "../PackageImportWizard"; - -const showInstances = { remote: true, store: true }; - -export const InstanceStoreSelectionStep: React.FC = ({ - packageImportRule, - onChange, -}) => { - const [creationDialogOpen, setCreationDialogOpen] = useState(false); - const [refreshKey, setRefreshKey] = useState(Math.random); - - const handleSelectionChange = (_type: InstanceSelectionOption, source?: PackageSource) => { - if (source) onChange(packageImportRule.updateSource(source)); - }; - - const handleOnSaved = (store: Store) => { - setCreationDialogOpen(false); - onChange(packageImportRule.updateSource(store)); - setRefreshKey(Math.random); - }; - - return ( - - - - setCreationDialogOpen(true)}> - add_circle_outline - - - - setCreationDialogOpen(false)} - onSaved={handleOnSaved} - /> - - ); -}; diff --git a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx b/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx deleted file mode 100644 index 2c0e35e05..000000000 --- a/src/presentation/react/components/package-import-wizard/steps/PackageMappingStep.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import { useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useState } from "react"; -import { DataSource } from "../../../../../domain/instance/entities/DataSource"; -import { JSONDataSource } from "../../../../../domain/instance/entities/JSONDataSource"; -import { DataSourceMapping } from "../../../../../domain/mapping/entities/DataSourceMapping"; -import { - MetadataMapping, - MetadataMappingDictionary, -} from "../../../../../domain/mapping/entities/MetadataMapping"; -import { - MetadataEntities, - MetadataPackage, -} from "../../../../../domain/metadata/entities/MetadataEntities"; -import { - isInstance, - PackageSource, -} from "../../../../../domain/package-import/entities/PackageSource"; -import { ListPackage } from "../../../../../domain/packages/entities/Package"; -import i18n from "../../../../../locales"; -import { - AggregatedDataElementModel, - GlobalCategoryComboModel, - GlobalCategoryModel, - GlobalCategoryOptionGroupModel, - GlobalCategoryOptionGroupSetModel, - GlobalCategoryOptionModel, - GlobalOptionModel, - IndicatorMappedModel, - OrganisationUnitMappedModel, -} from "../../../../../models/dhis/mapping"; -import { isGlobalAdmin } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../contexts/AppContext"; -import Dropdown from "../../dropdown/Dropdown"; -import MappingTable from "../../mapping-table/MappingTable"; -import { PackageImportWizardProps } from "../PackageImportWizard"; -import Alert from "@material-ui/lab/Alert/Alert"; -import { makeStyles, Theme } from "@material-ui/core"; - -const models = [ - GlobalCategoryModel, - GlobalCategoryComboModel, - GlobalCategoryOptionModel, - GlobalCategoryOptionGroupModel, - GlobalCategoryOptionGroupSetModel, - AggregatedDataElementModel, - GlobalOptionModel, - IndicatorMappedModel, - OrganisationUnitMappedModel, -]; - -export const PackageMappingStep: React.FC = ({ - packageImportRule, - onChange, -}) => { - const classes = useStyles(); - const { compositionRoot, api } = useAppContext(); - const snackbar = useSnackbar(); - - const [globalAdmin, setGlobalAdmin] = useState(false); - const [packages, setPackages] = useState([]); - const [instance, setInstance] = useState(); - - const [packageFilter, setPackageFilter] = useState(packageImportRule.packageIds[0]); - const [dataSourceMapping, setDataSourceMapping] = useState(); - const [packageContents, setPackageContents] = useState(); - const [mappingMessage, setMappingMessage] = useState(""); - - const onChangeMapping = useCallback( - async (metadataMapping: MetadataMappingDictionary) => { - if (!dataSourceMapping) { - snackbar.error(i18n.t("Attempting to update mapping without a valid data source")); - return; - } - - const newMapping = dataSourceMapping.updateMappingDictionary(metadataMapping); - - 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); - }, - }); - } else { - onChange(packageImportRule.addOrUpdateTemporalPackageMapping(newMapping)); - } - }, - [compositionRoot, dataSourceMapping, snackbar, packageImportRule, onChange] - ); - - const onApplyGlobalMapping = useCallback( - async (type: string, id: string, subMapping: MetadataMapping) => { - if (!dataSourceMapping) return; - const newMapping = _.clone(dataSourceMapping.mappingDictionary); - _.set(newMapping, [type, id], { ...subMapping, global: true }); - await onChangeMapping(newMapping); - }, - [dataSourceMapping, onChangeMapping] - ); - - const packageFilterComponent = ( - - ); - - const updateDataSource = useCallback( - async (source: PackageSource, packageId: string) => { - if (isInstance(source)) { - const mapping = await compositionRoot.mapping.get({ - type: "instance", - id: source.id, - }); - - const packageResult = await compositionRoot.packages.get(packageId, source); - - await packageResult.match({ - error: async () => { - snackbar.error(i18n.t("Unknown error happened loading package")); - }, - success: async ({ dhisVersion, module, contents }) => { - setPackageContents(contents); - - const fullModule = await compositionRoot.modules.get(module.id, source); - - if (fullModule) { - if (fullModule.autogenerated) { - 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)); - } else { - setDataSourceMapping(mapping); - setInstance(source); - } - } else { - snackbar.error(i18n.t("Unknown error happened loading module")); - } - }, - }); - } else { - const result = await compositionRoot.packages.getStore(source.id, packageId); - - await result.match({ - error: async () => { - snackbar.error(i18n.t("Unknown error happened loading store")); - }, - success: async ({ dhisVersion, contents, module }) => { - const owner = { - type: "store" as const, - id: source.id, - moduleId: module.id, - }; - - const mapping = await compositionRoot.mapping.get(owner); - const defaultMapping = DataSourceMapping.build({ - owner, - mappingDictionary: {}, - }); - - setPackageContents(contents); - setDataSourceMapping(mapping ?? defaultMapping); - setInstance(JSONDataSource.build(dhisVersion, contents)); - }, - }); - } - }, - [compositionRoot, snackbar, packageImportRule] - ); - - useEffect(() => { - updateDataSource(packageImportRule.source, packageFilter); - }, [updateDataSource, packageFilter, packageImportRule.source]); - - useEffect(() => { - if (packageContents && dataSourceMapping) { - const mapeableModels = models.map(model => model.getCollectionName()); - - const contentsIds: string[] = Object.entries(packageContents).reduce( - (acc: string[], [key, items]) => { - const modelKey = key as keyof MetadataEntities; - - const ids: string[] = - mapeableModels.includes(modelKey) && items - ? items.map(item => item.id) - : []; - return [...acc, ...ids]; - }, - [] - ); - - const mappingIds: string[] = Object.entries(dataSourceMapping.mappingDictionary).reduce( - (acc: string[], [_, mapping]) => [...acc, ...Object.keys(mapping)], - [] - ); - - const noMappedIds = _.difference(contentsIds, mappingIds); - - const message = - 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( - "Some elements have been already mapped previously, please continue mapping remaining one or changed previous mapping" - ) - : i18n.t("No mapping found"); - - setMappingMessage(message); - } - }, [packageContents, dataSourceMapping]); - - useEffect(() => { - isGlobalAdmin(api).then(setGlobalAdmin); - }, [api]); - - useEffect(() => { - if (isInstance(packageImportRule.source)) { - compositionRoot.packages - .list(globalAdmin, packageImportRule.source) - .then(packages => { - const importPackages = packages.filter(pkg => - packageImportRule.packageIds.includes(pkg.id) - ); - - setPackages(importPackages); - }) - .catch((error: Error) => { - snackbar.error(error.message); - setPackages([]); - }); - } else { - compositionRoot.packages.listStore(packageImportRule.source.id).then(result => { - result.match({ - success: packages => { - const importPackages = packages.filter(pkg => - packageImportRule.packageIds.includes(pkg.id) - ); - - setPackages(importPackages); - }, - error: error => { - snackbar.error(error); - setPackages([]); - }, - }); - }); - } - }, [compositionRoot, packageImportRule, globalAdmin, snackbar]); - - if (!dataSourceMapping || !instance) return null; - - return ( - - {mappingMessage && ( - - {mappingMessage} - - )} - - - ); -}; - -const useStyles = makeStyles((theme: Theme) => ({ - alert: { - textAlign: "center", - margin: theme.spacing(2), - display: "flex", - justifyContent: "center", - }, -})); diff --git a/src/presentation/react/components/package-import-wizard/steps/PackageSelectionStep.tsx b/src/presentation/react/components/package-import-wizard/steps/PackageSelectionStep.tsx deleted file mode 100644 index f1801a3d8..000000000 --- a/src/presentation/react/components/package-import-wizard/steps/PackageSelectionStep.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { PaginationOptions } from "d2-ui-components"; -import React from "react"; -import { isInstance, isStore } from "../../../../../domain/package-import/entities/PackageSource"; -import { PackagesListTable } from "../../package-list-table/PackageListTable"; -import { PackageImportWizardProps } from "../PackageImportWizard"; - -export const PackageSelectionStep: React.FC = ({ - packageImportRule, - onChange, -}) => { - const handleSelectionChange = (ids: string[]) => { - onChange(packageImportRule.updatePackageIds(ids)); - }; - - return ( - - ); -}; - -const paginationOptions: PaginationOptions = { - pageSizeOptions: [10], - pageSizeInitialValue: 10, -}; diff --git a/src/presentation/react/components/package-import-wizard/steps/SummaryStep.tsx b/src/presentation/react/components/package-import-wizard/steps/SummaryStep.tsx deleted file mode 100644 index 899774af4..000000000 --- a/src/presentation/react/components/package-import-wizard/steps/SummaryStep.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { ReactNode, useEffect, useState } from "react"; -import { isInstance } from "../../../../../domain/package-import/entities/PackageSource"; -import { ListPackage } from "../../../../../domain/packages/entities/Package"; -import i18n from "../../../../../locales"; -import { isGlobalAdmin } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../contexts/AppContext"; -import { PackageImportWizardProps } from "../PackageImportWizard"; - -export const SummaryStep: React.FC = ({ packageImportRule }) => { - const { api, compositionRoot } = useAppContext(); - const snackbar = useSnackbar(); - - const getPackagesFromInstance = compositionRoot.packages.list; - const getPackagesFromStore = compositionRoot.packages.listStore; - - const [globalAdmin, setGlobalAdmin] = useState(false); - const [packages, setPackages] = useState([]); - - useEffect(() => { - isGlobalAdmin(api).then(setGlobalAdmin); - }, [api]); - - useEffect(() => { - if (isInstance(packageImportRule.source)) { - getPackagesFromInstance(globalAdmin, packageImportRule.source) - .then(setPackages) - .catch((error: Error) => { - snackbar.error(error.message); - setPackages([]); - }); - } else { - getPackagesFromStore(packageImportRule.source.id).then(result => { - result.match({ - success: setPackages, - error: () => { - snackbar.error(i18n.t("Can't connect to store")); - setPackages([]); - }, - }); - }); - } - }, [getPackagesFromInstance, getPackagesFromStore, packageImportRule, globalAdmin, snackbar]); - - return ( - -
      - - -
        - {packages.length === 0 ? ( - - ) : ( - packageImportRule.packageIds.map(id => { - const instancePackage = packages.find(pkg => pkg.id === id); - return ; - }) - )} -
      -
      -
    -
    - ); -}; - -interface Entry { - label: string; - value?: string | number; - children?: ReactNode; - hide?: boolean; -} - -const LiEntry = ({ label, value, children, hide = false }: Entry) => { - if (hide) return null; - - return ( -
  • - {_.compact([label, value]).join(": ")} - {children} -
  • - ); -}; diff --git a/src/presentation/react/components/package-list-table/PackageListTable.tsx b/src/presentation/react/components/package-list-table/PackageListTable.tsx deleted file mode 100644 index 665a02bdc..000000000 --- a/src/presentation/react/components/package-list-table/PackageListTable.tsx +++ /dev/null @@ -1,936 +0,0 @@ -import { Icon } from "@material-ui/core"; -import { - ConfirmationDialog, - ConfirmationDialogProps, - ObjectsTable, - ObjectsTableDetailField, - RowConfig, - TableAction, - TableColumn, - TableSelection, - TableState, - useLoading, - useSnackbar, -} from "d2-ui-components"; -import _ from "lodash"; -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 { JSONDataSource } from "../../../../domain/instance/entities/JSONDataSource"; -import { Module } from "../../../../domain/modules/entities/Module"; -import { ImportedPackage } from "../../../../domain/package-import/entities/ImportedPackage"; -import { - isInstance, - isStore, - PackageSource, -} from "../../../../domain/package-import/entities/PackageSource"; -import { mapToImportedPackage } from "../../../../domain/package-import/mappers/ImportedPackageMapper"; -import { ListPackage, Package } from "../../../../domain/packages/entities/Package"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { isAppConfigurator, isGlobalAdmin } from "../../../../utils/permissions"; -import { ModulePackageListPageProps } from "../../../webapp/core/pages/module-package-list/ModulePackageListPage"; -import { useAppContext } from "../../contexts/AppContext"; -import Dropdown from "../dropdown/Dropdown"; -import PackageImportDialog from "../package-import-dialog/PackageImportDialog"; -import { DiffPackages, PackagesDiffDialog } from "../packages-diff-dialog/PackagesDiffDialog"; -import { - groupPackageByModuleAndVersion as groupPackagesByModuleAndVersion, - InstallStatus, - isPackageItem, - PackageItem, - PackageModuleItem, -} from "./PackageModuleItem"; - -interface PackagesListTableProps extends ModulePackageListPageProps { - isImportDialog?: boolean; - onSelectionChange?: (ids: string[]) => void; - selectedIds?: string[]; -} - -export const PackagesListTable: React.FC = ({ - remoteInstance, - remoteStore, - onActionButtonClick, - presentation = "app", - externalComponents, - openSyncSummary = _.noop, - paginationOptions, - isImportDialog = false, - onSelectionChange, - selectedIds, - actionButtonLabel, -}) => { - const { api, compositionRoot } = useAppContext(); - const snackbar = useSnackbar(); - const loading = useLoading(); - - 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()); - const [stateSelection, updateStateSelection] = useState([]); - const selection = selectedIds?.map(id => ({ id })) ?? stateSelection; - - const [dialogProps, updateDialog] = useState(null); - const [packagesToDiff, setPackagesToDiff] = useState(null); - const [moduleFilter, setModuleFilter] = useState(""); - const [dhis2VersionFilter, setDhis2VersionFilter] = useState(""); - const [localDhis2Version, setLocalDhis2Version] = useState(""); - const [installStatusFilter, setInstallStatusFilter] = useState(""); - - const [globalAdmin, setGlobalAdmin] = useState(false); - const [appConfigurator, setAppConfigurator] = useState(false); - const [loadingTable, setLoadingTable] = useState(true); - const [openImportPackageDialog, setOpenImportPackageDialog] = useState(false); - - const [toImportWizard, setToImportWizard] = useState([]); - - const isRemoteInstance = !!remoteInstance; - - useEffect(() => { - compositionRoot.modules.list(globalAdmin, remoteInstance, true).then(setModules); - }, [compositionRoot, globalAdmin, remoteInstance]); - - const updateSelection = useCallback( - (selection: TableSelection[]) => { - updateStateSelection(selection); - - if (onSelectionChange) { - onSelectionChange(selection.map(selection => selection.id)); - } - }, - [onSelectionChange] - ); - - const deletePackages = useCallback( - async (ids: string[]) => { - loading.show(true, "Deleting packages"); - for (const id of ids) { - await compositionRoot.packages.delete(id); - } - loading.reset(); - setResetKey(Math.random()); - updateSelection([]); - }, - [compositionRoot, loading, updateSelection] - ); - - const updateTable = useCallback( - ({ selection }: TableState) => { - updateSelection(selection); - }, - [updateSelection] - ); - - const downloadPackage = useCallback( - async (ids: string[]) => { - try { - compositionRoot.packages.download(remoteStore?.id, ids[0], remoteInstance); - } catch (error) { - snackbar.error(i18n.t("Invalid package")); - } - }, - [compositionRoot, remoteInstance, snackbar, remoteStore] - ); - - const publishPackage = useCallback( - async (ids: string[]) => { - loading.show(true, i18n.t("Publishing package to Store")); - const validation = await compositionRoot.packages.publish(ids[0]); - validation.match({ - success: () => { - loading.reset(); - snackbar.success(i18n.t("Package published to default store")); - }, - error: code => { - loading.reset(); - switch (code) { - case "BAD_CREDENTIALS": - case "NO_TOKEN": - case "DEFAULT_STORE_NOT_FOUND": - snackbar.error(i18n.t("Default store is not properly configured")); - return; - case "PACKAGE_NOT_FOUND": - snackbar.error(i18n.t("Could not read package")); - return; - case "WRITE_PERMISSIONS": - snackbar.error( - i18n.t("You don't have permissions to create file on GitHub") - ); - return; - case "UNKNOWN": - snackbar.error(i18n.t("Unknown error while creating file on GitHub")); - return; - case "ALREADY_PUBLISHED": - snackbar.warning(i18n.t("Package already published")); - return; - case "BRANCH_NOT_FOUND": - updateDialog({ - title: i18n.t("Branch not found"), - description: i18n.t( - "There are no branches for the department of this module. Do you want to create a new branch for this department?" - ), - onCancel: () => { - updateDialog(null); - }, - onSave: async () => { - updateDialog(null); - loading.show(true, i18n.t("Publishing package to Store")); - const validation = await compositionRoot.packages.publish( - ids[0], - true - ); - validation.match({ - success: () => - snackbar.success( - i18n.t("Package published to store in a new branch") - ), - error: () => - snackbar.error( - i18n.t("Couldn't create new branch on store") - ), - }); - loading.reset(); - }, - cancelText: i18n.t("Cancel"), - saveText: i18n.t("Proceed"), - }); - return; - default: - snackbar.error(i18n.t("Unknown error")); - } - }, - }); - }, - [compositionRoot, snackbar, loading] - ); - - const openPackageDiffDialog = useCallback( - async (ids: string[]) => { - const packageId = _(ids).get(0, null); - const remotePackage = packageId ? rows.find(row => row.id === packageId) : undefined; - if (packageId && remotePackage && isPackageItem(remotePackage)) { - setPackagesToDiff({ merge: remotePackage }); - } - }, - [rows, setPackagesToDiff] - ); - - const openPairPackageDiffDialog = useCallback( - async (ids: string[]) => { - const [packageBase, packageMerge] = ids.map(packageId => { - return rows.find(row => row.id === packageId); - }); - if ( - packageBase && - packageMerge && - isPackageItem(packageBase) && - isPackageItem(packageMerge) - ) { - setPackagesToDiff({ base: packageBase, merge: packageMerge }); - } - }, - [rows, setPackagesToDiff] - ); - - const closePackageDiffDialog = useCallback(() => setPackagesToDiff(null), [setPackagesToDiff]); - - const saveImportedPackage = useCallback( - async ( - pkg: Package, - author: NamedRef, - packageSource: PackageSource, - storePackageUrl?: string - ) => { - const importedPackage = mapToImportedPackage( - pkg, - author, - packageSource, - storePackageUrl - ); - - const result = await compositionRoot.importedPackages.save([importedPackage]); - - result.match({ - success: () => {}, - error: () => { - snackbar.error("An error has ocurred tracking the imported package"); - }, - }); - }, - [compositionRoot, snackbar] - ); - - const getPackage = useCallback( - ( - packageSource: PackageSource, - packageId: string - ): Promise> => { - if (isInstance(packageSource)) { - return compositionRoot.packages.get(packageId, packageSource); - } else { - return compositionRoot.packages.getStore(packageSource.id, packageId); - } - }, - [compositionRoot] - ); - - const getPackageSourceToImport = useCallback(() => { - if (remoteInstance) { - return remoteInstance; - } else if (remoteStore) { - return remoteStore; - } else { - throw new Error("The import action is only available for remote package source"); - } - }, [remoteInstance, remoteStore]); - - const importPackagesFromWizard = useCallback((ids: string[]) => { - setToImportWizard(ids); - 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(); - - const result = await getPackage(packageSource, ids[0]); - - 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 }) - ); - - const mapping = await compositionRoot.mapping.get({ - type: isInstance(packageSource) ? "instance" : "store", - id: packageSource.id, - moduleId: originPackage.module.id, - }); - - const originDataSource = - remoteInstance && isInstance(packageSource) - ? remoteInstance - : JSONDataSource.build( - originPackage.dhisVersion, - originPackage.contents - ); - - const result = await compositionRoot.packages.import( - originPackage, - mapping?.mappingDictionary, - originDataSource - ); - - 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" - : "DONE" - ); - - report.addSyncResult({ - ...result, - originPackage: originPackage.toRef(), - origin: remoteInstance?.toPublicObject(), - }); - await report.save(api); - - if (result.status === "SUCCESS") { - const author = { - id: currentUser.id, - name: currentUser.userCredentials.username, - }; - - await saveImportedPackage( - originPackage, - author, - packageSource, - isStore(packageSource) ? ids[0] : undefined - ); - } - - openSyncSummary(report); - setResetKey(Math.random()); - } catch (error) { - snackbar.error(error.message); - } - loading.reset(); - }, - error: async () => { - snackbar.error(i18n.t("Couldn't load package")); - }, - }); - }, - [ - compositionRoot, - api, - loading, - remoteInstance, - snackbar, - openSyncSummary, - getPackage, - getPackageSourceToImport, - saveImportedPackage, - ] - ); - - const getInstallStatusText = (installStatus: InstallStatus): string => { - switch (installStatus) { - case "Installed": - return i18n.t("Installed"); - case "NotInstalled": - 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)"); - } - }; - - 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: "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: PackageModuleItem) => - isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, - }, - ], - [] - ); - - const details: ObjectsTableDetailField[] = useMemo( - () => [ - { name: "id", text: i18n.t("ID") }, - { name: "name", text: i18n.t("Name") }, - { name: "description", text: i18n.t("Description") }, - { name: "version", text: i18n.t("Version") }, - { name: "dhisVersion", text: i18n.t("DHIS2 Version") }, - { name: "module", text: i18n.t("Module") }, - { name: "created", text: i18n.t("Created") }, - { name: "user", text: i18n.t("Created by") }, - { - name: "installStatus", - text: i18n.t("Status"), - getValue: (row: PackageModuleItem) => - isPackageItem(row) ? getInstallStatusText(row.installStatus) : undefined, - }, - ], - [] - ); - - const actions: TableAction[] = useMemo( - () => [ - { - name: "details", - text: i18n.t("Details"), - multiple: false, - primary: true, - isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), - }, - { - name: "delete", - text: i18n.t("Delete"), - multiple: true, - onClick: deletePackages, - icon: delete, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - !isImportDialog && - presentation === "app" && - !isRemoteInstance && - !remoteStore && - appConfigurator, - }, - { - name: "download", - text: i18n.t("Download as JSON"), - multiple: false, - onClick: downloadPackage, - icon: cloud_download, - isActive: (rows: PackageModuleItem[]) => _.every(rows, row => isPackageItem(row)), - }, - { - name: "publish", - text: i18n.t("Publish to Store"), - multiple: false, - onClick: publishPackage, - icon: publish, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - !isImportDialog && - presentation === "app" && - !isRemoteInstance && - !remoteStore && - appConfigurator, - }, - { - name: "compare-with-local", - text: i18n.t("Compare with local instance"), - multiple: false, - icon: compare, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - presentation === "app" && - (isRemoteInstance || remoteStore !== undefined) && - appConfigurator, - onClick: openPackageDiffDialog, - }, - { - name: "compare-selected-packages", - text: i18n.t("Compare selected packages"), - multiple: true, - icon: compare_arrows, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - presentation === "app" && - appConfigurator && - (selectedIds ? selectedIds.length === 2 : false), - onClick: openPairPackageDiffDialog, - }, - { - name: "import", - text: i18n.t("Import package"), - multiple: false, - onClick: importPackage, - icon: arrow_downward, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - !isImportDialog && - presentation === "app" && - (isRemoteInstance || remoteStore !== undefined) && - appConfigurator, - }, - { - name: "importFromWizard", - text: i18n.t("Import package (wizard)"), - multiple: true, - onClick: importPackagesFromWizard, - icon: arrow_downward, - isActive: (rows: PackageModuleItem[]) => - _.every(rows, row => isPackageItem(row)) && - !isImportDialog && - presentation === "app" && - (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, - deletePackages, - downloadPackage, - importPackage, - importPackagesFromWizard, - isRemoteInstance, - openPackageDiffDialog, - openPairPackageDiffDialog, - presentation, - publishPackage, - remoteStore, - isImportDialog, - selectedIds, - generateModule, - modules, - ] - ); - - const moduleFilterItems = useMemo(() => { - const packages = remoteStore ? storePackages : instancePackages; - - return _(packages) - .map(pkg => pkg.module) - .uniqBy(({ id }) => id) - .sortBy(({ name }) => name) - .value(); - }, [instancePackages, storePackages, remoteStore]); - - const dhis2VersionFilterItems = useMemo(() => { - const packages = remoteStore ? storePackages : instancePackages; - - return _(packages) - .map(pkg => ({ - id: pkg.dhisVersion, - name: - localDhis2Version === pkg.dhisVersion - ? pkg.dhisVersion - : `${pkg.dhisVersion} (${i18n.t("Not recommended")})`, - })) - .uniqBy(({ id }) => id) - .sortBy(({ name }) => name) - .value(); - }, [instancePackages, storePackages, remoteStore, localDhis2Version]); - - const installStatusFilterItems = useMemo(() => { - const packages = remoteStore ? storePackages : instancePackages; - - return _(packages) - .map(pkg => ({ - id: pkg.installStatus, - name: getInstallStatusText(pkg.installStatus), - })) - .uniqBy(({ id }) => id) - .sortBy(({ name }) => name) - .value(); - }, [instancePackages, storePackages, remoteStore]); - - const filterComponents = useMemo(() => { - const updateFilter = (fn: Function) => (...args: unknown[]) => { - fn(...args); - setResetKey(Math.random()); - }; - - const moduleFilterComponent = ( - - ); - - const dhis2VersionFilterComponent = ( - - ); - - const installStateFilterComponent = ( - - ); - return [ - externalComponents, - moduleFilterComponent, - dhis2VersionFilterComponent, - installStateFilterComponent, - ]; - }, [ - externalComponents, - moduleFilter, - moduleFilterItems, - dhis2VersionFilterItems, - dhis2VersionFilter, - installStatusFilterItems, - installStatusFilter, - ]); - - const rowsFiltered = useMemo(() => { - setLoadingTable(false); - - 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) => { - setOpenImportPackageDialog(false); - setToImportWizard([]); - openSyncSummary(syncReport); - setResetKey(Math.random()); - }; - - const handleCloseImportWizard = () => { - setOpenImportPackageDialog(false); - setToImportWizard([]); - }; - - const showImportFromWizardButton = !isImportDialog && presentation === "app" && appConfigurator; - - const packageSource = remoteInstance ?? remoteStore; - - useEffect(() => { - api.getVersion().then(setLocalDhis2Version); - }, [api]); - - useEffect(() => { - setLoadingTable(true); - compositionRoot.packages - .list(globalAdmin, remoteInstance) - .then(packages => { - setInstancePackages( - mapPackagesToPackageItems(modules, packages, importedPackages, packageSource) - ); - }) - .catch((error: Error) => { - snackbar.error(error.message); - setInstancePackages([]); - }); - }, [ - compositionRoot, - remoteInstance, - resetKey, - snackbar, - globalAdmin, - importedPackages, - remoteStore, - modules, - packageSource, - ]); - - useEffect(() => { - if (remoteStore) { - setLoadingTable(true); - compositionRoot.packages.listStore(remoteStore.id).then(validation => { - validation.match({ - success: packages => { - setStorePackages( - mapPackagesToPackageItems( - modules, - packages, - importedPackages, - packageSource - ) - ); - }, - error: () => { - snackbar.error(i18n.t("Can't connect to store")); - setStorePackages([]); - }, - }); - }); - } else { - setStorePackages([]); - } - }, [ - compositionRoot, - snackbar, - remoteStore, - importedPackages, - remoteInstance, - resetKey, - modules, - packageSource, - ]); - - useEffect(() => { - compositionRoot.importedPackages.list().then(result => - result.match({ - success: setImportedPackages, - error: () => { - snackbar.error(i18n.t("An error has ocurred retrieving imported packages")); - setImportedPackages([]); - }, - }) - ); - }, [compositionRoot, snackbar, resetKey]); - - useEffect(() => { - setModuleFilter(""); - setDhis2VersionFilter(""); - setInstallStatusFilter(""); - setResetKey(Math.random()); - }, [remoteInstance, remoteStore]); - - useEffect(() => { - isAppConfigurator(api).then(setAppConfigurator); - 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} - onActionButtonClick={showImportFromWizardButton ? onActionButtonClick : undefined} - forceSelectionColumn={presentation === "app"} - filterComponents={filterComponents} - selection={selection} - onChange={updateTable} - paginationOptions={paginationOptions} - actionButtonLabel={actionButtonLabel} - loading={loadingTable} - childrenKeys={["packages"]} - /> - - {dialogProps && } - - {packagesToDiff && ( - - )} - - {packageSource && ( - - )} - - ); -}; - -function mapPackagesToPackageItems( - modules: Module[], - packages: ListPackage[], - importedPackages: ImportedPackage[], - packageSource?: PackageSource -): PackageItem[] { - 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 installStatus: InstallStatus = installed - ? "Installed" - : newUpdates - ? "Upgrade" - : "NotInstalled"; - - return { ...pkg, installStatus }; - }); - - return listPackages; - } else { - const listPackages = packages.map(pkg => { - const isPackageImported = verifyIfPackageIsImported(pkg); - - 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; - } -} diff --git a/src/presentation/react/components/package-list-table/PackageModuleItem.ts b/src/presentation/react/components/package-list-table/PackageModuleItem.ts deleted file mode 100644 index d5864086e..000000000 --- a/src/presentation/react/components/package-list-table/PackageModuleItem.ts +++ /dev/null @@ -1,47 +0,0 @@ -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" - | "InstalledLocalPackage" - | "NotInstalledLocalPackage"; -export type PackageItem = Omit & { installStatus: InstallStatus }; - -export const isPackageItem = (item: PackageModuleItem): item is PackageItem => { - return (item as PackageItem).module !== undefined; -}; - -export const groupPackageByModuleAndVersion = (packages: PackageItem[]) => { - return packages.reduce((acc, item) => { - const parentKey = `${item.module.id}-${item.version}`; - - const parent = acc.find(parent => parent.id === parentKey); - - if (parent) { - return acc.map(parentItem => - parentItem.id === parentKey - ? { ...parentItem, packages: [...parentItem.packages, item] } - : parentItem - ); - } else { - const newParent = { - id: parentKey, - name: item.module.name, - version: item.version, - packages: [item], - }; - return [...acc, newParent]; - } - }, [] as ModuleItem[]); -}; diff --git a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx b/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx deleted file mode 100644 index 3ccb57956..000000000 --- a/src/presentation/react/components/packages-diff-dialog/PackagesDiffDialog.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { LinearProgress } from "@material-ui/core"; -import { makeStyles } from "@material-ui/styles"; -import { useSnackbar } from "d2-ui-components"; -import { ConfirmationDialog } from "d2-ui-components/confirmation-dialog/ConfirmationDialog"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { - MetadataPackageDiff, - ModelDiff, -} from "../../../../domain/packages/entities/MetadataPackageDiff"; -import { Store } from "../../../../domain/stores/entities/Store"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../contexts/AppContext"; -import SyncSummary from "../sync-summary/SyncSummary"; -import { getChange, getTitle, usePackageImporter } from "./utils"; - -export interface PackagesDiffDialogProps { - onClose(): void; - remoteInstance?: Instance; - remoteStore?: Store; - packages: DiffPackages; -} - -export interface DiffPackages { - base?: PackageToDiff; - merge: PackageToDiff; -} - -export type PackageToDiff = { id: string; name: string; version: string }; - -export const PackagesDiffDialog: React.FC = props => { - const { compositionRoot } = useAppContext(); - const snackbar = useSnackbar(); - const [metadataDiff, setMetadataDiff] = useState(); - const { packages, remoteStore, remoteInstance, onClose } = props; - const { base: packageBase, merge: packageMerge } = packages; - const showImportButton = !packageBase; - - useEffect(() => { - compositionRoot.packages - .diff(packageBase?.id, packageMerge.id, remoteStore?.id, remoteInstance) - .then(res => { - res.match({ - error: msg => { - snackbar.error(i18n.t("Cannot get data from remote instance") + ": " + msg); - onClose(); - }, - success: setMetadataDiff, - }); - }); - }, [ - compositionRoot, - packageBase, - packageMerge, - remoteStore, - remoteInstance, - onClose, - snackbar, - ]); - - const hasChanges = metadataDiff && metadataDiff.hasChanges; - const packageName = `${packageMerge.name} (${remoteInstance?.name ?? "Store"})`; - const { importPackage, syncReport, closeSyncReport } = usePackageImporter( - remoteInstance, - packageName, - metadataDiff, - onClose - ); - - return ( - - - {metadataDiff ? ( - - ) : ( - - )} - - - {!!syncReport && } - - ); -}; - -export const MetadataDiffTable: React.FC<{ - metadataDiff: MetadataPackageDiff["changes"]; -}> = props => { - const { metadataDiff } = props; - const classes = useStyles(); - - return ( -
      - {_.map(metadataDiff, (modelDiff, model) => ( -
    • -

      {model}

      : {modelDiff.total}{" "} - {i18n.t("objects")} ({i18n.t("Unmodified")}: {modelDiff.unmodified.length},{" "} - {i18n.t("New")}: {modelDiff.created.length}, {i18n.t("Updated")}:{" "} - {modelDiff.updates.length}) - -
    • - ))} -
    - ); -}; - -export const ModelDiffList: React.FC<{ modelDiff: ModelDiff }> = props => { - const { modelDiff: diff } = props; - const classes = useStyles(); - - return ( -
      - {diff.created.length > 0 && ( -
    • - - {i18n.t("New")}: {diff.created.length} - - - `${obj.name} (${obj.id})`)} /> -
    • - )} - - {diff.updates.length > 0 && ( -
    • - - {i18n.t("Updated")}: {diff.updates.length} - - - ( - - [{update.obj.id}] {update.obj.name} - - - ))} - /> -
    • - )} -
    - ); -}; - -export const List: React.FC<{ items: React.ReactNode[] }> = props => { - const { items } = props; - return ( -
      - {items.map((item, idx) => ( -
    • {item}
    • - ))} -
    - ); -}; - -const useStyles = makeStyles({ - modelTitle: { display: "inline" }, - added: { color: "green" }, - updated: { color: "orange" }, -}); diff --git a/src/presentation/react/components/packages-diff-dialog/utils.tsx b/src/presentation/react/components/packages-diff-dialog/utils.tsx deleted file mode 100644 index 55ea17154..000000000 --- a/src/presentation/react/components/packages-diff-dialog/utils.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useLoading, useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import { useCallback, useState } from "react"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { - FieldUpdate, - MetadataPackageDiff, -} from "../../../../domain/packages/entities/MetadataPackageDiff"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { useAppContext } from "../../contexts/AppContext"; -import { PackageToDiff } from "./PackagesDiffDialog"; - -export function getChange(u: FieldUpdate): string { - return `${u.field}: ${truncate(u.oldValue)} -> ${truncate(u.newValue)}`; -} - -function truncate(s: string) { - return _.truncate(s, { length: 50 }); -} - -export function getTitle( - packageBase: PackageToDiff | undefined, - packageMerge: PackageToDiff | undefined, - metadataDiff: MetadataPackageDiff | undefined -) { - let prefix: string; - if (!metadataDiff) { - prefix = i18n.t("Comparing package contents"); - } else if (metadataDiff.hasChanges) { - prefix = i18n.t("Changes found"); - } else { - prefix = i18n.t("No changes found"); - } - const info = [packageBase, packageMerge] - .map(package_ => (package_ ? `${package_.name} (${package_.version})` : i18n.t("Local"))) - .join(" - > "); - - return `${prefix}: ${info}`; -} - -export function usePackageImporter( - instance: Instance | undefined, - packageName: string, - metadataDiff: MetadataPackageDiff | undefined, - onClose: () => void -) { - const { compositionRoot, api } = useAppContext(); - const loading = useLoading(); - const snackbar = useSnackbar(); - const [syncReport, setSyncReport] = useState(); - - const closeSyncReport = useCallback(() => { - setSyncReport(undefined); - onClose(); - }, [setSyncReport, onClose]); - - const importPackage = useCallback(() => { - async function performImport() { - if (!metadataDiff) return; - loading.show(true, i18n.t("Importing package {{name}}", { name: packageName })); - - const result = await compositionRoot.metadata.import(metadataDiff.mergeableMetadata); - const report = SyncReport.create("metadata"); - report.setStatus( - result.status === "ERROR" || result.status === "NETWORK ERROR" ? "FAILURE" : "DONE" - ); - report.addSyncResult({ ...result, origin: instance?.toPublicObject() }); - await report.save(api); - - setSyncReport(report); - } - - performImport() - .catch(err => snackbar.error(err.message)) - .finally(() => loading.reset()); - }, [packageName, metadataDiff, compositionRoot, loading, snackbar, api, instance]); - - return { importPackage, syncReport, closeSyncReport }; -} diff --git a/src/presentation/react/components/page-header/PageHeader.tsx b/src/presentation/react/components/page-header/PageHeader.tsx deleted file mode 100644 index 9b6bd0d9c..000000000 --- a/src/presentation/react/components/page-header/PageHeader.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { ButtonProps, Icon, IconButton, Tooltip } from "@material-ui/core"; -import { Variant } from "@material-ui/core/styles/createTypography"; -import Typography from "@material-ui/core/Typography"; -import { DialogButton } from "d2-ui-components"; -import React, { ReactNode } from "react"; -import i18n from "../../../../locales"; - -const PageHeader: React.FC = ({ - variant = "h5", - title, - onBackClick, - help, - helpSize = "sm", - children, -}) => { - return ( -
    - {!!onBackClick && ( - - arrow_back - - )} - - - {title} - - {help && ( - - )} - {children} -
    - ); -}; - -export interface PageHeaderProps { - variant?: Variant; - title: string; - onBackClick?: () => void; - help?: ReactNode; - helpSize?: "xs" | "sm" | "md" | "lg" | "xl"; -} - -const styles = { - backArrow: { paddingTop: 10, marginBottom: 5 }, - help: { marginBottom: 8 }, - text: { display: "inline-block", fontWeight: 300 }, -}; - -const Button = ({ onClick }: ButtonProps) => ( - - - help - - -); - -export default PageHeader; diff --git a/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx deleted file mode 100644 index 545828fb1..000000000 --- a/src/presentation/react/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { Box, makeStyles, Theme } from "@material-ui/core"; -import { ConfirmationDialog } from "d2-ui-components"; -import React, { useState } from "react"; -import i18n from "../../../../locales"; -import { PeriodFilter } from "../../../webapp/msf-aggregate-data/pages/MSFHomePage"; -import PeriodSelection from "../period-selection/PeriodSelection"; - -export interface PeriodSelectionDialogProps { - title?: string; - period: PeriodFilter; - onClose(): void; - onSave(value: PeriodFilter): void; -} - -export const PeriodSelectionDialog: React.FC = ({ - title, - onClose, - onSave, - period, -}) => { - const classes = useStyles(); - const [periodState, setPeriodState] = useState(period); - - return ( - onSave(periodState)} - cancelText={i18n.t("Cancel")} - saveText={i18n.t("Save")} - > - - - - - ); -}; - -const useStyles = makeStyles((theme: Theme) => ({ - periodContainer: { - margin: "0 auto", - }, - periodContent: { - margin: theme.spacing(2), - }, -})); diff --git a/src/presentation/react/components/period-selection/PeriodSelection.tsx b/src/presentation/react/components/period-selection/PeriodSelection.tsx deleted file mode 100644 index 12900bf4c..000000000 --- a/src/presentation/react/components/period-selection/PeriodSelection.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { DatePicker } from "d2-ui-components"; -import _ from "lodash"; -import moment, { Moment } from "moment"; -import React, { useCallback, useMemo } from "react"; -import { DataSyncPeriod } from "../../../../domain/aggregated/types"; -import i18n from "../../../../locales"; -import { Maybe } from "../../../../types/utils"; -import { availablePeriods, PeriodType } from "../../../../utils/synchronization"; -import Dropdown from "../dropdown/Dropdown"; - -export interface ObjectWithPeriodInput { - period: DataSyncPeriod; - startDate?: Date | string; - endDate?: Date | string; -} - -export interface ObjectWithPeriod { - period: DataSyncPeriod; - startDate?: Date; - endDate?: Date; -} - -export interface PeriodSelectionProps { - periodTitle?: string; - objectWithPeriod: ObjectWithPeriodInput; - onChange?: (obj: ObjectWithPeriod) => void; - onFieldChange?( - field: Field, - value: ObjectWithPeriod[Field] - ): void; - skipPeriods?: Set; - className?: string; -} - -export type OnChange = Required["onChange"]; -export type OnFieldChange = Required["onFieldChange"]; - -const useStyles = makeStyles({ - dropdown: { - marginTop: 20, - marginLeft: 0, - }, - fixedPeriod: { - marginTop: 5, - marginBottom: -20, - marginLeft: 10, - }, - datePicker: { - marginTop: -10, - }, -}); - -const PeriodSelection: React.FC = props => { - const { - objectWithPeriod: obj, - onChange = _.noop as OnChange, - onFieldChange = _.noop as OnFieldChange, - skipPeriods = new Set(), - periodTitle = i18n.t("Period"), - className, - } = props; - - const objectWithPeriod: ObjectWithPeriod = { - period: obj.period, - startDate: obj.startDate ? moment(obj.startDate).toDate() : undefined, - endDate: obj.endDate ? moment(obj.endDate).toDate() : undefined, - }; - const { period, startDate, endDate } = objectWithPeriod; - - const classes = useStyles(); - - const periodItems = useMemo( - () => - _(availablePeriods) - .mapValues((value, key) => ({ ...value, id: key })) - .values() - .filter(period => !skipPeriods.has(period.id as PeriodType)) - .value(), - [skipPeriods] - ); - - const updatePeriod = useCallback( - (period: ObjectWithPeriodInput["period"]) => { - onChange({ ...objectWithPeriod, period }); - onFieldChange("period", period); - }, - [objectWithPeriod, onChange, onFieldChange] - ); - - const updateStartDate = useCallback( - (startDateM: Maybe) => { - const startDate = startDateM?.toDate(); - onChange({ ...objectWithPeriod, startDate }); - onFieldChange("startDate", startDate); - }, - [objectWithPeriod, onChange, onFieldChange] - ); - - const updateEndDate = useCallback( - (endDateM: Maybe) => { - const endDate = endDateM?.toDate(); - onChange({ ...objectWithPeriod, endDate }); - onFieldChange("endDate", endDate); - }, - [objectWithPeriod, onChange, onFieldChange] - ); - - return ( -
    -
    - -
    - - {period === "FIXED" && ( -
    -
    - -
    -
    - -
    -
    - )} -
    - ); -}; - -export default PeriodSelection; diff --git a/src/presentation/react/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx b/src/presentation/react/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx deleted file mode 100644 index b25f2093a..000000000 --- a/src/presentation/react/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx +++ /dev/null @@ -1,207 +0,0 @@ -import { makeStyles, TextField } from "@material-ui/core"; -import { - ConfirmationDialog, - SearchResult, - ShareUpdate, - Sharing, - SharingRule, - useLoading, - useSnackbar, -} from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useState } from "react"; -import { NamedRef } from "../../../../domain/common/entities/Ref"; -import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; -import { useAppContext } from "../../contexts/AppContext"; - -export interface PullRequestCreation { - instance: Instance; - builder: SynchronizationBuilder; - type: SynchronizationType; -} - -export interface PullRequestCreationDialogProps extends PullRequestCreation { - onClose: () => void; -} - -interface PullRequestFields { - subject?: string; - description?: string; -} - -export const PullRequestCreationDialog: React.FC = ({ - instance, - type, - builder, - onClose, -}) => { - const { compositionRoot } = useAppContext(); - const classes = useStyles(); - const snackbar = useSnackbar(); - const loading = useLoading(); - - const [fields, updateFields] = useState({}); - const [responsibles, updateResponsibles] = useState>(); - const [notificationUsers, updateNotificationUsers] = useState<{ - users: SharingRule[]; - userGroups: SharingRule[]; - }>({ users: [], userGroups: [] }); - - const save = useCallback(async () => { - const { subject, description } = fields; - - if (!subject) { - snackbar.error(i18n.t("You need to provide a subject")); - return; - } - - try { - loading.show(true, i18n.t("Creating pull request")); - const sync = compositionRoot.sync[type](builder); - const payload = await sync.buildPayload(); - - await compositionRoot.sync.createPullRequest({ - instance, - type, - ids: builder.metadataIds, - payload, - subject, - description, - notificationUsers: { - users: sharingToNamedRef(notificationUsers.users), - userGroups: sharingToNamedRef(notificationUsers.userGroups), - }, - }); - - onClose(); - snackbar.success(i18n.t("Pull request created")); - } catch (err) { - snackbar.error(err.message); - } finally { - loading.reset(); - } - }, [ - compositionRoot, - builder, - fields, - type, - instance, - notificationUsers, - onClose, - snackbar, - loading, - ]); - - const updateTextField = useCallback( - (field: keyof PullRequestFields) => (event: React.ChangeEvent<{ value: unknown }>) => { - const value = event.target.value as string; - updateFields(fields => ({ ...fields, [field]: value })); - }, - [] - ); - - const onSearchRequest = useCallback( - async (key: string) => - compositionRoot.instances - .getApi(instance) - .get("/sharing/search", { key }) - .getData(), - [compositionRoot, instance] - ); - - const onSharingChanged = useCallback(async (updatedAttributes: ShareUpdate) => { - updateNotificationUsers(({ users, userGroups }) => { - const { userAccesses = users, userGroupAccesses = userGroups } = updatedAttributes; - return { users: userAccesses, userGroups: userGroupAccesses }; - }); - }, []); - - useEffect(() => { - compositionRoot.responsibles.get(builder.metadataIds, instance).then(responsibles => { - const users = _.uniqBy( - namedRefToSharing(responsibles.flatMap(({ users }) => users)), - "id" - ); - const userGroups = _.uniqBy( - namedRefToSharing(responsibles.flatMap(({ userGroups }) => userGroups)), - "id" - ); - - updateResponsibles(new Set([...users, ...userGroups].map(({ id }) => id))); - updateNotificationUsers({ users, userGroups }); - }); - }, [compositionRoot, builder, instance]); - - return ( - - - - - - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, -}); - -function namedRefToSharing(namedRefs: NamedRef[]): SharingRule[] { - return namedRefs.map(({ id, name }) => ({ id, displayName: name, access: "------" })); -} - -function sharingToNamedRef(sharings: SharingRule[]): NamedRef[] { - return sharings.map(({ id, displayName }) => ({ id, name: displayName })); -} diff --git a/src/presentation/react/components/radio-button-group/RadioButtonGroup.tsx b/src/presentation/react/components/radio-button-group/RadioButtonGroup.tsx deleted file mode 100644 index 2161ffc30..000000000 --- a/src/presentation/react/components/radio-button-group/RadioButtonGroup.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import FormControl from "@material-ui/core/FormControl"; -import FormControlLabel from "@material-ui/core/FormControlLabel"; -import FormLabel from "@material-ui/core/FormLabel"; -import Radio from "@material-ui/core/Radio"; -import RadioGroup from "@material-ui/core/RadioGroup"; -import _ from "lodash"; -import React from "react"; - -interface RadioButtonGroupProps { - items: { - id: string; - name: string; - disabled?: boolean; - }[]; - value: string; - defaultValue?: string; - onChange?: (event: React.ChangeEvent) => void; - onValueChange?: (value: string) => void; - title?: string; - horizontal?: boolean; -} - -export default function RadioButtonGroup({ - items, - value, - defaultValue, - onChange = _.noop, - onValueChange = _.noop, - title, - horizontal = true, -}: RadioButtonGroupProps) { - const handleChange = (event: React.ChangeEvent) => { - onChange(event); - onValueChange(event.target.value as string); - }; - - return ( - - {title && {title}} - - - {items.map(({ id, name, disabled = false }, index) => ( - } - label={name} - disabled={disabled} - /> - ))} - - - ); -} diff --git a/src/presentation/react/components/responsible-dialog/ResponsibleDialog.tsx b/src/presentation/react/components/responsible-dialog/ResponsibleDialog.tsx deleted file mode 100644 index d62e19c73..000000000 --- a/src/presentation/react/components/responsible-dialog/ResponsibleDialog.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { SearchResult, ShareUpdate } from "d2-ui-components"; -import _ from "lodash"; -import React, { useMemo } from "react"; -import { NamedRef } from "../../../../domain/common/entities/Ref"; -import { MetadataEntities } from "../../../../domain/metadata/entities/MetadataEntities"; -import { MetadataResponsible } from "../../../../domain/metadata/entities/MetadataResponsible"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../contexts/AppContext"; -import { SharingDialog } from "../sharing-dialog/SharingDialog"; - -export interface ResponsibleDialogProps { - entity: keyof MetadataEntities; - responsibles: MetadataResponsible[]; - sharingSettingsElement?: NamedRef; - updateResponsibles: (responsibles: MetadataResponsible[]) => void; - onClose: () => void; -} - -export const ResponsibleDialog: React.FC = ({ - entity, - responsibles, - sharingSettingsElement, - updateResponsibles, - onClose, -}) => { - const { compositionRoot, api } = useAppContext(); - - const onSharingChanged = async (update: ShareUpdate) => { - if (!sharingSettingsElement) return; - - const { users: oldUsers = [], userGroups: oldUserGroups = [] } = - responsibles.find(({ id }) => id === sharingSettingsElement.id) ?? {}; - - const users = - update.userAccesses?.map(({ id, displayName }) => ({ id, name: displayName })) ?? - oldUsers; - const userGroups = - update.userGroupAccesses?.map(({ id, displayName }) => ({ id, name: displayName })) ?? - oldUserGroups; - - const newResponsible: MetadataResponsible = { - ...sharingSettingsElement, - entity, - users, - userGroups, - }; - - await compositionRoot.responsibles.set(newResponsible); - updateResponsibles(_.uniqBy([newResponsible, ...responsibles], "id")); - }; - - const onSearchRequest = async (key: string) => - api - .get("/sharing/search", { key }) - .getData(); - - const sharingObject = useMemo(() => { - if (!sharingSettingsElement) return undefined; - - const responsible = responsibles.find(({ id }) => id === sharingSettingsElement?.id); - const { users = [], userGroups = [] } = responsible ?? {}; - - return { - object: { - ...sharingSettingsElement, - userAccesses: users.map(({ id, name }) => ({ id, displayName: name, access: "" })), - userGroupAccesses: userGroups.map(({ id, name }) => ({ - id, - displayName: name, - access: "", - })), - }, - meta: {}, - }; - }, [responsibles, sharingSettingsElement]); - - if (!sharingObject) return null; - - return ( - - ); -}; diff --git a/src/presentation/react/components/share/Share.jsx b/src/presentation/react/components/share/Share.jsx deleted file mode 100644 index 565f85367..000000000 --- a/src/presentation/react/components/share/Share.jsx +++ /dev/null @@ -1,144 +0,0 @@ -import React from "react"; -import logo from "./logo-eyeseetea.png"; -import PropTypes from "prop-types"; - -class Share extends React.Component { - static propTypes = { - visible: PropTypes.bool.isRequired, - }; - - styles = { - eyeseeteaShare: { - backgroundColor: "rgb(243,243,243)", - position: "fixed", - bottom: "0px", - right: "100px", - borderRadius: "0px", - height: "auto", - opacity: ".85", - paddingBottom: "30px", - width: "65px", - zIndex: 10001, - textAlign: "center", - }, - - eyeseeteaShareButtons: { - width: "35px", - cursor: "pointer", - backgroundColor: "white", - borderradius: 0, - opacity: 1, - color: "white", - boxShadow: "none", - textShadow: "none", - border: "0px", - textAlign: "center", - }, - - eyeseeteaIcon: { - width: "15px", - }, - - twitterIcon: { - color: "#477726", - fontSize: "20px", - }, - - shareTab: { - bottom: "-3px", - right: "100px", - position: "fixed", - zIndex: 10002, - }, - - share: { - textShadow: "none", - backgroundColor: "#ff9800", - color: "white", - width: "65px", - height: "38.5px", - cursor: "pointer", - border: "1px solid rgba(0, 0, 0, 0.1)", - borderRadius: "2px", - backgroundClip: "padding-box", - boxShadow: "0 4px 16px rgba(0, 0, 0, 0.2)", - }, - - shareHover: { - border: "2px solid #ff9800", - }, - }; - - state = { - expanded: false, - hover: false, - }; - - toggleExpanded = () => { - this.setState({ expanded: !this.state.expanded }); - }; - - openMainPage = () => { - window.open("http://www.eyeseetea.com/", "_blank"); - }; - - openTwitter = () => { - window.open("https://twitter.com/eyeseetealtd", "_blank"); - }; - - setHover = () => { - this.setState({ hover: true }); - }; - - unsetHover = () => { - this.setState({ hover: false }); - }; - - render() { - const { visible } = this.props; - const { expanded, hover } = this.state; - const { styles } = this; - const shareStyles = hover ? { ...styles.share, ...styles.shareHover } : styles.share; - - if (!visible) return null; - - return ( -
    -
    - -
    - - {expanded && ( -
    -

    - -

    - -

    - -

    -
    - )} -
    - ); - } -} - -export default Share; diff --git a/src/presentation/react/components/share/logo-eyeseetea.png b/src/presentation/react/components/share/logo-eyeseetea.png deleted file mode 100644 index 6368320f3edcee7d769c80f3e7ca3ec35e43a060..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16196 zcmZv@by!qi^fyY%08-L0lqj82gEWeRBF)e#-7`pcioh_S2$BNQLzlEj4k+D?bPCea z_wfC__ulut&;4VbnRCwGd&OCMt-U^LO_YWjl!SFryIOra5 zC#+PN6Zj&8D;vDU!V+M=`(S(L$$0`d>D`s|-L+k9+`TN^tg#fVT;Eu;zI3**wbrt> zu<}9lT1#VL$qv7KuAt*RvpX;6%DB}c`1jypQR!$dai7wKC4a>kR#6`b9IdVFmnw?= z`jSN7D6gSyWQr$ojTT-l3QrY{GR`G}tHQ8puyvmXkP~foT1*XmAtHZL;*!<|%PBHb6 zD)M|55+q}Iq^EW?`G~AHYC;i$6F}BwO?z+_@u;1GJi7SVlSZSsT8?%KJ3lVchhvA6 z3JPXgkE7sthK$jE(Y*GL&ne{v!w~7M8RJw)wr%96vu>FBf&e{pYCN9oJIRdSTPL7V>C>#>I| zYHUT(xilQB@?K%WG)K4JBiiWRv=8t_NEL!Zq#~DDoAzD=OnqSK)dv`^*A^nx7FyV! zilY%RB$Hu?L2v3t?BQAVy$Bd>Grv!EIC(aq^wkaT8%bAkEW=7noZlgPWzR#Bq$<~Nk%q_Fhki;o9ZPmZ$_)t?a)EpdJ zby{Y|_pBr;J^vL$IQy{>9p-$t&TyHXOk18*3}0!el;{s(_sfY)`XaA`RkGIP*P^&Q zP;zARnX`kS2C#xvRMb-tbkm%xo1q)jja!Ov+&FjSVx>S0WtgYiTpM@ji(Xbe1S<$t zwa@A(5Gj9qqeu$smVGalo#e|3&d%cqtsD#XPY~C{S_1JGF|gCAe&=4S!+Ed-`a}5Q zml)pN`$pbf`h$_sP)+#}=_EJ#fHpc>>$LY0Vb}TqpFX zF!|F@!cZCq3XB@h)IQW%@_3(G%wN09PN zS2Fvjjpst1FJ9+9owyIYK%;TUz#_AKl$uXEhwd;g@DBc&gV$YMYo6OqKaz*wNZrd} zf?q6EOS2A&KJ(hrjd+s*>Nb5Z7LVJ{3Jyst?nc<8R|+oQ+kA9F1>6v8gY+$apLo#n z=3X%zPaMxuhm4YcSVcJOL|Fs`{cTBsoDeykj2U|?ADLWAB8RmbDRf)uB=)ib+VqD?5W(U4U942*4J z7CAMeMh&f@5y`Dgc7&eCtkpES5JgP!i~3mHUsB=h?v)N;+f%hQl!RWRUIe>4HyuF= zR1@JPtSd7NXzl4+OgSOf`@M0R%1^ixda+;X6^7rOrMIyJ3O9^V@j&f()0DME=Zo7Z z3{AvPA_;J=NMI~roh$@78E~$&Jy;@p%|NAjt`)*RRxIo%#f+BN-iGU)C_gR$S=8AX;rs$vs( zW1FlVJAl44VLAA>W6oa*!`TZNmYxYqZrthKPH`NfD>jnLC^i^Ch0^d6>Q?H>c&hql z1H1&PmHO#K2|bdou6YItMN`O%5975{=*0fbh9-G}poJaEltXY!RbT@=nGhTKx;?C# z4#)Fa{jE=Z$5B6XA#((KJtYCA1Rfq;+?|d7kfa-z#6$>%6OjJGlsT`3vwzaOlP|6Y zkDuDn4HnlbB<@+4c?#HewuZM;LwA%3TlhDu|VCgx7 z#`r1obOCxtoFx$Y#lLuyx8dw_MGk!2!7TYrvKcd^7qfOjuG+!ek8_~o}-A>E#x*>O%p ztYC}M4;()Z;^5(A4R7-YNk_t3C{S>I4{n*E2gm7^&^xa6pNs%HU5BlFfroqT`AqHo!lp}V7=z5_K%Kbu52Qj1AQxFcdPJKf2e>VYSga{i`~(Vl2*%z>f#FjXqP~Qh}$x-#%__`FR-=x%lDSWV@ zx5K$JiS+Ox_m+iE&j2n$5-+a)@hmA*-WSD&NJOyv_q>ML&8PJ%)We=UXII z_YsJcyco%ocboZt=gdF@A-}ISNOq&(;SRL{yPNLWhiT{=LzXY2ByEz$paEHrNzx1N z*C43FXcY-&aAVx8F&K^G$DDqC&_ws?=v8%$Q5hk$aLN?7wcvbr(7uuNOA(#{W$Ka3 z(N=WYcLkz83j%bX>0YJ7Hy#gN6!d&HCbczo)EyzEqEN_?i?j@=(6IO>EOh&L{(d1i zJ1lYKT#FnHDnkE6rj>bT@Zqn+*+kQfv;WvK+TapHxh5()%Uh;op7sl>Y4@w*;=ZlY zu^^?zc{`Q0tp!n3Y3W~xyL}Cds1X$^h++HGElAr@RW?2E7jVfJ&hF8uBJoium(1ea z1!T^~N5K6kk~(rt1ETo0h34I{G@tFXhu*Fg8y^{M`-rX&zOYfEVQyujgv3?tIbJHI z<-3Y;pW0UPqzJXYy@PGQQFNOSv&kc40P0lX$FGycoN6f#bR3qMRJ0flWE&+m?h3*p zDNXV;?DD))T*TFr&_BIpn%5+ys8Jea2|7#zqWC%wl8$<1R&-zkVTb0w1c!_1iyL9s zz5-;wIwC5JC5kqDm@6z3*|7qOcxaozd_RI6tC{Sl zLgqRM1jW)njN-roPD)tq6jc;2HL!tSxUh0oVA?AATLrOOm4PMw+iIEjrmi0291g50NOpT4&?BN?r2L zUQX{{#OG|Mh$d;fjvz-)29%C&|4t`ZmljnOdVuEXD>Y2iVl9ZE zg;MtkxbZg12@E6hVV`~U)x>~9zXgaziaH7K>}*c7Nt}%q5$sNs?<0vV><;e-{Pkr1 zR8*C2|6u98P`dh;C=Jn&du5#e`oa#MzXd@r^q9Jv%Cx3-YtdSek$#~$6$J>HlGNaj zKj{ToqPpB*dwW(=8XQe8ZmRt^Y+MDY&mbdL@?dL@QTfCL0lYlKt0iSz9E4y*wmhB! zH<&GAMJUKG=W*7sg5>ZC58x_)U<7DFt3D_AgaiI}>XA*08jv$g8khxcs{8L@IguaX zny!|&u1nN&o;yAQXY1ptVwHNvw|?)(b>jmoKyYN(!PrydU*LGbWPgaESex`q%893< z*sS2CXMq1DM`5{tAe6+a$UPz>1uj^@*uViZ7@k{s@9yE>Y=Ts*#wE_htC~>}UBHm) zhPOO^{XeGjzfHgcY)U?78V_r48yM}LMV+PXgoA}ojtYj~J^Fvm4Vd%)bL;=63yf7> z?7L&nqRlMugmdZ6W}iCq0Bo)Ox5)p8Bv$(ePpNUdA$nVYDVnP_y`V-a6xI-+s{du( zKoFRCO)1ZrP7EbS{fHE3y1^xa-iiGm#SwiGRjdplY7}q<>>_}cJVYx&1-J*FIAPI% zXr;7XZL8ZFV$W^<&#b#JxVv4-5$w6sK^_Iqb?9co{!hAKfxBJXT*UFe=stDg0cU&L z6yvm0Bu$wCu>#Berw&B%!2RwH%{9R22#^?wQ=+i`E|HYM!#}1w-~gKc-K`60e*rKB z0@(dMM~wo00H=dw2OPV+1-`BaC@iO9i8OBzG~GP-C!)8dxR{qkBJr|ULeAs&_=^7) zj)V!~fcIkEI^Hzq^k{OSeV^srE_Gk9^rC7BYIi0#*^ti~;P3}SEYTks zg_b~%tFX^6qMy2e^S1&fy0G|IS{k2zJ#%aMP~nHIgD! zo7$on=WTMR8*?%T*2zjbHL9MNpZorXLLrZnm$_PXgg~c`2`R1VSw{7|Z3cU-{kb9L zC_+g3!2Wc*J|l9?Vj>6eOSTlDBdJwa$KOaCw|`>AMs6$neL*+rsBCJ5Dhb%;)8l!M zXag?OBiKEjI{wQzQzGgF+_DqsSAu&wm|rI`r^G11&(}+Y3tXg4i4pqRbu3EejbMnAr942%(nul^Cnby9|6)c>h2mW-q@kqTKo0d~& z`bc;#A#HdS>5owzHno~3{L+H&xF4}d{GOE@9L%}?9&oP_R%8^euQ~s+QFU@2U+(%q z==urzq>Fg|@L6Pa$-ljpH3GiKt@d);=}Upr>f*(#(let!{a#zW&knho_B>Un)-&?NfuClre-zLSREFisz2W)Rt(m1Abg*!m)=SNrW37(71jHkKI zmPz)91KybUtExJcTp*vIdVjg4Cfz>7Hxk&S5BfQQ*Mnj^H!!O+p+dpmvJPZs!3&>S zg-rq|m?vYSnRZ;i_o(pvWC|Jxzz=0ee=>6H9eX}O!SO=gXBpq`EvLQt2V+LDFM+P_ zcb?NNE3;~a=oy4zxJIA$B0O7Ce(3o?n2DjA>a3(yXX;PAvy=+k{9S*)q_l0xXYvxb zymDt;QqELF+;pDcmMVmG8va}<=}IF75?0$p$WMlU?jHJ@sxQ2D5PA}?Xi*wLT%^@I zF50k%rKKE4@#%I%OCT?j&L)>ceAN;TPHrw+An3IVWJ7wQ10G*MNXd>@~MB%8hqnAS&4YL^j;&qT$L)ig6d;R9&7uDg@|RY z*=tRyA&H}nY-ZxS=Xl|{@Lbw#NMBh&Ik8f`kuo!V0u{=glWx(DkP8_l)&2p6mmBur zCt>dgkB_tG&nbuGH&U^PXgjD}57P$ix3d)>BTvLl-W^K6Vv~H?q;i*@$Ul%$$ymdd zRJ(k@4z{l^;s}1#Y1(O45D#`BT~ZG7Nn+9w=w>GXQri*#f>bOXv2h@eFuE#n7wzGJgwXWn;3G%Qv$8U2_l$Qex#~gEzse+D+#_;7No$=S zCE|h3&ExF1OaSzO+_OBKUG0rIy9y+n-8pGOYQ~m2+VU4<>HRi^h8Pi$nEv)}q*Rf# z8*IBc#*pgPH`VF2p3i;(Aea#*Kn$JOb_!Xmbi8zEJH_0=UGi4ZHtu>uLne0$;;ola zUm`~pGKV~-1b~1o4im-SR}`o$w{XSuS!yU`MAN>x^CRih{Bt191bh%V#5J59(z~SQ zUPJ_K9MFDhZu{T7NI;jRTh0`yddhn=n_ASU@o$8 z)Ur?hgG=K6E+_?dK*j-#V2(TNEdWJSxfeKms z!>ll_&qL|9xgK(TjHxVN4d7QG>qJ3t`?K8e#@U-C?zs7umqv(r3PgXC>vdP#ls~JR z-}RVPAA{SN#30=Oq%sWHrbR(fB;FEeR%KqbyavzZ1~>%ZD4um4l3>2U!;SRfM*BL! zbH|lAEOt1)tQ`E!L-&pR(pr-+Rm*3yebHcn>%e%QSFh+Z2ZM^D%gLnnjBe?wpMQ6ED>P2 zeI*iYOR-ll$=X8P9~a<|fLH=4nRu@#UIsvenq#PfDc;%Ir`I}W&RJG**>7XFj}Im5x{vjaVwN&0JYf)L=g4Jg~&x-6HD-b~O zcDONrI#?Jdlm`~rqy#)lcZB=I#~5HUbtF_ZzTCgwjSSoMb#gV0vferii)f_@3QUW7 zT|nIWMg5Af`na+AqV;yaBzMr;6oz`L3kw{s6?nS@>Lp zqMP?DzB^elry>qA*V-g`H;e|=tp#=_q$c^&t=*nQ_Weg zP0FBKFbtl+oiv8+!tI-9Vv)>haiGj!XCI^9p{Ym3iUH>z{5%NNQ-czMv3g4#Y84wUo`B*j+{(*CTc~m`9X8wF7 zdB;XgnJ==eA;($J)Q1bDRcpalIW16iu9e2$Xgu1P@$XW}E2{Wz7~0oAO%c&VwDOb+ zlTtSNEu|bPv+(3m*7}+P0>6k@IefS3s-5TEti;8fA5);Q8>oyr z3YK12_GZYBpIb}zPGoUq@1MJ$X@}EOHD==5tiivxNQLy_+S@3%_iWMC67n>ggS7O> z9T_yT&A3Z0;p{csbdl40BBV>s zL_KNf9s~`U{kPv{5XG2K;YQ8Y$p@bdlpcb!W3eT*#kAz9eUb@sw=1km-iDu`M6tc7 zC|ws;B@8jCifbSfH1i7T_Crz@)<#iux;+tqg3s$yYQ*c`b5F972Vm`%5&hw!q%2lj zj!mG16Ad7)m5;z7WW^YRo6}b+aOhyH1dx=cCO2H@iojJWThi$P{Qaq(jA>&%fOqkn?gapWJcNxs0V9gc5wM-(N?#=6(R^{qS1A}ou^iZ@Dd`=Ncf;gQ zdLGOdwNof3y#3-x_~h>AKY3$vLD6UBTWZv(A6h4NZlusg?WV2;w8mKPaWzZ#o+N?c zSFha+v>To{o%Mw5Y3v;%P9GcfFos|CNdIK;0QDB!Cmb94ms4bdM}vN7`o`X5cB%la zJC}9EpX5C^$&lgkg<$175M^;@_2Q~upE-Q)g5(ea{<&UBe%thqz%Oy2>O_#!w>w_u zlXyC&1i`5*Va{o!bBr2*3_6_-F#>hJY0i{>>_%2!tatW;CORm2>=!9};}b#eLNy^c z_^)C*p@v^;-^i>)*=*9zqcz5RPpetf4(k9WVI{UYk~oiJdcn-j#MWm!66X(KUAf*4U1oa|1;)eY$h@olci<7)W z2aC*~Lljk!a^fQmgFv7&+j&q8HgI+WHofEa7rUreT^~u}vfezlL=ELqj@%T(Ds6dyT8$>Y z66_2v=Xbi_1z=s4lnR!+Kik+i0>RHibCB9Yy_V!OA`oEiz#P2Cu2A}E9-4hJ0^X7V4KL#j#e#2WeCF+4b_CJrg z5<4v91_n*UBZYfx@dJA&x#>J|0A1)3ta3G7KF>&Y7a4eVpdSO=D#NpSJr?CMK~VQ^ zQC+(hGWro9=!i|f5}$D8k!1)d&7bZgfCCecKWwM?-4Q&e4qJUT{?Nqs5@OD4FOg<( z8$>&?;5ZHfb;aN-VrDaA^|g(oinfTEbc zl&iM>zz8qS5g*xrN+uNu%Eb#}$2uEztoSoF3rAQi;<{R_}(9ET8)JuVA(3ufc z_^fU+#V5`KErU>axQ9jk=T@eCApHzuzWxUk!XK1>ySLU7bS>>-emWA{XL_-+6y5q% z|H7$`d9vkVUrs%P8vr56-*-gF`xh!L_+%&^=4P`&$~QM)*IDlvQ${0({3W{62nxy^ zmOvSWq$}6obq#pCxiLPVkCrBZ~7jJ+8U&S9t#1HmyLYIBTLEi)F6Y>`w*UP z{pZi}mqBJ!Mf!FUP@N6e026j9HvjBE=vYELx0XdRLDc0504`1{xCDaXEei#uMSc|+ z4Hek*f$RZ&5C{7=IUUZ7CX>S7q=xZ%!P%Sg;9Pm+Brd;JwBSy1RzjrZ+-~#MFI8ot zpM_lNV7EW!mylGt=typIkF+G$WP!O)oW4()F}$tZ@3YbixnQUYXVw=IvmbaW-DrK* zAR3Yrqo?OP5Rn($U<)O#y#fvY5;2D(d*=8=)AWcHsA++N7WE=IfR6A|k!f?ujxf~5 zWTo1@GwdR7R|p&xD(7J5a=zAMAAFm;tpQ#uFVqSYq+_H;!Dc6SdAs7<0ki%U=Jrwi zmHe7A6j!*L5?Z7s`h^SJ@w(cz1`@^DXdLG(s=;rG`ot6s-LiImQ+=(H$e6(RwmEUC z-G%QF3l-60>oGDWY|(xWEyQ_EX}YX|_RrmPx$1F%B zO;UA!3|$@TcV3XmTbmTq)PLSK@N7{#RMrMTU!5MA-Pj2R#U4${Lp+lla-`nO-6~0I%ym}Zl*BACljx1 zS)(I8=7u2;-Em74$J!XX4M&ixZZX`dNdsK6sc3TM71^!{q51K=%(si=ZwetW@x(%6 zVDTo^`4&_Zrh~aub0=$2eho*Htwm7YWGdg zj0fHNtiMAeV7ZmbOH(@<>FV`93G*($>YxW=x>x5qJ5}r^#r^1qmV9m{+~Jmj#a-?S zzMdFCF&#-yb>FIJ_-lqM_nr1DW5I~6$~bX>cn`J zgFBwbREHID*p(#L-Z+~z8AMo~g;n-AAcn)H6l}XPcMPo^Io4_v1t)WeFnjObI9=~&*9Z=*qR`v;TPVhX(I?iOl^PQgRD%-mZJaFGlhbSw0*0()`5?woAo9aedO zRw3AE%h^;+Z9*ru*td`wb5f{+%?+NVjTYYGAe~`k;fL8m(9F|H+uudh=D`U}gC~6R zg@cE|yPfN6?Ug2N5asdZm3IM-XWEaOjAdSk!Iu&oVChd5*R8YLg|-bK-x?>M|V9pEE5@q0CF(if?}-ohb2< zJ|(P*byt|EHEi{esAet%zLmWZRmd})N1VUdz|n$CupldCh0tgM zri)7E(jsPdurtw+@hFF$u#VreG>)N9wYrrDY4{+FZA!@vDCYI$g=h z+OFf?I)}CP-wov1aSwr3yToTXU(AtGD;TC*VY%x+<+nG(FVD`~I5o4S-Ka!WOs5we z$GW!6yLCnFYU=lF{7;5fXdqWjFttUYyVrf{V$z&O6gEGXD@5G-kqX4lnsC zlxZW1>Fv*x=6p25V1-9{N#9wBv}y;D@m)tXuHV=>w)$P58OQo3zh^ZLVFynz3`UfI z(2eHlgUSI@)hq!fiQcP)5q5SR|g##8ppMf zSn1$*1|qh@PzN?4e9dqbFv#Ks zv%gy}6cHbKkUcTwOss5p1dmT!rllh_&WmtUY^A)T5old)huC~kB!B6TR#nRP;g-+Bgawve@m5M@jH=%b zSlbxCge*Aiayak`j^5k*)mHzx_A(`fvq(iBA}&P7zt`H-`)&_r^otOqI&L}le9Lrx zS9(XyYrE=_n^h&PjR)W_L)@dhJ7Tcr`lH|%RkN$0XY$?n3!^3zjLw^CGMhgJX+`u^ zW-U}8MBR18d zYINF-2PsqQ>P<${$lh=P{Aq(reBGc=+zxoZH*QW1iX49Up4QUjGW|kFSMb6+j2{?6 z3ocNlPU1~JKP1&YBW@nu8zIZ_JITl)SwV8j{5vO$1_Ld1p_MNe{`_dQxYG2{NVlI1 z%=+S0{{3~emtKlh|Dsea*UHxkIU;D=l>wotmW;$L8F;kMEE>Ez@a5&>P6c+kJA-wFTTs@v3Os7o13nwm}BoE+6-Z(iVHauk_MQi4T# zW=?8ipZI&8U>4^u4DxsYuBv*gn;xU)+lX?pc8)Oj3WGSj~ToJtqH+Aw42{zCT(j&bjXl$p>0dFV<$45jQDJ z#640e#|M!E9gh@qh*w4>@N<|e#z|d{;H#$Za?G7dZFjyUQ^p%@y%l^p8yBnERxd~w z(<;1c0?F@XqHLQoTqNo-@)=*Zm7vj(G5z*$s-By!fOMZxX3E1&&8!JVfTG6U8ffss zp6(`mI*XUXc>M?I<>Ln3Z$G%xdLD5^@NP;>v#Jj5v>fMVjWw}in|{e<(;WDu)=k0{Ov#gVfNcMj5oIGiV_6b{+ZCob!< zl*l{=VyM?B&Y`c#Vg&|!dM64ZK_|9bpX^HL#X+ZX;+?i#S+|s|7l*+5@>I>2BK{~P zI$)O{PFermx7leZR@6}xDs>!c8aJZp2i zzFOZoO4Lpt=Um~M7s#l$68bBMj_U0yg40e!kjaO0Vy5@X(v;Mu`lke z>Pa94yH$;iW28ONPst`^Dxcl4&DyB`-Ap1u4Btw+^3704T5f&Tj+V2o{0oU~TYK9t zPO3uP{j+@YeVU~ofYndu>m*)}rFc~bjxT3wj!%^YBLyyN=L@;%R+g^bZN!=!p^aFaZG8e5Z>9CnLJ+yghIw zroHm7Ig-UjHfM+H_S#gz%MdA%q` zS2hM4djurM@vBLMzj4obA!ct8L<}4eA}rD5oV=t0#ZOJmINUz;*qi z6H9&1%6D%3itasg#GrUAf4(_VBg1;q(Uk(VHpf=Jh2p9LqV@GmR_e~mh7y2!GjiHrnTKabV~>uKX?`9{STxT|jEx8{S=*E;*~SVgwj4YPS#kD^2b6*o zcu?I35|%pPDt|cWauM@b?_+_z4`F{XGiK{uwCBZ>a%Ob~6Ult%B^Rl85bnQ5l5WlM zo#7c%o&Ht!x;xRYsy3sf(B1ET3YHwCcx`bD28>3#j3-FVA>@8`)W%Eg)YPmrSiZv- zc6{1sh}ro<+d%a(K~(miwddKqr|G(x<2t*YyL@*oZ<*YNVUEYUSW=Ai)bid=*(`4? z@l_J4+P7eLR2s2DGjCh%toQ_f?d3M&!5ktxt%IosgC4IVa^C(RH&DTM$Sr_BWmxi*6F>{w^ zx5(@iTEnkmH+hI9V1Z61JrA6Zf$5xi~DYIus2xd9=GN3j4^}bQ4$f72G+hAZlrg?$NZV4(9M% zNQU!eOLU+wRvUpO=UnO|w84Mi*UJT0Sk|aG@>0>HhWaN>aI7=I( zF*bs-0!~`iUI--g^NyM2)kVIE*JfF<+>J}pH=gR(B|fK&nvzLV8I8Nr<;-WqCCnb> zTH$p?p^yce4W_3CN~tBIdG;q{;y{8K%P>4K z(Xtryzi+SAHC6e%&2$|zp(AqqvTPu-sa?rT!0DG(jAqcYhc z9D6?AGK&d&xIdtMw%xCZ@6$ysgqYgkdn%Iqc<~I5&lj^=lbZ6USwZ|j(F6-%!R^!rm|N7;5 z6|V#E)69v`prQl;$o^{h0=kYA(K_jpZ8YMv_jKgLu2get(RVf8a~iL4x14M5_R(Ek zDDE9@9^R5!L5|UX;=1#>CA!TLY9GE}zI$;x|4>y5#2CDCdZ@KAw3_{$-Bp zeN3Q+*7#IyG#3Z8YSnVVBAIrC^Sq{4z+<|tRPQIsjh;*e72w>R>6`ZAvLDi_i$-`$ z({5f{6B1ZTdMKt623ncHVho~fd?oOL{Ez9dO!=WX*#4fcp~)UU-kK@VU`f_*Q-%#3oZ{W;VU3WXb&Fl>;G?iSml(__S`;!7!1#8INSAdFH#P`mrpE~k=g8#^rS zGfow?TB$YOQhhXW#|aHQ;cIJQuv7CtV`*#L@Ri6z2ky|}3EN51!&8WWFslqfl~0@9 z2RRTX5n1iOY0zjv9zRc$*`a(#4<^>#HADGWmPSOMc;7+AaRbjvBy@PCKps*iHsbH7 z`pR&bjTN6=BgDuVXJ|#~w#ig^g+9LeYik9k&)9m5G+jX|whjv2Ja*pqpWI(`87Xp+`(B1P)pAM+v zsN%(m$dU4Go~iMc4+k<@F4nia^5%@Mu0G2}m(AMS_{5q6g%~g_*6XM~lxXA#;EEM& zL?55G%SVNsBHl(5A4*a95Il;4a4b`;DACXM@rPUa|M{G1AcV7~2atIaVB{wM!sFt( z2=R-oIry>Nms8+;yW865M8irKvfLqD>RbaekitEDRGo%0$9q)-(E3B#Go@-9Z$J$2 zS~^!hAnY7)N~Q|y@484USWNV+RC8oo*d2xejm_iWuj%EFlz7HzL;jUz%jK>JWx{=C$+oGDL2iN zvB4%P1)J!nK5lnNaxwf_!fJcYdlM@Tz?*a-BkhPn9|q2Ef79|gb3gGo8!vRbOmK!$ za6JYe#q6^lJwN9lSWSB(L97qz*))x{%qFQeShVqM!G2W+sIH$j4Ijx1>Y0LZq9Qr= z(IRFYTy)q=%BLQDb=-a&jzqnn<-w|TXH(|I!S7jWn%37bcIX#|DGWb3nZ>Z+taI2QQzfGT;>E0^0vJ>d&!L z7#+mtykIiyBq^@on(5qYltaJN5CYxg`mljqUX$#U_J2j`EJ{xs&B zDR)!6#@*5ElMzDr1nZ=7y$6))6Mc`kyGVUq*p zCak-j$p87#^Ykc6oH8543Xb?XU2r#sZ-Iau^{8As5i(AN)m}OHb;tdqA|hWBE8H?O z!*+5dinE&%J3NQyz;|LRWgTdf4>)}adrzr~tziav5#eU0J+>_k0|q_7PNhH%TW_SG z#$z7Sai;W&Va48uqD#pOQV`FByCZX^khU7Jl&0 z$~v`;kisUaEU85HrpUVHJ0Fl~J{D3ay$FOo#SNAS zx48?Y{pWs#O^&CcErUsBN!nOIo0p3Lf|TaQu70_k|26}r3-i!C=U3wx!6%YuyJ^M3 b#j^OIh?chRB?JBekM&YX?RlA^dFcNIZM>@f diff --git a/src/presentation/react/components/sharing-dialog/SharingDialog.tsx b/src/presentation/react/components/sharing-dialog/SharingDialog.tsx deleted file mode 100644 index 209e3f88a..000000000 --- a/src/presentation/react/components/sharing-dialog/SharingDialog.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import DialogContent from "@material-ui/core/DialogContent"; -import { ConfirmationDialog, Sharing, SharingProps } from "d2-ui-components"; -import React from "react"; -import i18n from "../../../../locales"; - -export interface SharingDialogProps extends SharingProps { - isOpen: boolean; - onCancel: () => void; - title?: string; -} - -export const SharingDialog: React.FC = ({ - isOpen, - onCancel, - title = i18n.t("Sharing settings"), - ...rest -}) => { - const classes = useStyles(); - - return ( - - - - - - - - ); -}; - -const useStyles = makeStyles({ - content: { - paddingTop: 0, - }, -}); diff --git a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx b/src/presentation/react/components/store-creation/StoreCreationDialog.tsx deleted file mode 100644 index 44a1268c7..000000000 --- a/src/presentation/react/components/store-creation/StoreCreationDialog.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { - Button, - ButtonProps, - DialogContent, - Icon, - IconButton, - TextField, - Tooltip, -} from "@material-ui/core"; -import { makeStyles } from "@material-ui/styles"; -import { - ConfirmationDialog, - ConfirmationDialogProps, - DialogButton, - useLoading, - useSnackbar, -} from "d2-ui-components"; -import React, { useCallback, useMemo, useState } from "react"; -import { GitHubError } from "../../../../domain/packages/entities/Errors"; -import { Store } from "../../../../domain/stores/entities/Store"; -import i18n from "../../../../locales"; -import { useAppContext } from "../../contexts/AppContext"; -import Linkify from "react-linkify"; -import helpStoreGithub from "../../../assets/img/help-store-github.png"; - -interface StoreCreationDialogProps { - isOpen: boolean; - onClose: () => void; - onSaved: (store: Store) => void; -} - -const initialState = { id: "", token: "", account: "", repository: "", default: false }; - -const StoreCreationDialog: React.FC = ({ isOpen, onClose, onSaved }) => { - const { compositionRoot } = useAppContext(); - const classes = useStyles(); - const snackbar = useSnackbar(); - const loading = useLoading(); - - const [state, setState] = useState(initialState); - const [dialogProps, updateDialog] = useState(null); - - const onChangeField = (field: keyof Store) => { - return (event: React.ChangeEvent) => { - const value = event.target.value; - setState(state => ({ ...state, [field]: value })); - }; - }; - - const validateError = useCallback((error?: GitHubError): string => { - switch (error) { - case "NO_TOKEN": - return i18n.t("The token is empty"); - case "NO_ACCOUNT": - return i18n.t("The account is empty"); - case "NO_REPOSITORY": - return i18n.t("The repository is empty"); - case "BAD_CREDENTIALS": - return i18n.t("The token is invalid"); - case "NOT_FOUND": - return i18n.t("Repository not found"); - case "UNKNOWN": - default: - return i18n.t("Unknown error"); - } - }, []); - - const testConnection = useCallback(async () => { - loading.show(true, i18n.t("Testing GitHub connection")); - - const validation = await compositionRoot.store.validate(state as Store); - validation.match({ - error: error => { - snackbar.error(validateError(error)); - }, - success: () => { - snackbar.success(i18n.t("Connected successfully")); - }, - }); - - loading.reset(); - }, [compositionRoot, state, validateError, snackbar, loading]); - - const save = useCallback(async () => { - loading.show(true, i18n.t("Saving store connection")); - - const handleError = (error: GitHubError) => { - switch (error) { - case "NO_TOKEN": - case "NO_ACCOUNT": - case "NO_REPOSITORY": - return snackbar.error(validateError(error)); - default: { - updateDialog({ - title: validateError(error), - description: i18n.t( - "There are issues with the connection details you provided.\nDo you want to proceed?" - ), - onCancel: () => { - updateDialog(null); - }, - onSave: async () => { - const saveResult = await compositionRoot.store.update( - state as Store, - false - ); - - saveResult.match({ - error: error => snackbar.error(validateError(error)), - success: store => { - updateDialog(null); - onSaved(store); - setState(initialState); - }, - }); - }, - cancelText: i18n.t("Cancel"), - saveText: i18n.t("Proceed"), - }); - } - } - }; - - const result = await compositionRoot.store.update(state as Store); - result.match({ - error: error => handleError(error), - success: store => { - onSaved(store); - setState(initialState); - }, - }); - - loading.reset(); - }, [compositionRoot, state, validateError, loading, snackbar, onSaved]); - - return ( - - } - onSave={save} - onCancel={onClose} - saveText={i18n.t("Save")} - maxWidth={"lg"} - fullWidth={true} - > - - - - - - - - - - - - {dialogProps && } - - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, - helpImage: { - width: "75%", - }, - center: { - textAlign: "center", - }, -}); - -export default StoreCreationDialog; - -const HelpButton: React.FC = ({ onClick }) => ( - - - help - - -); - -const DialogTitle: React.FC = () => { - const classes = useStyles(); - - const helpContainer = useMemo( - () => ( - -

    {i18n.t("To connect with a module store you need to:")}

    -

    - {i18n.t("- Create a repository at https://github.com/new", { - nsSeparator: false, - })} -

    -

    - {i18n.t( - "- Create a personal access token at https://github.com/settings/tokens/new", - { nsSeparator: false } - )} -

    -

    - {i18n.t( - "The personal access token requires either 'public_repo' or 'repo' scopes depending if the repository is public or private" - )} -

    -
    - {i18n.t("Create -
    -
    - ), - [classes] - ); - - return ( -
    - {i18n.t("New store")} - -
    - ); -}; diff --git a/src/presentation/react/components/sync-dialog/SyncDialog.tsx b/src/presentation/react/components/sync-dialog/SyncDialog.tsx deleted file mode 100644 index 747ffd687..000000000 --- a/src/presentation/react/components/sync-dialog/SyncDialog.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import DialogContent from "@material-ui/core/DialogContent"; -import { ConfirmationDialog } from "d2-ui-components"; -import React, { useEffect, useState } from "react"; -import i18n from "../../../../locales"; -import SyncRule from "../../../../models/syncRule"; -import SyncWizard from "../sync-wizard/SyncWizard"; - -interface SyncDialogProps { - title: string; - isOpen: boolean; - syncRule: SyncRule; - task: (syncRule: SyncRule) => void; - onChange(syncRule: SyncRule): void; - onClose: (importResponse?: any) => void; -} - -const SyncDialog: React.FC = ({ - title, - isOpen, - syncRule, - onChange, - onClose, - task, -}) => { - const [enableSync, updateEnableSync] = useState(false); - - useEffect(() => { - syncRule.isValid().then(updateEnableSync); - }, [syncRule]); - - return ( - task(syncRule)} - onCancel={onClose} - saveText={i18n.t("Synchronize")} - maxWidth={"lg"} - fullWidth={true} - disableSave={!enableSync} - > - - - - - ); -}; - -export default SyncDialog; diff --git a/src/presentation/react/components/sync-params-selector/SyncParamsSelector.tsx b/src/presentation/react/components/sync-params-selector/SyncParamsSelector.tsx deleted file mode 100644 index b6649d0ab..000000000 --- a/src/presentation/react/components/sync-params-selector/SyncParamsSelector.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { makeStyles, Typography } from "@material-ui/core"; -import React from "react"; -import i18n from "../../../../locales"; -import SyncRule from "../../../../models/syncRule"; -import RadioButtonGroup from "../radio-button-group/RadioButtonGroup"; -import { Toggle } from "../toggle/Toggle"; - -interface SyncParamsSelectorProps { - generateNewUidDisabled?: boolean; - syncRule: SyncRule; - onChange(newParams: SyncRule): void; -} - -const useStyles = makeStyles({ - advancedOptionsTitle: { - marginTop: "40px", - fontWeight: 500, - }, -}); - -const SyncParamsSelector: React.FC = ({ - syncRule, - onChange, - generateNewUidDisabled, -}) => { - const classes = useStyles(); - const { syncParams, dataParams } = syncRule; - - const changeSharingSettings = (includeSharingSettings: boolean) => { - onChange( - syncRule.updateSyncParams({ - ...syncParams, - includeSharingSettings, - }) - ); - }; - - const changeOrgUnitReferences = (removeOrgUnitReferences: boolean) => { - onChange(syncRule.updateSyncParams({ ...syncParams, removeOrgUnitReferences })); - }; - - const changeAtomic = (value: boolean) => { - onChange( - syncRule.updateSyncParams({ - ...syncParams, - atomicMode: value ? "NONE" : "ALL", - }) - ); - }; - - const changeReplace = (value: boolean) => { - onChange( - syncRule.updateSyncParams({ - ...syncParams, - mergeMode: value ? "REPLACE" : "MERGE", - }) - ); - }; - - const changeGenerateUID = (value: boolean) => { - onChange( - syncRule.updateDataParams({ - ...dataParams, - generateNewUid: value, - }) - ); - }; - - const changeMetadataStrategy = (importStrategy: string) => { - onChange( - syncRule.updateSyncParams({ - ...syncParams, - importStrategy: importStrategy as "CREATE_AND_UPDATE" | "CREATE" | "UPDATE", - }) - ); - }; - - const changeAggregatedStrategy = (strategy: string) => { - onChange( - syncRule.updateDataParams({ - ...dataParams, - strategy: strategy as "NEW_AND_UPDATES" | "NEW" | "UPDATES", - }) - ); - }; - - const changeDryRun = (dryRun: boolean) => { - if (syncRule.type === "metadata" || syncRule.type === "deleted") { - onChange( - syncRule.updateSyncParams({ - ...syncParams, - importMode: dryRun ? "VALIDATE" : "COMMIT", - }) - ); - } else { - onChange( - syncRule.updateDataParams({ - ...dataParams, - dryRun, - }) - ); - } - }; - - return ( - - - {i18n.t("Advanced options")} - - - {syncRule.type === "metadata" && ( - - )} - - {syncRule.type === "metadata" && ( -
    - -
    - )} - - {syncRule.type === "metadata" && ( -
    - -
    - )} - - {syncRule.type === "metadata" && ( -
    - -
    - )} - - {syncRule.type === "metadata" && ( -
    - -
    - )} - - {syncRule.type === "aggregated" && ( - - )} - - {syncRule.type === "events" && ( -
    - -
    - )} - -
    - -
    -
    - ); -}; - -export default SyncParamsSelector; diff --git a/src/presentation/react/components/sync-summary/SyncSummary.tsx b/src/presentation/react/components/sync-summary/SyncSummary.tsx deleted file mode 100644 index aaf9b8cf1..000000000 --- a/src/presentation/react/components/sync-summary/SyncSummary.tsx +++ /dev/null @@ -1,319 +0,0 @@ -import { - Accordion, - AccordionDetails, - AccordionSummary, - DialogContent, - makeStyles, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - Tooltip, - Typography, -} from "@material-ui/core"; -import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; -import { ConfirmationDialog } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import ReactJson from "react-json-view"; -import { PublicInstance } from "../../../../domain/instance/entities/Instance"; -import { Store } from "../../../../domain/stores/entities/Store"; -import { - ErrorMessage, - SynchronizationResult, - SynchronizationStats, -} from "../../../../domain/synchronization/entities/SynchronizationResult"; -import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../locales"; -import SyncReport from "../../../../models/syncReport"; -import { useAppContext } from "../../contexts/AppContext"; - -const useStyles = makeStyles(theme => ({ - accordionHeading1: { - marginLeft: 30, - fontSize: theme.typography.pxToRem(15), - flexBasis: "55%", - flexShrink: 0, - }, - accordionHeading2: { - fontSize: theme.typography.pxToRem(15), - color: theme.palette.text.secondary, - }, - accordionDetails: { - padding: "4px 24px 4px", - }, - accordion: { - paddingBottom: "10px", - }, - tooltip: { - maxWidth: 650, - fontSize: "0.9em", - }, -})); - -export const formatStatusTag = (value: string) => { - const text = _.startCase(_.toLower(value)); - const color = - value === "ERROR" || value === "FAILURE" || value === "NETWORK ERROR" - ? "#e53935" - : value === "DONE" || value === "SUCCESS" || value === "OK" - ? "#7cb342" - : "#3e2723"; - - return {text}; -}; - -const buildSummaryTable = (stats: SynchronizationStats[]) => { - return ( -
    - - - {i18n.t("Type")} - {i18n.t("Imported")} - {i18n.t("Updated")} - {i18n.t("Deleted")} - {i18n.t("Ignored")} - {i18n.t("Total")} - - - - {stats.map(({ type, imported, updated, deleted, ignored, total }, i) => ( - - {type} - {imported} - {updated} - {deleted} - {ignored} - - {total || _.sum([imported, deleted, ignored, updated])} - - - ))} - -
    - ); -}; - -const buildDataStatsTable = (type: SynchronizationType, stats: any[], classes: any) => { - const elementName = type === "aggregated" ? i18n.t("Data element") : i18n.t("Program"); - - return ( - - - - {elementName} - {i18n.t("Number of entries")} - {type === "events" && {i18n.t("Org units")}} - - - - {stats.map(({ dataElement, program, count, orgUnits }, i) => ( - - {dataElement || program} - {count} - {type === "events" && ( - - {`${_.take(orgUnits, 3).join(", ")} ${ - orgUnits.length > 3 ? "and more" : "" - }`} - - )} - - ))} - -
    - ); -}; - -const buildMessageTable = (messages: ErrorMessage[]) => { - return ( - - - - {i18n.t("Identifier")} - {i18n.t("Type")} - {i18n.t("Property")} - {i18n.t("Message")} - - - - {messages.map(({ id, type, property, message }, i) => ( - - {id} - {type} - {property} - {message} - - ))} - -
    - ); -}; - -const getTypeName = (reportType: SynchronizationType, syncType: string) => { - switch (reportType) { - case "aggregated": - return syncType === "events" ? i18n.t("Program Indicators") : i18n.t("Aggregated"); - case "events": - return i18n.t("Events"); - case "metadata": - return i18n.t("Metadata"); - case "deleted": - return i18n.t("Deleted"); - default: - return i18n.t("Unknown"); - } -}; - -interface SyncSummaryProps { - response: SyncReport; - onClose: () => void; -} - -const getOriginName = (source: PublicInstance | Store) => { - if ((source as Store).token) { - const store = source as Store; - return store.account + " - " + store.repository; - } else { - const instance = source as PublicInstance; - return instance.name; - } -}; - -const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { - const { api } = useAppContext(); - const classes = useStyles(); - const [results, setResults] = useState([]); - - useEffect(() => { - response.loadSyncResults(api).then(setResults); - }, [api, response]); - - if (results.length === 0) return null; - return ( - - - {results.map( - ( - { - origin, - instance, - status, - typeStats = [], - stats, - message, - errors, - type, - originPackage, - }, - i - ) => ( - - }> - - {`Type: ${getTypeName(type, response.syncReport.type)}`} -
    - {origin && `${i18n.t("Origin")}: ${getOriginName(origin)}`} - {origin &&
    } - {originPackage && - `${i18n.t("Origin package")}: ${originPackage.name}`} - {originPackage &&
    } - {`${i18n.t("Destination instance")}: ${instance.name}`} -
    - - {`${i18n.t("Status")}: `} - {formatStatusTag(status)} - -
    - - - {i18n.t("Summary")} - - - {message && ( - - {message} - - )} - - {stats && ( - - {buildSummaryTable([ - ...typeStats, - { ...stats, type: i18n.t("Total") }, - ])} - - )} - - {errors && errors.length > 0 && ( -
    - - - {i18n.t("Messages")} - - - - {buildMessageTable(_.take(errors, 10))} - -
    - )} -
    - ) - )} - - {response.syncReport.dataStats && ( - - }> - - {i18n.t("Data Statistics")} - - - - - {buildDataStatsTable( - response.syncReport.type, - response.syncReport.dataStats, - classes - )} - - - )} - - - }> - - {i18n.t("JSON Response")} - - - - - - - -
    -
    - ); -}; - -export default SyncSummary; diff --git a/src/presentation/react/components/sync-wizard/Steps.ts b/src/presentation/react/components/sync-wizard/Steps.ts deleted file mode 100644 index ee3125991..000000000 --- a/src/presentation/react/components/sync-wizard/Steps.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { WizardStep } from "d2-ui-components"; -import i18n from "../../../../locales"; -import SyncRule from "../../../../models/syncRule"; -import GeneralInfoStep from "./common/GeneralInfoStep"; -import InstanceSelectionStep from "./common/InstanceSelectionStep"; -import MetadataSelectionStep from "./common/MetadataSelectionStep"; -import SchedulerStep from "./common/SchedulerStep"; -import SummaryStep from "./common/SummaryStep"; -import AggregationStep from "./data/AggregationStep"; -import CategoryOptionsSelectionStep from "./data/CategoryOptionsSelectionStep"; -import EventsSelectionStep from "./data/EventsSelectionStep"; -import OrganisationUnitsSelectionStep from "./data/OrganisationUnitsSelectionStep"; -import PeriodSelectionStep from "./data/PeriodSelectionStep"; -import MetadataIncludeExcludeStep from "./metadata/MetadataIncludeExcludeStep"; -import MetadataFilterRulesStep from "./common/MetadataFilterRulesStep"; - -export interface SyncWizardStep extends WizardStep { - validationKeys: string[]; - showOnSyncDialog?: boolean; - hidden?: (syncRule: SyncRule) => boolean; -} - -export interface SyncWizardStepProps { - syncRule: SyncRule; - onChange: (syncRule: SyncRule) => void; - onCancel: () => void; -} - -const commonSteps: { - [key: string]: SyncWizardStep; -} = { - generalInfo: { - key: "general-info", - label: i18n.t("General info"), - component: GeneralInfoStep, - validationKeys: ["name"], - }, - instanceSelection: { - key: "instance-selection", - label: i18n.t("Instance Selection"), - component: InstanceSelectionStep, - validationKeys: ["targetInstances"], - showOnSyncDialog: true, - }, - scheduler: { - key: "scheduler", - label: i18n.t("Scheduling"), - component: SchedulerStep, - validationKeys: ["frequency", "enabled"], - description: i18n.t("Configure the scheduling frequency for the synchronization rule"), - warning: i18n.t( - "This step is optional and requires an external server with the metadata synchronization script properly configured" - ), - help: [ - i18n.t( - "This step allows to schedule background metadata synchronization jobs in a remote server." - ), - i18n.t( - "You can either select a pre-defined frequency from the drop-down menu or you enter a custom cron expression." - ), - "A cron expression is a string comprising six fields separated by white space that represents a routine.", - i18n.t("Second (0 - 59)"), - i18n.t("Minute (0 - 59)"), - i18n.t("Hour (0 - 23)"), - i18n.t("Day of the month (1 - 31)"), - i18n.t("Month (1 - 12)"), - i18n.t("Day of the week (1 - 7) (Monday to Sunday)"), - i18n.t( - "An asterisk (*) matches all possibilities. For instance, if we want to run a rule every day we would use asterisks for day of the month, day of the week, and month of the year to match all values." - ), - i18n.t( - "A wildcard (?) means no specific value and only works for day of the month or day of the week. For example, if you want to execute a rule on a particular day (10th) but you don't care about what day of the week that is, you would use ? in the day of the week field." - ), - ].join("\n"), - }, - summary: { - key: "summary", - label: i18n.t("Summary"), - component: SummaryStep, - validationKeys: [], - showOnSyncDialog: true, - }, - aggregation: { - key: "aggregation", - label: i18n.t("Aggregation"), - component: AggregationStep, - validationKeys: ["dataSyncAggregation"], - showOnSyncDialog: true, - }, -}; - -export const metadataSteps: SyncWizardStep[] = [ - commonSteps.generalInfo, - { - key: "metadata", - label: i18n.t("Metadata"), - component: MetadataSelectionStep, - validationKeys: [], - }, - { - key: "filter-rules", - label: i18n.t("Filter rules"), - component: MetadataFilterRulesStep, - validationKeys: ["metadata"], - }, - { - key: "dependencies-selection", - label: i18n.t("Select dependencies"), - component: MetadataIncludeExcludeStep, - validationKeys: ["metadataIncludeExclude"], - showOnSyncDialog: true, - }, - commonSteps.instanceSelection, - commonSteps.scheduler, - commonSteps.summary, -]; - -export const deletedSteps: SyncWizardStep[] = [commonSteps.instanceSelection]; - -export const aggregatedSteps: SyncWizardStep[] = [ - commonSteps.generalInfo, - { - key: "data-elements", - label: i18n.t("Data elements"), - component: MetadataSelectionStep, - validationKeys: ["metadataIds"], - showOnSyncDialog: false, - }, - { - key: "organisations-units", - label: i18n.t("Organisation units"), - component: OrganisationUnitsSelectionStep, - validationKeys: ["dataSyncOrganisationUnits"], - showOnSyncDialog: true, - }, - { - key: "period", - label: i18n.t("Period"), - component: PeriodSelectionStep, - validationKeys: ["dataSyncStartDate", "dataSyncEndDate"], - showOnSyncDialog: true, - }, - { - key: "category-options", - label: i18n.t("Category options"), - component: CategoryOptionsSelectionStep, - validationKeys: ["categoryOptionIds"], - showOnSyncDialog: true, - }, - { - ...commonSteps.aggregation, - warning: i18n.t( - "If aggregation is enabled, the synchronization will use the Analytics endpoint and group data by organisation units children and the chosen time periods." - ), - }, - commonSteps.instanceSelection, - commonSteps.scheduler, - commonSteps.summary, -]; - -export const eventsSteps: SyncWizardStep[] = [ - commonSteps.generalInfo, - { - key: "organisations-units", - label: i18n.t("Organisation units"), - component: OrganisationUnitsSelectionStep, - validationKeys: ["dataSyncOrganisationUnits"], - showOnSyncDialog: true, - }, - { - key: "programs", - label: i18n.t("Programs"), - component: MetadataSelectionStep, - validationKeys: ["metadataIds"], - showOnSyncDialog: false, - }, - { - key: "period", - label: i18n.t("Period"), - component: PeriodSelectionStep, - validationKeys: ["dataSyncStartDate", "dataSyncEndDate"], - showOnSyncDialog: true, - }, - { - key: "events", - label: i18n.t("Events"), - component: EventsSelectionStep, - validationKeys: ["dataSyncEvents"], - showOnSyncDialog: true, - }, - { - ...commonSteps.aggregation, - warning: i18n.t( - "If aggregation is enabled, the synchronization will use the Analytics endpoint and group data by organisation units children and the chosen time periods. Program indicators are only included during a synchronization if aggregation is enabled." - ), - }, - commonSteps.instanceSelection, - commonSteps.scheduler, - commonSteps.summary, -]; diff --git a/src/presentation/react/components/sync-wizard/SyncWizard.tsx b/src/presentation/react/components/sync-wizard/SyncWizard.tsx deleted file mode 100644 index c44977ed9..000000000 --- a/src/presentation/react/components/sync-wizard/SyncWizard.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Wizard, WizardStep } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useRef } from "react"; -import { useLocation } from "react-router-dom"; -import SyncRule from "../../../../models/syncRule"; -import { getValidationMessages } from "../../../../utils/old-validations"; -import { getMetadata } from "../../../../utils/synchronization"; -import { useAppContext } from "../../contexts/AppContext"; -import { aggregatedSteps, deletedSteps, eventsSteps, metadataSteps } from "./Steps"; - -interface SyncWizardProps { - syncRule: SyncRule; - isDialog?: boolean; - onChange?(syncRule: SyncRule): void; - onCancel?(): void; -} - -const config = { - metadata: metadataSteps, - aggregated: aggregatedSteps, - events: eventsSteps, - deleted: deletedSteps, -}; - -const SyncWizard: React.FC = ({ - syncRule, - isDialog = false, - onChange = _.noop, - onCancel = _.noop, -}) => { - const location = useLocation(); - const { api } = useAppContext(); - const memoizedRule = useRef(syncRule); - - const steps = config[syncRule.type] - .filter(({ showOnSyncDialog }) => !isDialog || showOnSyncDialog) - .filter(({ hidden }) => !hidden || !hidden(syncRule)) - .map(step => ({ - ...step, - props: { - syncRule, - onCancel, - onChange, - }, - })); - - const onStepChangeRequest = async (_currentStep: WizardStep, newStep: WizardStep) => { - const index = _(steps).findIndex(step => step.key === newStep.key); - const validationMessages = _.take(steps, index).map(({ validationKeys }) => - getValidationMessages(syncRule, validationKeys) - ); - - return _.flatten(validationMessages); - }; - - // This effect should only run in the first load - useEffect(() => { - getMetadata(api, memoizedRule.current.metadataIds, "id").then(metadata => { - const types = _.keys(metadata); - onChange( - memoizedRule.current - .updateMetadataTypes(types) - .updateDataSyncEnableAggregation( - types.includes("indicators") || types.includes("programIndicators") - ) - ); - }); - }, [api, onChange, memoizedRule]); - - const urlHash = location.hash.slice(1); - const stepExists = steps.find(step => step.key === urlHash); - const firstStepKey = steps.map(step => step.key)[0]; - const initialStepKey = stepExists ? urlHash : firstStepKey; - - return ( - - ); -}; - -export default SyncWizard; diff --git a/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx deleted file mode 100644 index e2b8a8f2e..000000000 --- a/src/presentation/react/components/sync-wizard/common/GeneralInfoStep.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { makeStyles, TextField } from "@material-ui/core"; -import React, { useCallback, useState } from "react"; -import { Instance } from "../../../../../domain/instance/entities/Instance"; -import { Store } from "../../../../../domain/stores/entities/Store"; -import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; -import { Dictionary } from "../../../../../types/utils"; -import { getValidationMessages } from "../../../../../utils/old-validations"; -import { - InstanceSelectionDropdown, - InstanceSelectionOption, -} from "../../instance-selection-dropdown/InstanceSelectionDropdown"; -import { SyncWizardStepProps } from "../Steps"; - -export const GeneralInfoStep = ({ syncRule, onChange }: SyncWizardStepProps) => { - const classes = useStyles(); - - const [errors, setErrors] = useState>({}); - - const onChangeField = useCallback( - (field: keyof SyncRule) => { - return (event: React.ChangeEvent<{ value: unknown }>) => { - const newRule = syncRule.update({ [field]: event.target.value }); - const messages = getValidationMessages(newRule, [field]); - - setErrors(errors => ({ ...errors, [field]: messages.join("\n") })); - onChange(newRule); - }; - }, - [syncRule, onChange] - ); - - const onChangeInstance = useCallback( - (_type: InstanceSelectionOption, instance?: Instance | Store) => { - const originInstance = instance?.id ?? "LOCAL"; - const targetInstances = originInstance === "LOCAL" ? [] : ["LOCAL"]; - - onChange( - syncRule - .updateBuilder({ originInstance }) - .updateTargetInstances(targetInstances) - .updateMetadataIds([]) - .updateExcludedIds([]) - ); - }, - [syncRule, onChange] - ); - - return ( - - - - - -
    - -
    - - -
    - ); -}; - -const useStyles = makeStyles({ - row: { - marginBottom: 25, - }, -}); - -export default GeneralInfoStep; diff --git a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx b/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx deleted file mode 100644 index f31038cbb..000000000 --- a/src/presentation/react/components/sync-wizard/common/InstanceSelectionStep.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { makeStyles, Typography } from "@material-ui/core"; -import { MultiSelector } from "d2-ui-components"; -import React, { useEffect, useState } from "react"; -import { Instance } from "../../../../../domain/instance/entities/Instance"; -import i18n from "../../../../../locales"; -import { useAppContext } from "../../../contexts/AppContext"; -import SyncParamsSelector from "../../sync-params-selector/SyncParamsSelector"; -import { SyncWizardStepProps } from "../Steps"; - -export const buildInstanceOptions = (instances: Instance[]) => { - return instances.map(instance => ({ - value: instance.id, - text: instance.username - ? i18n.t("{{name}} ({{url}}) with user {{username}}") - : i18n.t("{{name}} ({{url}}) with logged user"), - })); -}; - -const InstanceSelectionStep: React.FC = ({ syncRule, onChange }) => { - const { d2, compositionRoot } = useAppContext(); - const classes = useStyles(); - - const [selectedOptions, setSelectedOptions] = useState(syncRule.targetInstances); - const [targetInstances, setTargetInstances] = useState([]); - const instanceOptions = buildInstanceOptions(targetInstances); - - const includeCurrentUrlAndTypeIsEvents = (selectedinstanceIds: string[]) => { - return ( - syncRule.type === "events" && - selectedinstanceIds - .map(id => targetInstances.find(instance => instance.id === id)?.url) - .includes(compositionRoot.instances.getApi().baseUrl) - ); - }; - - const changeInstances = (instances: string[]) => { - setSelectedOptions(instances); - - if (includeCurrentUrlAndTypeIsEvents(instances)) { - onChange( - syncRule.updateTargetInstances(instances).updateDataParams({ - ...syncRule.dataParams, - generateNewUid: true, - }) - ); - } else { - onChange(syncRule.updateTargetInstances(instances)); - } - }; - - useEffect(() => { - compositionRoot.instances.list().then(setTargetInstances); - }, [compositionRoot]); - - return ( - - {syncRule.originInstance === "LOCAL" ? ( - - ) : ( - - {i18n.t("Destination")}: {i18n.t("This instance")} - - )} - - - - ); -}; - -const useStyles = makeStyles({ - advancedOptionsTitle: { - fontWeight: 500, - }, -}); - -export default InstanceSelectionStep; diff --git a/src/presentation/react/components/sync-wizard/common/MetadataFilterRulesStep.tsx b/src/presentation/react/components/sync-wizard/common/MetadataFilterRulesStep.tsx deleted file mode 100644 index 497e26d84..000000000 --- a/src/presentation/react/components/sync-wizard/common/MetadataFilterRulesStep.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { useCallback } from "react"; -import FilterRulesTable, { FilterRulesTableProps } from "../../filter-rules-table/FilterRulesTable"; -import { SyncWizardStepProps } from "../Steps"; - -const MetadataFilterRulesStep: React.FC = props => { - const { syncRule, onChange } = props; - const setFilterRules = useCallback( - filterRules => { - onChange(syncRule.updateFilterRules(filterRules)); - }, - [syncRule, onChange] - ); - - return ; -}; - -export default React.memo(MetadataFilterRulesStep); diff --git a/src/presentation/react/components/sync-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/components/sync-wizard/common/MetadataSelectionStep.tsx deleted file mode 100644 index 2722db989..000000000 --- a/src/presentation/react/components/sync-wizard/common/MetadataSelectionStep.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import { Instance } from "../../../../../domain/instance/entities/Instance"; -import i18n from "../../../../../locales"; -import { metadataModels } from "../../../../../models/dhis/factory"; -import { - AggregatedDataElementModel, - EventProgramWithDataElementsModel, - EventProgramWithIndicatorsModel, -} from "../../../../../models/dhis/mapping"; -import { - DataElementGroupModel, - DataElementGroupSetModel, - DataSetModel, - IndicatorModel, -} from "../../../../../models/dhis/metadata"; -import { getMetadata } from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../contexts/AppContext"; -import MetadataTable from "../../metadata-table/MetadataTable"; -import { SyncWizardStepProps } from "../Steps"; - -const config = { - metadata: { - models: metadataModels, - childrenKeys: undefined, - }, - aggregated: { - models: [ - DataSetModel, - AggregatedDataElementModel, - DataElementGroupModel, - DataElementGroupSetModel, - IndicatorModel, - ], - childrenKeys: ["dataElements", "dataElementGroups"], - }, - events: { - models: [EventProgramWithDataElementsModel, EventProgramWithIndicatorsModel], - childrenKeys: ["dataElements", "programIndicators"], - }, - deleted: { - models: [], - childrenKeys: undefined, - }, -}; - -export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizardStepProps) { - const { api, compositionRoot } = useAppContext(); - const snackbar = useSnackbar(); - - const [metadataIds, updateMetadataIds] = useState([]); - const [remoteInstance, setRemoteInstance] = useState(); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(false); - - const { models, childrenKeys } = config[syncRule.type]; - - const changeSelection = (newMetadataIds: string[], newExclusionIds: string[]) => { - const additions = _.difference(newMetadataIds, metadataIds); - if (additions.length > 0) { - snackbar.info( - i18n.t("Selected {{difference}} elements", { difference: additions.length }), - { - autoHideDuration: 1000, - } - ); - } - - const removals = _.difference(metadataIds, newMetadataIds); - if (removals.length > 0) { - snackbar.info( - i18n.t("Removed {{difference}} elements", { - difference: Math.abs(removals.length), - }), - { autoHideDuration: 1000 } - ); - } - - getMetadata(api, newMetadataIds, "id").then(metadata => { - const types = _.keys(metadata); - onChange( - syncRule - .updateMetadataIds(newMetadataIds) - .updateExcludedIds(newExclusionIds) - .updateMetadataTypes(types) - .updateDataSyncEnableAggregation( - types.includes("indicators") || types.includes("programIndicators") - ) - ); - }); - - updateMetadataIds(newMetadataIds); - }; - - useEffect(() => { - compositionRoot.instances.getById(syncRule.originInstance).then(result => { - result.match({ - success: instance => { - setRemoteInstance(instance); - setLoading(false); - }, - error: () => { - snackbar.error(i18n.t("Instance not found")); - setLoading(false); - setError(true); - }, - }); - }); - }, [compositionRoot, snackbar, syncRule]); - - if (loading || error) return null; - - return ( - - ); -} diff --git a/src/presentation/react/components/sync-wizard/common/SchedulerStep.jsx b/src/presentation/react/components/sync-wizard/common/SchedulerStep.jsx deleted file mode 100644 index 170383de1..000000000 --- a/src/presentation/react/components/sync-wizard/common/SchedulerStep.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import { DropDown, TextField } from "@dhis2/d2-ui-core"; -import { FormBuilder } from "@dhis2/d2-ui-forms"; -import PropTypes from "prop-types"; -import React from "react"; -import i18n from "../../../../../locales"; -import isValidCronExpression from "../../../../../utils/validCronExpression"; -import { Toggle } from "../../toggle/Toggle"; - -const cronExpressions = [ - { displayName: i18n.t("Every day"), id: "0 0 0 ? * *" }, - { displayName: i18n.t("Every month"), id: "0 0 0 1 1/1 ?" }, - { displayName: i18n.t("Every three months"), id: "0 0 0 1 1/3 ?" }, - { displayName: i18n.t("Every six months"), id: "0 0 0 1 1/6 ?" }, - { displayName: i18n.t("Every year"), id: "0 0 0 1 1 ?" }, -]; - -const SchedulerStep = ({ syncRule, onChange }) => { - const selectedCron = cronExpressions.find(({ id }) => id === syncRule.frequency); - - const updateFields = (field, value) => { - if (field === "enabled") { - onChange(syncRule.updateEnabled(value)); - } else if (field === "frequency" || field === "frequencyDropdown") { - const enabled = syncRule.enabled || !!value; - onChange(syncRule.updateFrequency(value || "").updateEnabled(enabled)); - } - }; - - const fields = [ - { - name: "enabled", - value: syncRule.enabled, - component: Toggle, - props: { - label: i18n.t("Enabled"), - style: { width: "100%" }, - }, - validators: [], - }, - { - name: "frequencyDropdown", - value: selectedCron?.value ?? "", - component: DropDown, - props: { - hintText: syncRule.readableFrequency || i18n.t("Select frequency template"), - menuItems: cronExpressions, - includeEmpty: true, - emptyLabel: i18n.t(""), - style: { width: "100%", marginTop: 20 }, - }, - validators: [], - }, - { - name: "frequency", - value: syncRule.frequency, - component: TextField, - props: { - floatingLabelText: i18n.t("Cron expression"), - style: { width: "100%" }, - changeEvent: "onBlur", - }, - validators: [ - { - message: i18n.t("Cron expression must be valid"), - validator(value) { - return !value || isValidCronExpression(value); - }, - }, - ], - }, - ]; - - return ; -}; - -SchedulerStep.propTypes = { - syncRule: PropTypes.object.isRequired, - onChange: PropTypes.func.isRequired, -}; - -SchedulerStep.defaultProps = {}; - -export default SchedulerStep; diff --git a/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx b/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx deleted file mode 100644 index 4f694b7f2..000000000 --- a/src/presentation/react/components/sync-wizard/common/SummaryStep.jsx +++ /dev/null @@ -1,460 +0,0 @@ -import { Button, LinearProgress, makeStyles } from "@material-ui/core"; -import { ConfirmationDialog, useLoading, useSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import moment from "moment"; -import React, { useEffect, useMemo, useState } from "react"; -import { useHistory } from "react-router-dom"; -import { includeExcludeRulesFriendlyNames } from "../../../../../domain/metadata/entities/MetadataFriendlyNames"; -import { cleanOrgUnitPaths } from "../../../../../domain/synchronization/utils"; -import i18n from "../../../../../locales"; -import { getValidationMessages } from "../../../../../utils/old-validations"; -import { - availablePeriods, - getMetadata, - requestJSONDownload, -} from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../contexts/AppContext"; -import { buildAggregationItems } from "../data/AggregationStep"; -import { buildInstanceOptions } from "./InstanceSelectionStep"; -import { filterRuleToString } from "../../../../../domain/metadata/entities/FilterRule"; - -const LiEntry = ({ label, value, children }) => { - return ( -
  • - {label} - {value || children ? ": " : ""} - {value} - {children} -
  • - ); -}; - -const useStyles = makeStyles({ - saveButton: { - margin: 10, - backgroundColor: "#2b98f0", - color: "white", - }, - buttonContainer: { - display: "flex", - justifyContent: "space-between", - }, -}); - -const SaveStep = ({ syncRule, onCancel }) => { - const { api, compositionRoot } = useAppContext(); - - const snackbar = useSnackbar(); - const loading = useLoading(); - const classes = useStyles(); - const history = useHistory(); - - const [cancelDialogOpen, setCancelDialogOpen] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [metadata, updateMetadata] = useState({}); - const [targetInstances, setTargetInstances] = useState([]); - const instanceOptions = buildInstanceOptions(targetInstances); - - const openCancelDialog = () => setCancelDialogOpen(true); - - const closeCancelDialog = () => setCancelDialogOpen(false); - - const name = syncRule.isOnDemand() - ? `Rule generated on ${moment().format("YYYY-MM-DD HH:mm:ss")}` - : syncRule.name; - - const save = async () => { - setIsSaving(true); - - const errors = getValidationMessages(syncRule); - if (errors.length > 0) { - snackbar.error(errors.join("\n")); - } else { - const newSyncRule = await syncRule.updateName(name).save(api); - history.push(`/sync-rules/${newSyncRule.type}/edit/${newSyncRule.id}`); - onCancel(); - } - - setIsSaving(false); - }; - - const downloadJSON = async () => { - loading.show(true, "Generating JSON file"); - const sync = compositionRoot.sync[syncRule.type](syncRule.toBuilder()); - const payload = await sync.buildPayload(); - requestJSONDownload(payload, syncRule); - loading.reset(); - }; - - useEffect(() => { - const ids = [ - ...syncRule.metadataIds, - ...syncRule.excludedIds, - ...syncRule.dataSyncAttributeCategoryOptions, - ...cleanOrgUnitPaths(syncRule.dataSyncOrgUnitPaths), - ]; - getMetadata(api, ids, "id,name").then(updateMetadata); - compositionRoot.instances.list().then(setTargetInstances); - }, [api, compositionRoot, syncRule]); - - const aggregationItems = useMemo(buildAggregationItems, []); - - const destinationInstances = useMemo( - () => - _.compact( - syncRule.targetInstances.map(id => instanceOptions.find(e => e.value === id)) - ), - [instanceOptions, syncRule.targetInstances] - ); - - const originInstance = useMemo( - () => instanceOptions.find(e => e.value === syncRule.originInstance), - [instanceOptions, syncRule.originInstance] - ); - - return ( - - - -
      - - - - - - - {originInstance && ( - - )} - - -
        - {destinationInstances.map(instanceOption => ( - - ))} -
      -
      - - {_.keys(metadata).map(metadataType => { - const items = metadata[metadataType].filter( - ({ id }) => !syncRule.excludedIds.includes(id) - ); - return ( - items.length > 0 && ( - -
        - {items.map(({ id, name }) => ( - - ))} -
      -
      - ) - ); - })} - - {syncRule.filterRules.length > 0 && ( - -
        - {_.sortBy(syncRule.filterRules, fr => fr.type).map(filterRule => { - return ( - - ); - })} -
      -
      - )} - - {syncRule.excludedIds.length > 0 && ( - -
        - {syncRule.excludedIds.map(id => { - const element = _(metadata).values().flatten().find({ id }); - - return ( - - ); - })} -
      -
      - )} - {syncRule.type === "metadata" && ( - - )} - - {syncRule.type === "metadata" && !syncRule.useDefaultIncludeExclude && ( - -
        - {_.keys(syncRule.metadataIncludeExcludeRules).map(key => { - const { - includeRules, - excludeRules, - } = syncRule.metadataIncludeExcludeRules[key]; - - return ( - -
          - {includeRules.length > 0 && ( - - {includeRules.map((includeRule, idx) => ( -
            - -
          - ))} -
          - )} - - {excludeRules.length > 0 && ( - - {excludeRules.map((excludeRule, idx) => ( -
            - -
          - ))} -
          - )} -
        -
        - ); - })} -
      -
      - )} - - {syncRule.type === "events" && ( - - )} - - {syncRule.dataSyncAllAttributeCategoryOptions && ( - - )} - - {syncRule.type !== "metadata" && ( - - {syncRule.dataSyncPeriod === "FIXED" && ( -
        - -
      - )} - {syncRule.dataSyncPeriod === "FIXED" && ( -
        - -
      - )} -
      - )} - - {syncRule.type !== "metadata" && ( - - )} - - {syncRule.type === "metadata" && ( - -
        - -
      -
        - -
      -
        - -
      -
        - -
      -
        - -
      -
      - )} - {(syncRule.type === "events" || syncRule.type === "aggregated") && ( - - {syncRule.type === "aggregated" && ( -
        - -
      - )} - {syncRule.type === "events" && ( -
        - -
      - )} -
        - -
      -
      - )} - - - - {syncRule.longFrequency && ( - - )} -
    - -
    -
    - {!syncRule.isOnDemand() && ( - - )} - -
    -
    - -
    -
    - - {isSaving && } -
    - ); -}; - -export default SaveStep; diff --git a/src/presentation/react/components/sync-wizard/data/AggregationStep.tsx b/src/presentation/react/components/sync-wizard/data/AggregationStep.tsx deleted file mode 100644 index 06f1c6d20..000000000 --- a/src/presentation/react/components/sync-wizard/data/AggregationStep.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { useSnackbar } from "d2-ui-components"; -import React, { useMemo } from "react"; -import { DataSyncAggregation } from "../../../../../domain/aggregated/types"; -import i18n from "../../../../../locales"; -import Dropdown from "../../dropdown/Dropdown"; -import { Toggle } from "../../toggle/Toggle"; -import { SyncWizardStepProps } from "../Steps"; - -const useStyles = makeStyles({ - dropdown: { - marginTop: 20, - marginLeft: -10, - }, - fixedPeriod: { - marginTop: 5, - marginBottom: -20, - }, - datePicker: { - marginTop: -10, - }, -}); - -export const buildAggregationItems = () => [ - { id: "DAILY", name: i18n.t("Daily"), format: "YYYYMMDD" }, - { id: "WEEKLY", name: i18n.t("Weekly"), format: "YYYY[W]W" }, - { id: "MONTHLY", name: i18n.t("Monthly"), format: "YYYYMM" }, - { id: "QUARTERLY", name: i18n.t("Quarterly"), format: "YYYY[Q]Q" }, - { id: "YEARLY", name: i18n.t("Yearly"), format: "YYYY" }, -]; - -const AggregationStep: React.FC = ({ syncRule, onChange }) => { - const classes = useStyles(); - const snackbar = useSnackbar(); - - const updateEnableAggregation = (value: boolean) => { - if (syncRule.metadataTypes.includes("indicators") && !value) { - snackbar.warning( - i18n.t( - "Without aggregation, any data value related to an indicator will be ignored" - ) - ); - } else if (syncRule.metadataTypes.includes("programIndicators") && !value) { - snackbar.warning( - i18n.t( - "Without aggregation, program indicators will not be aggregated and synchronized" - ) - ); - } - onChange(syncRule.updateDataSyncEnableAggregation(value).updateDataSyncAggregationType()); - }; - - const updateAggregationType = (value: DataSyncAggregation) => { - onChange( - syncRule.updateDataSyncEnableAggregation(true).updateDataSyncAggregationType(value) - ); - }; - - const aggregationItems = useMemo(buildAggregationItems, []); - - return ( - - - - {syncRule.dataSyncEnableAggregation && ( -
    - -
    - )} -
    - ); -}; - -export default AggregationStep; diff --git a/src/presentation/react/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx b/src/presentation/react/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx deleted file mode 100644 index 89bc47963..000000000 --- a/src/presentation/react/components/sync-wizard/data/CategoryOptionsSelectionStep.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useD2ApiData } from "d2-api"; -import { MultiSelector } from "d2-ui-components"; -import _ from "lodash"; -import React, { useMemo } from "react"; -import i18n from "../../../../../locales"; -import { useAppContext } from "../../../contexts/AppContext"; -import { Toggle } from "../../toggle/Toggle"; -import { SyncWizardStepProps } from "../Steps"; - -const CategoryOptionsSelectionStep: React.FC = ({ syncRule, onChange }) => { - const { d2, api } = useAppContext(); - - const { data } = useD2ApiData( - api.models.categoryOptionCombos.get({ - paging: false, - fields: { id: true, name: true }, - filter: { - "categoryCombo.dataDimensionType": { eq: "ATTRIBUTE" }, - }, - }) - ); - - const options = useMemo( - () => - _.uniqBy( - _.map(data?.objects ?? [], ({ name }) => ({ value: name, text: name })), - "value" - ), - [data] - ); - - const selected = useMemo( - () => - _(syncRule.dataSyncAttributeCategoryOptions) - .map(id => _.find(data?.objects, { id })?.name) - .uniq() - .compact() - .value(), - [data, syncRule] - ); - - const updateSyncAll = (value: boolean) => { - onChange( - syncRule - .updateDataSyncAllAttributeCategoryOptions(value) - .updateDataSyncAttributeCategoryOptions(undefined) - ); - }; - - const changeAttributeCategoryOptions = (selectedNames: string[]) => { - const attributeCategoryOptions = _(selectedNames) - .map(name => _.filter(data?.objects, { name })) - .flatten() - .map(({ id }) => id) - .value(); - - onChange(syncRule.updateDataSyncAttributeCategoryOptions(attributeCategoryOptions)); - }; - - return ( - - - {!syncRule.dataSyncAllAttributeCategoryOptions && ( - - )} - - ); -}; - -export default CategoryOptionsSelectionStep; diff --git a/src/presentation/react/components/sync-wizard/data/EventsSelectionStep.tsx b/src/presentation/react/components/sync-wizard/data/EventsSelectionStep.tsx deleted file mode 100644 index f23b4dfd9..000000000 --- a/src/presentation/react/components/sync-wizard/data/EventsSelectionStep.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { Typography } from "@material-ui/core"; -import { ObjectsTable, ObjectsTableDetailField, TableColumn, TableState } from "d2-ui-components"; -import _ from "lodash"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { ProgramEvent } from "../../../../../domain/events/entities/ProgramEvent"; -import { DataElement, Program } from "../../../../../domain/metadata/entities/MetadataEntities"; -import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; -import { useAppContext } from "../../../contexts/AppContext"; -import Dropdown from "../../dropdown/Dropdown"; -import { Toggle } from "../../toggle/Toggle"; -import { SyncWizardStepProps } from "../Steps"; - -interface ProgramEventObject extends ProgramEvent { - [key: string]: any; -} - -type CustomProgram = Program & { - programStages?: { programStageDataElements: { dataElement: DataElement }[] }[]; -}; - -export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardStepProps) { - const { compositionRoot } = useAppContext(); - - const [memoizedSyncRule] = useState(syncRule); - const [objects, setObjects] = useState(); - const [programs, setPrograms] = useState([]); - const [programFilter, changeProgramFilter] = useState(""); - const [error, setError] = useState(); - - useEffect(() => { - const sync = compositionRoot.sync.events(memoizedSyncRule.toBuilder()); - sync.extractMetadata().then(({ programs = [] }) => setPrograms(programs)); - }, [memoizedSyncRule, compositionRoot]); - - useEffect(() => { - if (programs.length === 0) return; - compositionRoot.events - .list( - { - ...memoizedSyncRule.dataParams, - allEvents: true, - }, - programs.map(({ id }) => id) - ) - .then(setObjects) - .catch(setError); - }, [compositionRoot, memoizedSyncRule, programs]); - - const handleTableChange = useCallback( - (tableState: TableState) => { - const { selection } = tableState; - onChange(syncRule.updateDataSyncEvents(selection.map(({ id }) => id))); - }, - [onChange, syncRule] - ); - - const updateSyncAll = useCallback( - (value: boolean) => { - onChange(syncRule.updateDataSyncAllEvents(value).updateDataSyncEvents(undefined)); - }, - [onChange, syncRule] - ); - - const addToSelection = useCallback( - (ids: string[]) => { - const oldSelection = _.difference(syncRule.dataSyncEvents, ids); - const newSelection = _.difference(ids, syncRule.dataSyncEvents); - - onChange(syncRule.updateDataSyncEvents([...oldSelection, ...newSelection])); - }, - [onChange, syncRule] - ); - - const columns: TableColumn[] = useMemo( - () => [ - { name: "id" as const, text: i18n.t("UID"), sortable: true }, - { - name: "program" as const, - text: i18n.t("Program"), - sortable: true, - getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program, - }, - { name: "orgUnitName" as const, text: i18n.t("Organisation unit"), sortable: true }, - { name: "eventDate" as const, text: i18n.t("Event date"), sortable: true }, - { - name: "lastUpdated" as const, - text: i18n.t("Last updated"), - sortable: true, - hidden: true, - }, - { name: "status" as const, text: i18n.t("Status"), sortable: true }, - { name: "storedBy" as const, text: i18n.t("Stored by"), sortable: true }, - ], - [programs] - ); - - const details: ObjectsTableDetailField[] = useMemo( - () => [ - { name: "id" as const, text: i18n.t("UID") }, - { - name: "program" as const, - text: i18n.t("Program"), - getValue: ({ program }) => _.find(programs, { id: program })?.name ?? program, - }, - { name: "orgUnitName" as const, text: i18n.t("Organisation unit") }, - { name: "created" as const, text: i18n.t("Created") }, - { name: "lastUpdated" as const, text: i18n.t("Last updated") }, - { name: "eventDate" as const, text: i18n.t("Event date") }, - { name: "dueDate" as const, text: i18n.t("Due date") }, - { name: "status" as const, text: i18n.t("Status") }, - { name: "storedBy" as const, text: i18n.t("Stored by") }, - ], - [programs] - ); - - const actions = useMemo( - () => [ - { - name: "select", - text: i18n.t("Select"), - primary: true, - multiple: true, - onClick: addToSelection, - isActive: () => false, - }, - ], - [addToSelection] - ); - - const filterComponents = useMemo( - () => ( - - ), - [programFilter, programs] - ); - - const additionalColumns = useMemo(() => { - const program = _.find(programs, { id: programFilter }); - const dataElements = _(program?.programStages ?? []) - .map(({ programStageDataElements }) => - programStageDataElements.map(({ dataElement }) => dataElement) - ) - .flatten() - .value(); - - return dataElements.map(({ id, displayFormName }) => ({ - name: id, - text: displayFormName, - sortable: true, - hidden: true, - getValue: (row: ProgramEvent) => { - return _.find(row.dataValues, { dataElement: id })?.value ?? "-"; - }, - })); - }, [programFilter, programs]); - - const filteredObjects = - objects?.filter(({ program }) => !programFilter || program === programFilter) ?? []; - - if (error) { - console.error(error); - return ( - - {i18n.t("An error ocurred while trying to access the required events")} - - ); - } - - return ( - - - {!syncRule.dataSyncAllEvents && ( - - rows={filteredObjects} - loading={objects === undefined} - columns={[...columns, ...additionalColumns]} - details={details} - actions={actions} - forceSelectionColumn={true} - onChange={handleTableChange} - selection={syncRule.dataSyncEvents?.map(id => ({ id })) ?? []} - filterComponents={filterComponents} - /> - )} - - ); -} diff --git a/src/presentation/react/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx b/src/presentation/react/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx deleted file mode 100644 index 34f9ae5f4..000000000 --- a/src/presentation/react/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { makeStyles, Typography } from "@material-ui/core"; -import CircularProgress from "@material-ui/core/CircularProgress"; -import { OrgUnitsSelector } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import i18n from "../../../../../locales"; -import { useAppContext } from "../../../contexts/AppContext"; -import { SyncWizardStepProps } from "../Steps"; - -const useStyles = makeStyles({ - loading: { - display: "flex", - justifyContent: "center", - }, -}); - -const OrganisationUnitsSelectionStep: React.FC = ({ syncRule, onChange }) => { - const { api, compositionRoot } = useAppContext(); - const classes = useStyles(); - const [orgUnitRootIds, setOrgUnitRootIds] = useState(); - - useEffect(() => { - compositionRoot.instances - .getOrgUnitRoots() - .then(roots => roots.map(({ id }) => id)) - .then(setOrgUnitRootIds); - }, [compositionRoot]); - - const changeSelection = (orgUnitsPaths: string[]) => { - onChange(syncRule.updateDataSyncOrgUnitPaths(orgUnitsPaths).updateDataSyncEvents([])); - }; - - if (!orgUnitRootIds) { - return ( -
    - -
    - ); - } else if (_.isEmpty(orgUnitRootIds)) { - return {i18n.t("You do not have assigned any organisation unit")}; - } else { - return ( - - ); - } -}; - -export default OrganisationUnitsSelectionStep; diff --git a/src/presentation/react/components/sync-wizard/data/PeriodSelectionStep.tsx b/src/presentation/react/components/sync-wizard/data/PeriodSelectionStep.tsx deleted file mode 100644 index c06d1f5af..000000000 --- a/src/presentation/react/components/sync-wizard/data/PeriodSelectionStep.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { DataSyncPeriod } from "../../../../../domain/aggregated/types"; -import PeriodSelection, { ObjectWithPeriod } from "../../period-selection/PeriodSelection"; -import { SyncWizardStepProps } from "../Steps"; - -const PeriodSelectionStep: React.FC = ({ syncRule, onChange }) => { - const updatePeriod = useCallback( - (period: DataSyncPeriod) => { - onChange( - syncRule - .updateDataSyncPeriod(period) - .updateDataSyncStartDate(undefined) - .updateDataSyncEndDate(undefined) - .updateDataSyncEvents([]) - ); - }, - [onChange, syncRule] - ); - - const updateStartDate = useCallback( - (date: Date | null) => { - onChange(syncRule.updateDataSyncStartDate(date ?? undefined).updateDataSyncEvents([])); - }, - [onChange, syncRule] - ); - - const updateEndDate = useCallback( - (date: Date | null) => { - onChange(syncRule.updateDataSyncEndDate(date ?? undefined).updateDataSyncEvents([])); - }, - [onChange, syncRule] - ); - - const onFieldChange = useCallback( - (field: keyof ObjectWithPeriod, value: ObjectWithPeriod[keyof ObjectWithPeriod]) => { - switch (field) { - case "period": - return updatePeriod(value as ObjectWithPeriod["period"]); - case "startDate": - return updateStartDate((value as ObjectWithPeriod["startDate"]) || null); - case "endDate": - return updateEndDate((value as ObjectWithPeriod["endDate"]) || null); - } - }, - [updatePeriod, updateStartDate, updateEndDate] - ); - - const objectWithPeriod = useMemo(() => { - return { - period: syncRule.dataSyncPeriod, - startDate: syncRule.dataSyncStartDate || undefined, - endDate: syncRule.dataSyncEndDate || undefined, - }; - }, [syncRule]); - - return ; -}; - -export default PeriodSelectionStep; diff --git a/src/presentation/react/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx b/src/presentation/react/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx deleted file mode 100644 index 0f245b143..000000000 --- a/src/presentation/react/components/sync-wizard/metadata/MetadataIncludeExcludeStep.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import { makeStyles } from "@material-ui/core"; -import { D2SchemaProperties } from "d2-api/schemas"; -import { MultiSelector, withSnackbar } from "d2-ui-components"; -import _ from "lodash"; -import React, { useEffect, useState } from "react"; -import { MetadataPackage } from "../../../../../domain/metadata/entities/MetadataEntities"; -import { includeExcludeRulesFriendlyNames } from "../../../../../domain/metadata/entities/MetadataFriendlyNames"; -import i18n from "../../../../../locales"; -import { D2Model } from "../../../../../models/dhis/default"; -import { modelFactory } from "../../../../../models/dhis/factory"; -import { getMetadata } from "../../../../../utils/synchronization"; -import { useAppContext } from "../../../contexts/AppContext"; -import Dropdown, { DropdownOption } from "../../dropdown/Dropdown"; -import { Toggle } from "../../toggle/Toggle"; -import { SyncWizardStepProps } from "../Steps"; - -const useStyles = makeStyles({ - includeExcludeContainer: { - display: "flex", - flexDirection: "column", - alignItems: "flex-start", - marginTop: "20px", - }, - multiselectorContainer: { - width: "100%", - }, -}); - -const MetadataIncludeExcludeStep: React.FC = ({ syncRule, onChange }) => { - const classes = useStyles(); - const { d2, api } = useAppContext(); - - const [modelSelectItems, setModelSelectItems] = useState([]); - const [models, setModels] = useState([]); - const [selectedType, setSelectedType] = useState(""); - - useEffect(() => { - getMetadata(api, syncRule.metadataIds, "id,name").then((metadata: MetadataPackage) => { - const models = _.keys(metadata).map((type: string) => { - return modelFactory(type); - }); - - const options = models - .map((model: typeof D2Model) => api.models[model.getCollectionName()].schema) - .map((schema: D2SchemaProperties) => ({ - name: schema.displayName, - id: schema.name, - })); - - setModels(models); - setModelSelectItems(options); - }); - }, [d2, api, syncRule]); - - const { includeRules = [], excludeRules = [] } = - syncRule.metadataIncludeExcludeRules[selectedType] || {}; - const allRules = [...includeRules, ...excludeRules]; - const ruleOptions = allRules.map(rule => ({ - value: rule, - text: includeExcludeRulesFriendlyNames[rule] || rule, - })); - - const changeUseDefaultIncludeExclude = (useDefault: boolean) => { - onChange( - useDefault - ? syncRule.markToUseDefaultIncludeExclude() - : syncRule.markToNotUseDefaultIncludeExclude(models) - ); - }; - - const changeModelName = (modelName: string) => { - setSelectedType(modelName); - }; - - const changeInclude = (currentIncludeRules: any) => { - const type: string = selectedType; - - const oldIncludeRules: string[] = includeRules; - - const ruleToExclude = _.difference(oldIncludeRules, currentIncludeRules); - const ruleToInclude = _.difference(currentIncludeRules, oldIncludeRules); - - if (ruleToInclude.length > 0) { - onChange(syncRule.moveRuleFromExcludeToInclude(type, ruleToInclude)); - } else if (ruleToExclude.length > 0) { - onChange(syncRule.moveRuleFromIncludeToExclude(type, ruleToExclude)); - } - }; - - return ( - - - {!syncRule.useDefaultIncludeExclude && ( -
    - - - {selectedType && ( -
    - -
    - )} -
    - )} -
    - ); -}; - -export default withSnackbar(MetadataIncludeExcludeStep); diff --git a/src/presentation/react/components/test-wrapper/TestWrapper.tsx b/src/presentation/react/components/test-wrapper/TestWrapper.tsx deleted file mode 100644 index 123460e87..000000000 --- a/src/presentation/react/components/test-wrapper/TestWrapper.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import _ from "lodash"; -import React from "react"; -import { - concatStrings, - generateTestId, - isClassComponent, - recursiveMap, - removeParentheses, - wrapType, -} from "./utils"; - -interface TestWrapperProps { - namespace?: string; - attributeName?: string; - componentParent?: string; -} - -const dataTestDictionary = new Map(); - -/** - * A wrapper that recursively adds `data-test` attributes to children and render components. - * - * Disclaimer: - * - This only "modern" functional components (HOCs and class components might not work as expected) - * - * Implementation based on: - * - https://github.com/dennismorello/react-test-attributes/blob/master/src/components/TestAttribute.tsx - * - https://github.com/ctrlplusb/react-tree-walker/blob/master/src/index.js - */ -export const TestWrapper: React.FC = ({ - children, - namespace, - attributeName, - componentParent, -}) => { - const testAttributeName = attributeName || `data-test`; - - function withTestAttribute(nodes: React.ReactNode, parentId?: string) { - const node = React.Children.only(nodes) as any; - const { type, props } = node; - if (isClassComponent(type)) return node; - - const id = generateTestId(node); - const className = removeParentheses(type.displayName || type.name || type); - const testAttribute = concatStrings([className, namespace, componentParent, parentId, id]); - const children = _.flatten([props.children]); - const element = props["data-test-wrapped"] - ? node - : React.createElement(wrapType(type, parentId), props, ...children); - - const count = dataTestDictionary.get(testAttribute) ?? 0; - const testId = concatStrings([testAttribute, count > 1 ? String(count) : undefined]); - const dataTest = props[testAttributeName] ?? testId; - const isReactFragment = element.type.toString() === "Symbol(react.fragment)"; - // TODO: Disabled for now - // dataTestDictionary.set(testAttribute, count + 1); - - return isReactFragment - ? element - : React.cloneElement(element, { - [testAttributeName]: isReactFragment ? undefined : dataTest, - }); - } - - return ( - - {process.env.REACT_APP_CYPRESS ? recursiveMap(children, withTestAttribute) : children} - - ); -}; diff --git a/src/presentation/react/components/test-wrapper/utils.tsx b/src/presentation/react/components/test-wrapper/utils.tsx deleted file mode 100644 index 6340a857a..000000000 --- a/src/presentation/react/components/test-wrapper/utils.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import _ from "lodash"; -import React from "react"; -import { TestWrapper } from "./TestWrapper"; -import memoize from "nano-memoize"; - -export function recursiveMap(children: React.ReactNode, fn: Function, parentId?: string) { - const result: any[] = []; - React.Children.forEach(children, child => { - const id = concatStrings([parentId, generateTestId(child || {})]); - if (!React.isValidElement(child)) { - result.push(child); - return; - } - - const clone = child.props.children - ? React.cloneElement(child, { - children: recursiveMap(child.props.children, fn, id), - }) - : child; - - result.push(fn(clone, id)); - }); - - return result; -} - -export function concatStrings( - strings: (string | undefined)[], - separator = "-", - duplicates = false -) { - return _(strings) - .map(string => (duplicates ? string : string?.split(separator))) - .flatten() - .compact() - .uniq() - .join(separator); -} - -export function generateTestId({ props = {}, key }: { props?: any; key?: string }) { - const id = _.kebabCase( - _.toLower( - props.id || - props.title || - props.name || - props.label || - props["aria-label"] || - key || - props.value - ) - ); - return id ? id : undefined; -} - -export function removeParentheses(string: string) { - if (typeof string !== "string") return undefined; - const result = string.substring(string.lastIndexOf("(") + 1, string.indexOf(")")); - return result ? result : string; -} - -export function isClassComponent(component: any) { - return typeof component === "function" && !!component.prototype.isReactComponent ? true : false; -} - -export const wrapType = memoize((type: any, parentId?: string) => { - return typeof type === "function" && !isClassComponent(type) - ? (...props: any[]) => { - return ( - - {type(...props)} - - ); - } - : type; -}); diff --git a/src/presentation/react/components/text-field-on-blur/TextFieldOnBlur.tsx b/src/presentation/react/components/text-field-on-blur/TextFieldOnBlur.tsx deleted file mode 100644 index c60ed2099..000000000 --- a/src/presentation/react/components/text-field-on-blur/TextFieldOnBlur.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { TextField, TextFieldProps } from "@material-ui/core"; -import React, { useCallback, useEffect, useRef, useState } from "react"; - -/* Wrap TextField with those two changes: - -- props.onChange is called with the string, not the event. -- props.onChange is called on blur, not on every keystroke, this way the UI is much more responsive. -*/ - -type TextFieldOnBlurProps = Omit & { - value: string; - onChange(newValue: string): void; -}; - -const TextFieldOnBlur: React.FC = props => { - const { onChange } = props; - // Use props.value as initial value for the initial state but also react to changes from the parent - const propValue = props.value; - const prevPropValue = useRef(propValue); - const [value, setValue] = useState(propValue); - - useEffect(() => { - if (propValue !== prevPropValue.current) { - console.log("upchange", { value, propValue, prev: prevPropValue.current }); - setValue(propValue); - prevPropValue.current = propValue; - } - }, [propValue, prevPropValue, value]); - - const callParentOnChange = useCallback(() => { - onChange(value); - }, [value, onChange]); - - const setValueFromEvent = useCallback( - (ev: React.ChangeEvent<{ value: string }>) => { - setValue(ev.target.value); - }, - [setValue] - ); - - return ( - - ); -}; - -export default React.memo(TextFieldOnBlur); diff --git a/src/presentation/react/components/toggle/Toggle.tsx b/src/presentation/react/components/toggle/Toggle.tsx deleted file mode 100644 index 7d341be12..000000000 --- a/src/presentation/react/components/toggle/Toggle.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { FormControlLabel, Switch } from "@material-ui/core"; -import _ from "lodash"; -import React from "react"; - -interface InputParameters { - disabled?: boolean; - label: string; - onChange?: Function; - onValueChange?: Function; - value: boolean; -} - -export const Toggle = ({ - label, - onChange = _.noop, - onValueChange = _.noop, - value, - disabled, -}: InputParameters) => ( - { - onChange({ target: { value: e.target.checked } }); - onValueChange(e.target.checked); - }} - checked={value} - color="primary" - /> - } - label={label} - /> -); diff --git a/src/presentation/react/contexts/AppContext.ts b/src/presentation/react/contexts/AppContext.ts deleted file mode 100644 index b7e68e6be..000000000 --- a/src/presentation/react/contexts/AppContext.ts +++ /dev/null @@ -1,20 +0,0 @@ -import React, { useContext } from "react"; -import { CompositionRoot } from "../../CompositionRoot"; -import { D2Api } from "../../../types/d2-api"; - -export interface AppContext { - api: D2Api; - d2: object; - compositionRoot: CompositionRoot; -} - -export const AppContext = React.createContext(null); - -export function useAppContext() { - const context = useContext(AppContext); - if (context) { - return context; - } else { - throw new Error("Context not found"); - } -} diff --git a/src/presentation/react/hooks/useOpenState.ts b/src/presentation/react/hooks/useOpenState.ts deleted file mode 100644 index d8f85fb4c..000000000 --- a/src/presentation/react/hooks/useOpenState.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useCallback, useState } from "react"; - -export function useOpenState(initialValue?: Value) { - const [value, setValue] = useState(initialValue); - const open = useCallback((value: Value) => setValue(value), [setValue]); - const close = useCallback(() => setValue(undefined), [setValue]); - const isOpen = !!value; - - return { isOpen, value, open, close }; -} diff --git a/src/presentation/react/hooks/useQueryParams.ts b/src/presentation/react/hooks/useQueryParams.ts deleted file mode 100644 index 985e47638..000000000 --- a/src/presentation/react/hooks/useQueryParams.ts +++ /dev/null @@ -1,7 +0,0 @@ -import qs from "qs"; -import { useLocation } from "react-router-dom"; - -export function useQueryParams() { - const location = useLocation(); - return qs.parse(location.search, { ignoreQueryPrefix: true }); -} diff --git a/src/presentation/react/themes/dhis2-legacy.theme.js b/src/presentation/react/themes/dhis2-legacy.theme.js deleted file mode 100644 index a702c0419..000000000 --- a/src/presentation/react/themes/dhis2-legacy.theme.js +++ /dev/null @@ -1,62 +0,0 @@ -import { - cyan100, - cyan500, - cyan700, - darkBlack, - grey100, - grey400, - grey500, - orange500, - white, -} from "material-ui/styles/colors"; -import { fade } from "material-ui/utils/colorManipulator"; -import Spacing from "material-ui/styles/spacing"; -import getMuiTheme from "material-ui/styles/getMuiTheme"; - -const theme = { - spacing: Spacing, - fontFamily: "Roboto, sans-serif", - palette: { - primary1Color: cyan500, - primary2Color: cyan700, - primary3Color: cyan100, - accent1Color: orange500, - accent2Color: grey100, - accent3Color: grey500, - textColor: darkBlack, - alternateTextColor: white, - canvasColor: white, - borderColor: grey400, - disabledColor: fade(darkBlack, 0.3), - }, -}; - -function createAppTheme(style) { - return { - sideBar: { - backgroundColor: "#F3F3F3", - backgroundColorItem: "transparent", - backgroundColorItemActive: style.palette.accent2Color, - textColor: style.palette.textColor, - textColorActive: "#276696", - borderStyle: "1px solid #e1e1e1", - }, - forms: { - minWidth: 350, - maxWidth: 900, - }, - formFields: { - secondaryColor: style.palette.accent4Color, - }, - tabs: { - backgroundColor: "#E4E4E4", - inkBarColor: style.palette.accent1Color, - textColor: "#666666", - }, - }; -} - -const muiTheme = getMuiTheme(theme); -const appTheme = createAppTheme(muiTheme); - -export default Object.assign({}, muiTheme, appTheme); diff --git a/src/presentation/react/themes/dhis2.theme.js b/src/presentation/react/themes/dhis2.theme.js deleted file mode 100644 index 0f264f688..000000000 --- a/src/presentation/react/themes/dhis2.theme.js +++ /dev/null @@ -1,91 +0,0 @@ -import { createMuiTheme } from "@material-ui/core/styles"; - -// Color palette from https://projects.invisionapp.com/share/A7LT4TJYETS#/screens/302550228_Color -export const colors = { - accentPrimary: "#1976d2", - accentPrimaryDark: "#004BA0", - accentPrimaryLight: "#63A4FF", - accentPrimaryLightest: "#EAF4FF", - - accentSecondary: "#fb8c00", - accentSecondaryLight: "#f57c00", - accentSecondaryDark: "#ff9800", - - black: "#000000", - greyBlack: "#494949", - grey: "#9E9E9E", - greyLight: "#E0E0E0", - greyDisabled: "#8E8E8E", - blueGrey: "#ECEFF1", - snow: "#F4F6F8", - white: "#FFFFFF", // Not included in palette! - - negative: "#E53935", - warning: "#F19C02", - positive: "#3D9305", - info: "#EAF4FF", -}; - -export const palette = { - common: { - white: colors.white, - black: colors.black, - }, - action: { - active: colors.greyBlack, - disabled: colors.greyDisabled, - }, - text: { - primary: colors.black, - secondary: colors.greyBlack, - disabled: colors.greyDisabled, - hint: colors.grey, - }, - primary: { - main: colors.accentPrimary, - dark: colors.accentPrimaryDark, - light: colors.accentPrimaryLight, - lightest: colors.accentPrimaryLightest, // Custom extension, not used by default - // contrastText: 'white', - }, - secondary: { - main: colors.accentSecondary, - light: colors.accentSecondaryLight, - dark: colors.accentSecondaryDark, - contrastText: "#fff", - }, - error: { - main: colors.negative, // This is automatically expanded to main/light/dark/contrastText, what do we use here? - }, - status: { - //Custom colors collection, not used by default in MUI - negative: colors.negative, - warning: colors.warning, - positive: colors.positive, - info: colors.info, - }, - background: { - paper: colors.white, - default: colors.snow, - grey: "#FCFCFC", - hover: colors.greyLight, - }, - divider: colors.greyLight, - shadow: colors.grey, -}; - -export const muiTheme = createMuiTheme({ - colors, - palette, - typography: { - fontFamily: "Roboto, Helvetica, Arial, sans-serif", - useNextVariants: true, - }, - overrides: { - MuiDivider: { - light: { - backgroundColor: palette.divider, // No light dividers for now - }, - }, - }, -}); From ae9c2c0115ddf20e1d4c92f7c851fad7db718532 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 11:06:14 +0100 Subject: [PATCH 048/163] Create MSF Settings Dialog --- i18n/en.pot | 4 ++-- .../msf-Settings/MSFSettingsDialog.tsx | 23 +++++++++++++++++++ .../msf-aggregate-data/pages/MSFHomePage.tsx | 23 ++++++++++++++++++- 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 122ea4599..732bd4d50 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-12-03T09:49:40.074Z\n" -"PO-Revision-Date: 2020-12-03T09:49:40.074Z\n" +"POT-Creation-Date: 2020-12-03T10:05:26.349Z\n" +"PO-Revision-Date: 2020-12-03T10:05:26.349Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx new file mode 100644 index 000000000..c329d0990 --- /dev/null +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -0,0 +1,23 @@ +import { ConfirmationDialog } from "d2-ui-components"; +import React from "react"; +import i18n from "../../../../../locales"; + +export interface MSFSettingsDialogProps { + onClose(): void; + onSave(): void; +} + +export const MSFSettingsDialog: React.FC = ({ onClose, onSave }) => { + return ( + onSave()} + cancelText={i18n.t("Cancel")} + saveText={i18n.t("Save")} + > + ); +}; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 34fe7f7dd..330eb7189 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -7,6 +7,7 @@ import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; import { useAppContext } from "../../../react/core/contexts/AppContext"; +import { MSFSettingsDialog } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; export interface PeriodFilter { period: DataSyncPeriod; @@ -19,6 +20,7 @@ export const MSFHomePage: React.FC = () => { const history = useHistory(); const [showPeriodDialog, setShowPeriodDialog] = useState(false); + const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); const [period, setPeriod] = useState({ period: "ALL" }); const [globalAdmin, setGlobalAdmin] = useState(false); const { api } = useAppContext(); @@ -31,7 +33,11 @@ export const MSFHomePage: React.FC = () => { const handleAdvancedSettings = () => { setShowPeriodDialog(true); }; - const handleMSFSettings = () => {}; + + const handleMSFSettings = () => { + setShowMSFSettingsDialog(true); + }; + const handleGoToDashboard = () => { history.push("/dashboard"); }; @@ -48,6 +54,14 @@ export const MSFHomePage: React.FC = () => { setPeriod(period); }; + const handleCloseMSFSettings = () => { + setShowMSFSettingsDialog(false); + }; + + const handleSaveMSFSettings = () => { + setShowMSFSettingsDialog(false); + }; + return ( @@ -128,6 +142,13 @@ export const MSFHomePage: React.FC = () => { onSave={handleSaveAdvancedSettings} /> )} + + {showMSFSettingsDialog && ( + + )} ); }; From fe7d6f099c0e06a08ac162c4ec75b73126fdd2ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 3 Dec 2020 12:14:25 +0100 Subject: [PATCH 049/163] Add title to period selection dialog --- i18n/en.pot | 4 ++-- .../webapp/msf-aggregate-data/pages/MSFHomePage.tsx | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 732bd4d50..5d6fde1ad 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-12-03T10:05:26.349Z\n" -"PO-Revision-Date: 2020-12-03T10:05:26.349Z\n" +"POT-Creation-Date: 2020-12-03T11:13:18.127Z\n" +"PO-Revision-Date: 2020-12-03T11:13:18.127Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 330eb7189..cd6194bbc 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -137,6 +137,7 @@ export const MSFHomePage: React.FC = () => { {showPeriodDialog && ( Date: Fri, 4 Dec 2020 07:41:33 +0100 Subject: [PATCH 050/163] Create Period value object --- i18n/en.pot | 4 +- src/domain/common/entities/Period.ts | 53 +++++++++++++++ src/domain/common/entities/Validations.ts | 5 ++ .../common/entities/__tests__/Period.spec.ts | 67 +++++++++++++++++++ .../PeriodSelectionDialog.tsx | 36 +++++++--- .../msf-aggregate-data/pages/MSFHomePage.tsx | 12 +--- 6 files changed, 157 insertions(+), 20 deletions(-) create mode 100644 src/domain/common/entities/Period.ts create mode 100644 src/domain/common/entities/__tests__/Period.spec.ts diff --git a/i18n/en.pot b/i18n/en.pot index 849206116..00288cddd 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-12-03T11:19:40.860Z\n" -"PO-Revision-Date: 2020-12-03T11:19:40.860Z\n" +"POT-Creation-Date: 2020-12-04T06:39:39.419Z\n" +"PO-Revision-Date: 2020-12-04T06:39:39.419Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/domain/common/entities/Period.ts b/src/domain/common/entities/Period.ts new file mode 100644 index 000000000..7a85a0d9c --- /dev/null +++ b/src/domain/common/entities/Period.ts @@ -0,0 +1,53 @@ +import { DataSyncPeriod } from "../../aggregated/types"; +import { Either } from "./Either"; +import { ModelValidation, validateModel, ValidationError } from "./Validations"; + +interface PeriodData { + type: DataSyncPeriod; + startDate?: Date; + endDate?: Date; +} + +export class Period { + public readonly type: DataSyncPeriod; + public readonly startDate?: Date; + public readonly endDate?: Date; + + private constructor(data: PeriodData) { + this.type = data.type; + this.startDate = data.startDate; + this.endDate = data.endDate; + } + + static create({ type, startDate, endDate }: PeriodData): Either { + const validations: ModelValidation[] = + type === "FIXED" + ? [ + { + property: "startDate", + validation: "hasValue", + alias: "start date", + }, + { + property: "endDate", + validation: "hasValue", + alias: "end date", + }, + ] + : []; + + const newPeriod = new Period({ + type: type ?? "ALL", + startDate, + endDate, + }); + + const errors = validateModel(newPeriod, validations); + + return errors.length > 0 ? Either.error(errors) : Either.success(newPeriod); + } + + static createDefault() { + return new Period({ type: "ALL" }); + } +} diff --git a/src/domain/common/entities/Validations.ts b/src/domain/common/entities/Validations.ts index 4476df7c9..d4f1079fb 100644 --- a/src/domain/common/entities/Validations.ts +++ b/src/domain/common/entities/Validations.ts @@ -23,6 +23,11 @@ const availableValidations = { getDescription: (field: string) => i18n.t("Field {{field}} cannot be blank", { field }), check: (value?: string) => !value?.trim(), }, + hasValue: { + error: "cannot_be_blank", + getDescription: (field: string) => i18n.t("Field {{field}} cannot be blank", { field }), + check: (value?: string) => !value, + }, hasItems: { error: "cannot_be_empty", getDescription: (field: string) => diff --git a/src/domain/common/entities/__tests__/Period.spec.ts b/src/domain/common/entities/__tests__/Period.spec.ts new file mode 100644 index 000000000..1d7bbfdb9 --- /dev/null +++ b/src/domain/common/entities/__tests__/Period.spec.ts @@ -0,0 +1,67 @@ +import { Period } from "../Period"; + +describe("Period", () => { + it("should return success creating from a non fixed type", () => { + const periodResult = Period.create({ type: "LAST_WEEK" }); + + periodResult.match({ + error: errors => fail(errors), + success: period => expect(period.type).toEqual("LAST_WEEK"), + }); + }); + it("should return success creating from a fixed type with start date and end date", () => { + const today = new Date(); + const tomorrow = new Date(today.getDate() + 1); + const periodResult = Period.create({ type: "FIXED", startDate: today, endDate: tomorrow }); + + periodResult.match({ + error: errors => fail(errors), + success: period => { + expect(period.type).toEqual("FIXED"); + expect(period.startDate).toEqual(today); + expect(period.endDate).toEqual(tomorrow); + }, + }); + }); + it("should return error creating from a fixed type with start date and end date without value", () => { + const periodResult = Period.create({ type: "FIXED" }); + + periodResult.match({ + error: errors => { + expect(errors.length).toBe(2); + expect(errors[0].error).toBe("cannot_be_blank"); + expect(errors[0].property).toBe("startDate"); + expect(errors[1].error).toBe("cannot_be_blank"); + expect(errors[1].property).toBe("endDate"); + }, + success: () => fail("should be fail"), + }); + }); + it("should return error creating from a fixed type with start date without value", () => { + const today = new Date(); + const tomorrow = new Date(today.getDate() + 1); + const periodResult = Period.create({ type: "FIXED", endDate: tomorrow }); + + periodResult.match({ + error: errors => { + expect(errors.length).toBe(1); + expect(errors[0].property).toBe("startDate"); + expect(errors[0].error).toBe("cannot_be_blank"); + }, + success: () => fail("should be fail"), + }); + }); + it("should return error creating from a fixed type with end date without value", () => { + const today = new Date(); + const periodResult = Period.create({ type: "FIXED", startDate: today }); + + periodResult.match({ + error: errors => { + expect(errors.length).toBe(1); + expect(errors[0].property).toBe("endDate"); + expect(errors[0].error).toBe("cannot_be_blank"); + }, + success: () => fail("should be fail"), + }); + }); +}); diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx index b2a82674f..353521404 100644 --- a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx @@ -1,15 +1,15 @@ import { Box, makeStyles, Theme } from "@material-ui/core"; -import { ConfirmationDialog } from "d2-ui-components"; +import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; import React, { useState } from "react"; +import { Period } from "../../../../../domain/common/entities/Period"; import i18n from "../../../../../locales"; -import { PeriodFilter } from "../../../../webapp/msf-aggregate-data/pages/MSFHomePage"; -import PeriodSelection from "../period-selection/PeriodSelection"; +import PeriodSelection, { ObjectWithPeriod } from "../period-selection/PeriodSelection"; export interface PeriodSelectionDialogProps { title?: string; - period: PeriodFilter; + period: Period; onClose(): void; - onSave(value: PeriodFilter): void; + onSave(period: Period): void; } export const PeriodSelectionDialog: React.FC = ({ @@ -19,7 +19,25 @@ export const PeriodSelectionDialog: React.FC = ({ period, }) => { const classes = useStyles(); - const [periodState, setPeriodState] = useState(period); + const snackbar = useSnackbar(); + const [objectWithPeriod, setObjectWithPeriod] = useState({ + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }); + + const handleSave = () => { + const periodValidation = Period.create({ + type: objectWithPeriod.period, + startDate: objectWithPeriod.startDate, + endDate: objectWithPeriod.endDate, + }); + + periodValidation.match({ + error: errors => snackbar.error(errors.map(error => error.description).join("\n")), + success: period => onSave(period), + }); + }; return ( = ({ fullWidth={true} title={title} onCancel={onClose} - onSave={() => onSave(periodState)} + onSave={() => handleSave()} cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index cd6194bbc..3f8635b5e 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -2,26 +2,20 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@materi import i18n from "d2-ui-components/locales"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { DataSyncPeriod } from "../../../../domain/aggregated/types"; +import { Period } from "../../../../domain/common/entities/Period"; import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; import { useAppContext } from "../../../react/core/contexts/AppContext"; import { MSFSettingsDialog } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; -export interface PeriodFilter { - period: DataSyncPeriod; - startDate?: Date; - endDate?: Date; -} - export const MSFHomePage: React.FC = () => { const classes = useStyles(); const history = useHistory(); const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); - const [period, setPeriod] = useState({ period: "ALL" }); + const [period, setPeriod] = useState(Period.createDefault()); const [globalAdmin, setGlobalAdmin] = useState(false); const { api } = useAppContext(); @@ -49,7 +43,7 @@ export const MSFHomePage: React.FC = () => { setShowPeriodDialog(false); }; - const handleSaveAdvancedSettings = (period: PeriodFilter) => { + const handleSaveAdvancedSettings = (period: Period) => { setShowPeriodDialog(false); setPeriod(period); }; From 47bf1ecc9c0da44cb486953a9195e62dd5ff1f0e Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 09:01:44 +0100 Subject: [PATCH 051/163] Update use-cases --- i18n/en.pot | 7 +- i18n/es.po | 5 +- i18n/fr.po | 5 +- i18n/pt.po | 5 +- src/data/reports/ReportsD2ApiRepository.ts | 9 +++ .../usecases/GetLocalInstanceUseCase.ts | 1 + .../reports/repositories/ReportsRepository.ts | 2 + .../usecases/DeleteSyncReportUseCase.ts | 11 ++++ .../reports/usecases/GetSyncReportUseCase.ts | 12 ++++ .../reports/usecases/GetSyncResultsUseCase.ts | 12 ++++ .../reports/usecases/ListSyncReportUseCase.ts | 64 +++++++++++++++++++ src/presentation/CompositionRoot.ts | 10 +++ .../components/sync-summary/SyncSummary.tsx | 8 +-- .../webapp/pages/history/HistoryPage.tsx | 57 +++++++---------- .../sync-rules-list/SyncRulesListPage.tsx | 21 +++--- 15 files changed, 161 insertions(+), 68 deletions(-) create mode 100644 src/domain/reports/usecases/DeleteSyncReportUseCase.ts create mode 100644 src/domain/reports/usecases/GetSyncReportUseCase.ts create mode 100644 src/domain/reports/usecases/GetSyncResultsUseCase.ts create mode 100644 src/domain/reports/usecases/ListSyncReportUseCase.ts diff --git a/i18n/en.pot b/i18n/en.pot index 173e135e4..8f16e313a 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-12-03T08:51:09.935Z\n" -"PO-Revision-Date: 2020-12-03T08:51:09.935Z\n" +"POT-Creation-Date: 2020-12-04T07:06:19.722Z\n" +"PO-Revision-Date: 2020-12-04T07:06:19.723Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1237,9 +1237,6 @@ msgstr "" msgid "Deleting History Notifications" msgstr "" -msgid "Failed to delete some history notifications" -msgstr "" - msgid "Successfully deleted {{count}} history notifications" msgid_plural "Successfully deleted {{count}} history notifications" msgstr[0] "" diff --git a/i18n/es.po b/i18n/es.po index 030514897..309e7e21a 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-12-03T08:51:09.935Z\n" +"POT-Creation-Date: 2020-12-04T07:06:19.722Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1244,9 +1244,6 @@ msgstr "" msgid "Deleting History Notifications" msgstr "" -msgid "Failed to delete some history notifications" -msgstr "" - msgid "Successfully deleted {{count}} history notifications" msgid_plural "Successfully deleted {{count}} history notifications" msgstr[0] "" diff --git a/i18n/fr.po b/i18n/fr.po index aadaf6bbf..b27aef39f 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-12-03T08:51:09.935Z\n" +"POT-Creation-Date: 2020-12-04T07:06:19.722Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1240,9 +1240,6 @@ msgstr "" msgid "Deleting History Notifications" msgstr "" -msgid "Failed to delete some history notifications" -msgstr "" - msgid "Successfully deleted {{count}} history notifications" msgid_plural "Successfully deleted {{count}} history notifications" msgstr[0] "" diff --git a/i18n/pt.po b/i18n/pt.po index aadaf6bbf..b27aef39f 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-12-03T08:51:09.935Z\n" +"POT-Creation-Date: 2020-12-04T07:06:19.722Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1240,9 +1240,6 @@ msgstr "" msgid "Deleting History Notifications" msgstr "" -msgid "Failed to delete some history notifications" -msgstr "" - msgid "Successfully deleted {{count}} history notifications" msgid_plural "Successfully deleted {{count}} history notifications" msgstr[0] "" diff --git a/src/data/reports/ReportsD2ApiRepository.ts b/src/data/reports/ReportsD2ApiRepository.ts index 3c21df5c0..e1c54c843 100644 --- a/src/data/reports/ReportsD2ApiRepository.ts +++ b/src/data/reports/ReportsD2ApiRepository.ts @@ -3,6 +3,7 @@ import { SynchronizationReport, SynchronizationReportData, } from "../../domain/reports/entities/SynchronizationReport"; +import { SynchronizationResult } from "../../domain/reports/entities/SynchronizationResult"; import { ReportsRepository } from "../../domain/reports/repositories/ReportsRepository"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Namespace } from "../storage/Namespaces"; @@ -24,6 +25,14 @@ export class ReportsD2ApiRepository implements ReportsRepository { return data ? SynchronizationReport.build(data) : undefined; } + public async getSyncResults(id: string): Promise { + const data = await this.storageClient.getObject( + `${Namespace.HISTORY}-${id}` + ); + + return data ?? []; + } + public async list(): Promise { const stores = await this.storageClient.listObjectsInCollection( Namespace.HISTORY diff --git a/src/domain/instance/usecases/GetLocalInstanceUseCase.ts b/src/domain/instance/usecases/GetLocalInstanceUseCase.ts index e9efcc81b..fb1e295c7 100644 --- a/src/domain/instance/usecases/GetLocalInstanceUseCase.ts +++ b/src/domain/instance/usecases/GetLocalInstanceUseCase.ts @@ -4,6 +4,7 @@ import { Instance } from "../entities/Instance"; export class GetLocalInstanceUseCase implements UseCase { constructor(private localInstance: Instance) {} + // TODO public async execute(): Promise { return this.localInstance; } diff --git a/src/domain/reports/repositories/ReportsRepository.ts b/src/domain/reports/repositories/ReportsRepository.ts index 34366bf01..349d7071f 100644 --- a/src/domain/reports/repositories/ReportsRepository.ts +++ b/src/domain/reports/repositories/ReportsRepository.ts @@ -1,5 +1,6 @@ import { Instance } from "../../instance/entities/Instance"; import { SynchronizationReport } from "../entities/SynchronizationReport"; +import { SynchronizationResult } from "../entities/SynchronizationResult"; export interface ReportsRepositoryConstructor { new (instance: Instance): ReportsRepository; @@ -7,6 +8,7 @@ export interface ReportsRepositoryConstructor { export interface ReportsRepository { getById(id: string): Promise; + getSyncResults(id: string): Promise; list(): Promise; save(report: SynchronizationReport): Promise; delete(id: string): Promise; diff --git a/src/domain/reports/usecases/DeleteSyncReportUseCase.ts b/src/domain/reports/usecases/DeleteSyncReportUseCase.ts new file mode 100644 index 000000000..b35cbd8ad --- /dev/null +++ b/src/domain/reports/usecases/DeleteSyncReportUseCase.ts @@ -0,0 +1,11 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; + +export class DeleteSyncReportUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(id: string): Promise { + await this.repositoryFactory.reportsRepository(this.localInstance).delete(id); + } +} diff --git a/src/domain/reports/usecases/GetSyncReportUseCase.ts b/src/domain/reports/usecases/GetSyncReportUseCase.ts new file mode 100644 index 000000000..a30c7f59e --- /dev/null +++ b/src/domain/reports/usecases/GetSyncReportUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationReport } from "../entities/SynchronizationReport"; + +export class GetSyncReportUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(id: string): Promise { + return this.repositoryFactory.reportsRepository(this.localInstance).getById(id); + } +} diff --git a/src/domain/reports/usecases/GetSyncResultsUseCase.ts b/src/domain/reports/usecases/GetSyncResultsUseCase.ts new file mode 100644 index 000000000..526535094 --- /dev/null +++ b/src/domain/reports/usecases/GetSyncResultsUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationResult } from "../entities/SynchronizationResult"; + +export class GetSyncResultsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(id: string): Promise { + return this.repositoryFactory.reportsRepository(this.localInstance).getSyncResults(id); + } +} diff --git a/src/domain/reports/usecases/ListSyncReportUseCase.ts b/src/domain/reports/usecases/ListSyncReportUseCase.ts new file mode 100644 index 000000000..1bae4512d --- /dev/null +++ b/src/domain/reports/usecases/ListSyncReportUseCase.ts @@ -0,0 +1,64 @@ +import _ from "lodash"; +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationReport } from "../entities/SynchronizationReport"; + +export interface ListSyncReportUseCaseParams { + paging?: boolean; + pageSize?: number; + page?: number; + sorting?: { field: keyof SynchronizationReport; order: "asc" | "desc" }; + filters: { statusFilter?: string; syncRuleFilter?: string; type?: string; search?: string }; +} + +export interface ListSyncReportUseCaseResult { + rows: SynchronizationReport[]; + pager: { total: number; page: number; pageSize: number; sorting: string[]; paging: boolean }; +} + +export class ListSyncReportUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute({ + paging = true, + pageSize = 25, + page = 1, + sorting = { field: "id", order: "asc" }, + filters, + }: ListSyncReportUseCaseParams): Promise { + const rawData = await this.repositoryFactory.reportsRepository(this.localInstance).list(); + + const { statusFilter, syncRuleFilter, type, search } = filters; + + const filteredData = search + ? _.filter(rawData, item => + _(item) + .values() + .map(value => (typeof value === "string" ? value : undefined)) + .compact() + .some(field => field.toLowerCase().includes(search.toLowerCase())) + ) + : rawData; + + const { field, order } = sorting; + const sortedData = _.orderBy( + filteredData, + [data => _.toLower(data[field] as string)], + [order] + ); + + const filteredObjects = _(sortedData) + .filter(e => (statusFilter ? e.status === statusFilter : true)) + .filter(e => (syncRuleFilter ? e.syncRule === syncRuleFilter : true)) + .filter(({ type: elementType = "metadata" }) => (type ? elementType === type : true)) + .value(); + + const total = filteredObjects.length; + const firstItem = paging ? (page - 1) * pageSize : 0; + const lastItem = paging ? firstItem + pageSize : total; + const rows = _.slice(filteredObjects, firstItem, lastItem); + + return { rows, pager: { page, pageSize, total, sorting: [field, order], paging } }; + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index a3e36fedd..35c8837ba 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -5,6 +5,7 @@ import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepositor import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepository"; import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; +import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; import { StorageDataStoreClient } from "../data/storage/StorageDataStoreClient"; import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; @@ -61,6 +62,10 @@ import { ImportPackageUseCase } from "../domain/packages/usecases/ImportPackageU import { ListPackagesUseCase } from "../domain/packages/usecases/ListPackagesUseCase"; import { ListStorePackagesUseCase } from "../domain/packages/usecases/ListStorePackagesUseCase"; import { PublishStorePackageUseCase } from "../domain/packages/usecases/PublishStorePackageUseCase"; +import { DeleteSyncReportUseCase } from "../domain/reports/usecases/DeleteSyncReportUseCase"; +import { GetSyncReportUseCase } from "../domain/reports/usecases/GetSyncReportUseCase"; +import { GetSyncResultsUseCase } from "../domain/reports/usecases/GetSyncResultsUseCase"; +import { ListSyncReportUseCase } from "../domain/reports/usecases/ListSyncReportUseCase"; import { SaveSyncReportUseCase } from "../domain/reports/usecases/SaveSyncReportUseCase"; import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; @@ -87,6 +92,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.EventsRepository, EventsD2ApiRepository); this.repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); this.repositoryFactory.bind(Repositories.FileRepository, FileD2Repository); + this.repositoryFactory.bind(Repositories.ReportsRepository, ReportsD2ApiRepository); this.repositoryFactory.bind( Repositories.MetadataRepository, MetadataJSONRepository, @@ -304,7 +310,11 @@ export class CompositionRoot { @cache() public get reports() { return getExecute({ + list: new ListSyncReportUseCase(this.repositoryFactory, this.localInstance), save: new SaveSyncReportUseCase(this.repositoryFactory, this.localInstance), + delete: new DeleteSyncReportUseCase(this.repositoryFactory, this.localInstance), + get: new GetSyncReportUseCase(this.repositoryFactory, this.localInstance), + getSyncResults: new GetSyncResultsUseCase(this.repositoryFactory, this.localInstance), }); } } diff --git a/src/presentation/react/components/sync-summary/SyncSummary.tsx b/src/presentation/react/components/sync-summary/SyncSummary.tsx index afcd16b2e..5decaf453 100644 --- a/src/presentation/react/components/sync-summary/SyncSummary.tsx +++ b/src/presentation/react/components/sync-summary/SyncSummary.tsx @@ -187,15 +187,13 @@ const getOriginName = (source: PublicInstance | Store) => { }; const SyncSummary = ({ response, onClose }: SyncSummaryProps) => { - const { api } = useAppContext(); + const { compositionRoot } = useAppContext(); const classes = useStyles(); const [results, setResults] = useState([]); useEffect(() => { - // TODO: Add use-case - //response.loadSyncResults(api).then(setResults); - setResults([]); - }, [api, response]); + compositionRoot.reports.getSyncResults(response.id).then(setResults); + }, [compositionRoot, response]); if (results.length === 0) return null; return ( diff --git a/src/presentation/webapp/pages/history/HistoryPage.tsx b/src/presentation/webapp/pages/history/HistoryPage.tsx index 1a8349751..8daa8a126 100644 --- a/src/presentation/webapp/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/pages/history/HistoryPage.tsx @@ -21,6 +21,7 @@ import { SynchronizationRule } from "../../../../domain/synchronization/entities import { SynchronizationType } from "../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../locales"; import SyncRule from "../../../../models/syncRule"; +import { promiseMap } from "../../../../utils/common"; import { getValueForCollection } from "../../../../utils/d2-ui-components"; import { isAppConfigurator } from "../../../../utils/permissions"; import Dropdown from "../../../react/components/dropdown/Dropdown"; @@ -68,7 +69,7 @@ const initialState = { }; const HistoryPage: React.FC = () => { - const { api } = useAppContext(); + const { api, compositionRoot } = useAppContext(); const snackbar = useSnackbar(); const loading = useLoading(); const history = useHistory(); @@ -76,7 +77,7 @@ const HistoryPage: React.FC = () => { const { title } = config[type]; const [syncRules, setSyncRules] = useState([]); - const [syncReport, setSyncReport] = useState(null); + const [syncReport, setSyncReport] = useState(); const [toDelete, setToDelete] = useState([]); const [selection, updateSelection] = useState([]); const [response, updateResponse] = useState<{ @@ -92,26 +93,30 @@ const HistoryPage: React.FC = () => { const updateTable = useCallback( (tableState?: TableState) => { - // TODO: Add use-case - /**SyncReport.list( - api, - { type, statusFilter, syncRuleFilter }, - tableState ?? initialState - ).then(updateResponse);**/ updateResponse({ rows: [], pager: {} }); updateSelection(oldSelection => tableState?.selection ?? oldSelection); + + const { pagination, sorting } = tableState ?? initialState; + compositionRoot.reports + .list({ + paging: true, + pageSize: pagination.pageSize, + page: pagination.page, + sorting, + filters: { type, statusFilter, syncRuleFilter }, + }) + .then(updateResponse); }, - [statusFilter, syncRuleFilter, type, updateSelection] + [statusFilter, syncRuleFilter, type, compositionRoot] ); useEffect(() => { SyncRule.list(api, { type }, { paging: false }).then(({ objects }) => setSyncRules(objects) ); - // TODO: Add use-case - //if (id) SyncReport.get(api, id).then(setSyncReport); + if (id) compositionRoot.reports.get(id).then(setSyncReport); isAppConfigurator(api).then(setAppConfigurator); - }, [api, id, type]); + }, [api, id, type, compositionRoot]); useEffect(() => { updateTable(); @@ -212,30 +217,16 @@ const HistoryPage: React.FC = () => { const confirmDelete = async () => { loading.show(true, i18n.t("Deleting History Notifications")); - const notifications = _(toDelete) - .map(id => _.find(response.rows, ["id", id])) - .compact() - .map(data => SynchronizationReport.build(data)) - .value(); - const results: any[] = []; - for (const notification of notifications) { - // TODO: Add use-case - //results.push(await notification.remove(api)); - console.log(notification, results); - } + await promiseMap(toDelete, id => compositionRoot.reports.delete(id)); loading.reset(); - if (_.some(results, ["status", false])) { - snackbar.error(i18n.t("Failed to delete some history notifications")); - } else { - snackbar.success( - i18n.t("Successfully deleted {{count}} history notifications", { - count: toDelete.length, - }) - ); - } + snackbar.success( + i18n.t("Successfully deleted {{count}} history notifications", { + count: toDelete.length, + }) + ); updateSelection([]); setToDelete([]); @@ -276,7 +267,7 @@ const HistoryPage: React.FC = () => { /> {!!syncReport && ( - setSyncReport(null)} /> + setSyncReport(undefined)} /> )} {toDelete.length > 0 && ( diff --git a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx index 558589149..48a16c4b9 100644 --- a/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/pages/sync-rules-list/SyncRulesListPage.tsx @@ -205,8 +205,6 @@ const SyncRulesPage: React.FC = () => { const confirmDelete = async () => { loading.show(true, i18n.t("Deleting Sync Rules")); - // TODO: Add use-case - /** const results = []; for (const id of toDelete) { const rule = await SyncRule.get(api, id); @@ -214,22 +212,19 @@ const SyncRulesPage: React.FC = () => { results.push(await rule.remove(api)); - const syncReports = await SyncReport.list( - api, - { type: rule.type, syncRuleFilter: id }, - {}, - false - ); + // TODO: Fully refactor with SyncRule + const syncReports = await compositionRoot.reports.list({ + filters: { type: rule.type, syncRuleFilter: id }, + paging: false, + }); for (const syncReportData of syncReports.rows) { - const syncReport = SyncReport.build({ + const syncReport = SynchronizationReport.build({ ...syncReportData, deletedSyncRuleLabel: deletedRuleLabel, }); - const syncResults = await syncReport.loadSyncResults(api); - syncReport.addSyncResult(syncResults[0]); - await syncReport.save(api); + await compositionRoot.reports.save(syncReport); } } @@ -239,7 +234,7 @@ const SyncRulesPage: React.FC = () => { snackbar.success( i18n.t("Successfully deleted {{count}} rules", { count: toDelete.length }) ); - }**/ + } loading.reset(); setToDelete([]); From eddd2ebf83a927667e7e652ba7f3819d9d369607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 4 Dec 2020 09:12:40 +0100 Subject: [PATCH 052/163] Rename dashboard title --- src/presentation/webapp/core/pages/home/HomePage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index 57d513bbf..da41506e2 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -245,7 +245,7 @@ const LandingPage: React.FC = ({ type }) => { return ( appVariantConfiguration[appVariant].includes(card.key) From eaaef80f1cf5ad66cd961a57c5039fa7a0da1850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 4 Dec 2020 09:13:33 +0100 Subject: [PATCH 053/163] Change sync progress paper to full width --- .../webapp/msf-aggregate-data/pages/MSFHomePage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 3f8635b5e..053bc42cc 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -157,8 +157,8 @@ const useStyles = makeStyles((theme: Theme) => ({ margin: "0 auto", }, log: { - width: "60%", - margin: theme.spacing(4), + width: "100%", + margin: theme.spacing(2), padding: theme.spacing(4), overflow: "auto", minHeight: 300, From 34347da3b9308034577b355539a229cce3680adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 4 Dec 2020 09:15:12 +0100 Subject: [PATCH 054/163] Update locales --- i18n/en.pot | 6 +++--- i18n/es.po | 4 ++-- i18n/fr.po | 4 ++-- i18n/pt.po | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 00288cddd..a278dd214 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-12-04T06:39:39.419Z\n" -"PO-Revision-Date: 2020-12-04T06:39:39.419Z\n" +"POT-Creation-Date: 2020-12-04T08:14:20.273Z\n" +"PO-Revision-Date: 2020-12-04T08:14:20.273Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1375,7 +1375,7 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" -msgid "Dashboard" +msgid "Admin Dashboard" msgstr "" msgid "Please fix the issues before testing the connection" diff --git a/i18n/es.po b/i18n/es.po index b8738906d..4a9460ae1 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-12-03T09:02:53.049Z\n" +"POT-Creation-Date: 2020-12-04T08:14:20.273Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1382,7 +1382,7 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" -msgid "Dashboard" +msgid "Admin Dashboard" msgstr "" msgid "Please fix the issues before testing the connection" diff --git a/i18n/fr.po b/i18n/fr.po index 73a3c5c7b..7e966d636 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-12-03T09:02:53.049Z\n" +"POT-Creation-Date: 2020-12-04T08:14:20.273Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1378,7 +1378,7 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" -msgid "Dashboard" +msgid "Admin Dashboard" msgstr "" msgid "Please fix the issues before testing the connection" diff --git a/i18n/pt.po b/i18n/pt.po index 73a3c5c7b..7e966d636 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-12-03T09:02:53.049Z\n" +"POT-Creation-Date: 2020-12-04T08:14:20.273Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1378,7 +1378,7 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" -msgid "Dashboard" +msgid "Admin Dashboard" msgstr "" msgid "Please fix the issues before testing the connection" From 111f033755c818d7f0411deaccb58f0485885474 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 09:16:49 +0100 Subject: [PATCH 055/163] Prettify and localize after merge --- i18n/en.pot | 28 +++++++++++++++++-- i18n/es.po | 26 ++++++++++++++++- i18n/fr.po | 26 ++++++++++++++++- i18n/pt.po | 26 ++++++++++++++++- .../module-list-table/ModuleListTable.tsx | 4 +-- .../ModulePackageListTable.tsx | 2 +- .../PackageImportDialog.tsx | 6 +++- .../package-list-table/PackageListTable.tsx | 6 ++-- .../components/packages-diff-dialog/utils.tsx | 2 +- .../webapp/core/pages/history/HistoryPage.tsx | 4 ++- .../core/pages/manual-sync/ManualSyncPage.tsx | 20 ++++++++++--- .../ModulePackageListPage.tsx | 2 +- .../NotificationsListPage.tsx | 4 +-- .../sync-rules-list/SyncRulesListPage.tsx | 15 ++++++++-- 14 files changed, 147 insertions(+), 24 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 8f16e313a..99e8cde33 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-12-04T07:06:19.722Z\n" -"PO-Revision-Date: 2020-12-04T07:06:19.723Z\n" +"POT-Creation-Date: 2020-12-04T08:16:36.678Z\n" +"PO-Revision-Date: 2020-12-04T08:16:36.678Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1186,6 +1186,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "MSF Settings" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1369,6 +1372,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Admin Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" @@ -1715,6 +1721,24 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Aggregate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 309e7e21a..52ba0b0d8 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-12-04T07:06:19.722Z\n" +"POT-Creation-Date: 2020-12-04T08:16:36.678Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1192,6 +1192,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "MSF Settings" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1376,6 +1379,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Admin Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" @@ -1722,6 +1728,24 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Aggregate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index b27aef39f..b951b0393 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-12-04T07:06:19.722Z\n" +"POT-Creation-Date: 2020-12-04T08:16:36.678Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1189,6 +1189,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "MSF Settings" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1372,6 +1375,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Admin Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" @@ -1718,6 +1724,24 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Aggregate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index b27aef39f..b951b0393 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-12-04T07:06:19.722Z\n" +"POT-Creation-Date: 2020-12-04T08:16:36.678Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1189,6 +1189,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "MSF Settings" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1372,6 +1375,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Admin Dashboard" +msgstr "" + msgid "Please fix the issues before testing the connection" msgstr "" @@ -1718,6 +1724,24 @@ msgid_plural "Are you sure you want to delete {{count}} rules?" msgstr[0] "" msgstr[1] "" +msgid "Aggregate Data For HMIS" +msgstr "" + +msgid "Aggregate Data" +msgstr "" + +msgid "Synchronization Progress" +msgstr "" + +msgid "Advanced Settings" +msgstr "" + +msgid "Go To Admin Dashboard" +msgstr "" + +msgid "Go to History" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx b/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx index 3c53d000d..fe5623ce2 100644 --- a/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx +++ b/src/presentation/react/core/components/module-list-table/ModuleListTable.tsx @@ -12,7 +12,7 @@ import { TableSelection, TableState, useLoading, - useSnackbar + useSnackbar, } from "d2-ui-components"; import _ from "lodash"; import React, { useCallback, useEffect, useMemo, useState } from "react"; @@ -27,7 +27,7 @@ import { useAppContext } from "../../contexts/AppContext"; import Dropdown from "../dropdown/Dropdown"; import { PullRequestCreation, - PullRequestCreationDialog + PullRequestCreationDialog, } from "../pull-request-creation-dialog/PullRequestCreationDialog"; import { SharingDialog } from "../sharing-dialog/SharingDialog"; import { NewPackageDialog } from "./NewPackageDialog"; diff --git a/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx b/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx index 5905aff01..b1f2a5a0b 100644 --- a/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx +++ b/src/presentation/react/core/components/module-package-list-table/ModulePackageListTable.tsx @@ -8,7 +8,7 @@ import Dropdown from "../dropdown/Dropdown"; import { InstanceSelectionConfig, InstanceSelectionDropdown, - InstanceSelectionOption + InstanceSelectionOption, } from "../instance-selection-dropdown/InstanceSelectionDropdown"; import { ModulesListTable } from "../module-list-table/ModuleListTable"; import { PackagesListTable } from "../package-list-table/PackageListTable"; diff --git a/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx b/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx index 955c46c20..6516bf518 100644 --- a/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx +++ b/src/presentation/react/core/components/package-import-dialog/PackageImportDialog.tsx @@ -6,7 +6,11 @@ import { Either } from "../../../../../domain/common/entities/Either"; import { NamedRef } from "../../../../../domain/common/entities/Ref"; import { JSONDataSource } from "../../../../../domain/instance/entities/JSONDataSource"; import { PackageImportRule } from "../../../../../domain/package-import/entities/PackageImportRule"; -import { PackageSource, isInstance, isStore } from "../../../../../domain/package-import/entities/PackageSource"; +import { + PackageSource, + isInstance, + isStore, +} from "../../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../../domain/package-import/mappers/ImportedPackageMapper"; import { Package } from "../../../../../domain/packages/entities/Package"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; diff --git a/src/presentation/react/core/components/package-list-table/PackageListTable.tsx b/src/presentation/react/core/components/package-list-table/PackageListTable.tsx index 332f218d3..86db15270 100644 --- a/src/presentation/react/core/components/package-list-table/PackageListTable.tsx +++ b/src/presentation/react/core/components/package-list-table/PackageListTable.tsx @@ -10,7 +10,7 @@ import { TableSelection, TableState, useLoading, - useSnackbar + useSnackbar, } from "d2-ui-components"; import _ from "lodash"; import React, { useCallback, useEffect, useMemo, useState } from "react"; @@ -23,7 +23,7 @@ import { ImportedPackage } from "../../../../../domain/package-import/entities/I import { isInstance, isStore, - PackageSource + PackageSource, } from "../../../../../domain/package-import/entities/PackageSource"; import { mapToImportedPackage } from "../../../../../domain/package-import/mappers/ImportedPackageMapper"; import { ListPackage, Package } from "../../../../../domain/packages/entities/Package"; @@ -40,7 +40,7 @@ import { InstallStatus, isPackageItem, PackageItem, - PackageModuleItem + PackageModuleItem, } from "./PackageModuleItem"; interface PackagesListTableProps extends ModulePackageListPageProps { diff --git a/src/presentation/react/core/components/packages-diff-dialog/utils.tsx b/src/presentation/react/core/components/packages-diff-dialog/utils.tsx index 1041183fa..19c643a8e 100644 --- a/src/presentation/react/core/components/packages-diff-dialog/utils.tsx +++ b/src/presentation/react/core/components/packages-diff-dialog/utils.tsx @@ -4,7 +4,7 @@ import { useCallback, useState } from "react"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import { FieldUpdate, - MetadataPackageDiff + MetadataPackageDiff, } from "../../../../../domain/packages/entities/MetadataPackageDiff"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; import i18n from "../../../../../locales"; diff --git a/src/presentation/webapp/core/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx index db82bcfa8..d1f8088f7 100644 --- a/src/presentation/webapp/core/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -26,7 +26,9 @@ import { getValueForCollection } from "../../../../../utils/d2-ui-components"; import { isAppConfigurator } from "../../../../../utils/permissions"; import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; -import SyncSummary, { formatStatusTag } from "../../../../react/core/components/sync-summary/SyncSummary"; +import SyncSummary, { + formatStatusTag, +} from "../../../../react/core/components/sync-summary/SyncSummary"; import { useAppContext } from "../../../../react/core/contexts/AppContext"; const config = { diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 26efd35b4..0794f66ce 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -3,7 +3,7 @@ import { ConfirmationDialog, ConfirmationDialogProps, useLoading, - useSnackbar + useSnackbar, } from "d2-ui-components"; import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; @@ -16,8 +16,17 @@ import { SynchronizationType } from "../../../../../domain/synchronization/entit import i18n from "../../../../../locales"; import { D2Model } from "../../../../../models/dhis/default"; import { metadataModels } from "../../../../../models/dhis/factory"; -import { AggregatedDataElementModel, DataSetWithDataElementsModel, EventProgramWithDataElementsModel, EventProgramWithIndicatorsModel, IndicatorMappedModel } from "../../../../../models/dhis/mapping"; -import { DataElementGroupModel, DataElementGroupSetModel } from "../../../../../models/dhis/metadata"; +import { + AggregatedDataElementModel, + DataSetWithDataElementsModel, + EventProgramWithDataElementsModel, + EventProgramWithIndicatorsModel, + IndicatorMappedModel, +} from "../../../../../models/dhis/mapping"; +import { + DataElementGroupModel, + DataElementGroupSetModel, +} from "../../../../../models/dhis/metadata"; import SyncRule from "../../../../../models/syncRule"; import { MetadataType } from "../../../../../utils/d2"; import { isAppConfigurator } from "../../../../../utils/permissions"; @@ -25,7 +34,10 @@ import DeletedObjectsTable from "../../../../react/core/components/delete-object import { InstanceSelectionOption } from "../../../../react/core/components/instance-selection-dropdown/InstanceSelectionDropdown"; import MetadataTable from "../../../../react/core/components/metadata-table/MetadataTable"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; -import { PullRequestCreation, PullRequestCreationDialog } from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import { + PullRequestCreation, + PullRequestCreationDialog, +} from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; import SyncDialog from "../../../../react/core/components/sync-dialog/SyncDialog"; import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; diff --git a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index e1925a848..df8e773cd 100644 --- a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -10,7 +10,7 @@ import { CreatePackageFromFileDialog } from "../../../../react/core/components/c import { ModulePackageListTable, PresentationOption, - ViewOption + ViewOption, } from "../../../../react/core/components/module-package-list-table/ModulePackageListTable"; import PackageImportDialog from "../../../../react/core/components/package-import-dialog/PackageImportDialog"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; diff --git a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index ac08dc4ba..6afeaa9b4 100644 --- a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -5,7 +5,7 @@ import { IconButton, makeStyles, Tooltip, - Typography + Typography, } from "@material-ui/core"; import { ObjectsTable, @@ -14,7 +14,7 @@ import { TableColumn, TableGlobalAction, useLoading, - useSnackbar + useSnackbar, } from "d2-ui-components"; import i18n from "d2-ui-components/locales"; import React, { useCallback, useEffect, useMemo, useState } from "react"; diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index 805538600..e6df1d1e9 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -14,7 +14,7 @@ import { TableSelection, TableState, useLoading, - useSnackbar + useSnackbar, } from "d2-ui-components"; import _ from "lodash"; import { Moment } from "moment"; @@ -28,11 +28,20 @@ import i18n from "../../../../../locales"; import SyncRule from "../../../../../models/syncRule"; import { getValueForCollection } from "../../../../../utils/d2-ui-components"; import { getValidationMessages } from "../../../../../utils/old-validations"; -import { getUserInfo, isAppConfigurator, isAppExecutor, isGlobalAdmin, UserInfo } from "../../../../../utils/permissions"; +import { + getUserInfo, + isAppConfigurator, + isAppExecutor, + isGlobalAdmin, + UserInfo, +} from "../../../../../utils/permissions"; import { requestJSONDownload } from "../../../../../utils/synchronization"; import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; -import { PullRequestCreation, PullRequestCreationDialog } from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; +import { + PullRequestCreation, + PullRequestCreationDialog, +} from "../../../../react/core/components/pull-request-creation-dialog/PullRequestCreationDialog"; import { SharingDialog } from "../../../../react/core/components/sharing-dialog/SharingDialog"; import SyncSummary from "../../../../react/core/components/sync-summary/SyncSummary"; import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; From 4036ab46d6c2698d16de5e9290b4588fbe4e47c1 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 09:20:34 +0100 Subject: [PATCH 056/163] Fix translations import --- .../core/pages/module-package-list/ModulePackageListPage.tsx | 2 +- .../core/pages/notifications-list/NotificationsListPage.tsx | 2 +- .../webapp/msf-aggregate-data/pages/MSFHomePage.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index df8e773cd..f7c67c34a 100644 --- a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -1,11 +1,11 @@ import { Icon } from "@material-ui/core"; import { PaginationOptions } from "d2-ui-components"; -import i18n from "d2-ui-components/locales"; import React, { ReactNode, useCallback, useEffect, useMemo, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; import { Store } from "../../../../../domain/stores/entities/Store"; +import i18n from "../../../../../locales"; import { CreatePackageFromFileDialog } from "../../../../react/core/components/create-package-from-file-dialog/CreatePackageFromFileDialog"; import { ModulePackageListTable, diff --git a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index 6afeaa9b4..ac986a9da 100644 --- a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -16,7 +16,6 @@ import { useLoading, useSnackbar, } from "d2-ui-components"; -import i18n from "d2-ui-components/locales"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Either } from "../../../../../domain/common/entities/Either"; @@ -26,6 +25,7 @@ import { ImportPullRequestError } from "../../../../../domain/notifications/usec import { UpdatePullRequestStatusError } from "../../../../../domain/notifications/usecases/UpdatePullRequestStatusUseCase"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; import { SynchronizationResult } from "../../../../../domain/reports/entities/SynchronizationResult"; +import i18n from "../../../../../locales"; import Dropdown from "../../../../react/core/components/dropdown/Dropdown"; import { NotificationViewerDialog } from "../../../../react/core/components/notification-viewer-dialog/NotificationViewerDialog"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 053bc42cc..9338af8b9 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,8 +1,8 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; -import i18n from "d2-ui-components/locales"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { Period } from "../../../../domain/common/entities/Period"; +import i18n from "../../../../locales"; import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; From 348cf902a22e5bec2f60021c8a73c25f292169fb Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 10:39:10 +0100 Subject: [PATCH 057/163] Remvoe comments --- .../reports/entities/SynchronizationReport.ts | 8 +-- src/models/syncReport.md | 57 ------------------- 2 files changed, 3 insertions(+), 62 deletions(-) delete mode 100644 src/models/syncReport.md diff --git a/src/domain/reports/entities/SynchronizationReport.ts b/src/domain/reports/entities/SynchronizationReport.ts index 6eef307f8..3559d6d7a 100644 --- a/src/domain/reports/entities/SynchronizationReport.ts +++ b/src/domain/reports/entities/SynchronizationReport.ts @@ -5,15 +5,13 @@ import { SynchronizationType } from "../../synchronization/entities/Synchronizat import { SynchronizationResult } from "./SynchronizationResult"; export class SynchronizationReport implements SynchronizationReportData { - // TODO: Review functional private results: SynchronizationResult[] | null; + public status: SynchronizationReportStatus; + public types: string[]; + public readonly id: string; public readonly date?: Date | undefined; public readonly user: string; - // TODO: Review functional - public status: SynchronizationReportStatus; - // TODO: Review functional - public types: string[]; public readonly syncRule?: string | undefined; public readonly packageImport?: boolean | undefined; public readonly deletedSyncRuleLabel?: string | undefined; diff --git a/src/models/syncReport.md b/src/models/syncReport.md deleted file mode 100644 index 9955fc4db..000000000 --- a/src/models/syncReport.md +++ /dev/null @@ -1,57 +0,0 @@ -//const dataStoreKey = Namespace.HISTORY; - -/** - public static async get(api: D2Api, id: string): Promise { - const data = await getDataById(api, dataStoreKey, id); - return data ? this.build(data) : null; - } - - public static async list( - api: D2Api, - filters: SyncReportTableFilters, - state?: TableInitialState, - paging = true - ): Promise<{ rows: SynchronizationReport[]; pager: Partial }> { - const { statusFilter, syncRuleFilter, type } = filters; - const { pagination, sorting } = state || {}; - const { page = 1, pageSize = 25 } = pagination || {}; - - const data = await getPaginatedData(api, dataStoreKey, filters, { - paging: false, - sorting: [sorting?.field ?? "id", sorting?.order ?? "asc"], - }); - - const filteredObjects = _(data.objects) - .filter(e => (statusFilter ? e.status === statusFilter : true)) - .filter(e => (syncRuleFilter ? e.syncRule === syncRuleFilter : true)) - .filter(({ type: elementType = "metadata" }) => elementType === type) - .value(); - - const total = filteredObjects.length; - const firstItem = paging ? (page - 1) * pageSize : 0; - const lastItem = paging ? firstItem + pageSize : total; - const rows = _.slice(filteredObjects, firstItem, lastItem); - - return { rows, pager: { ...pagination, page, pageSize, total } }; - } - - public async save(api: D2Api): Promise { - const exists = !!this.syncReport.id; - const element = exists ? this.syncReport : { ...this.syncReport, id: generateUid() }; - - if (exists) await this.remove(api); - await saveDataStore(api, `${dataStoreKey}-${element.id}`, this.results); - await saveData(api, dataStoreKey, element); - } - - public async remove(api: D2Api): Promise { - await deleteDataStore(api, `${dataStoreKey}-${this.syncReport.id}`); - await deleteData(api, dataStoreKey, this.syncReport); - } - - public async loadSyncResults(api: D2Api): Promise { - const { id } = this.syncReport; - return id ? getDataStore(api, `${dataStoreKey}-${id}`, []) : []; - } - -**/ \ No newline at end of file From 541acdbc3d27c3735020f0f744ffb732c934b17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 4 Dec 2020 10:46:09 +0100 Subject: [PATCH 058/163] Add RunAnalyticsSettings to MSFDialog --- i18n/en.pot | 16 ++++- i18n/es.po | 14 ++++- i18n/fr.po | 14 ++++- i18n/pt.po | 14 ++++- .../msf-Settings/MSFSettingsDialog.tsx | 62 +++++++++++++++++-- .../msf-aggregate-data/pages/MSFHomePage.tsx | 12 +++- 6 files changed, 120 insertions(+), 12 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a278dd214..caa933952 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-12-04T08:14:20.273Z\n" -"PO-Revision-Date: 2020-12-04T08:14:20.273Z\n" +"POT-Creation-Date: 2020-12-04T09:39:33.056Z\n" +"PO-Revision-Date: 2020-12-04T09:39:33.056Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1186,9 +1186,21 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "True" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Use sync rule settings" +msgstr "" + msgid "MSF Settings" msgstr "" +msgid "Run Analytics" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 4a9460ae1..8ed78073e 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-12-04T08:14:20.273Z\n" +"POT-Creation-Date: 2020-12-04T09:29:33.239Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1192,9 +1192,21 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "True" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Use sync rule settings" +msgstr "" + msgid "MSF Settings" msgstr "" +msgid "Run Analytics" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 7e966d636..217231ba6 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-12-04T08:14:20.273Z\n" +"POT-Creation-Date: 2020-12-04T09:29:33.239Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1189,9 +1189,21 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "True" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Use sync rule settings" +msgstr "" + msgid "MSF Settings" msgstr "" +msgid "Run Analytics" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 7e966d636..217231ba6 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-12-04T08:14:20.273Z\n" +"POT-Creation-Date: 2020-12-04T09:29:33.239Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1189,9 +1189,21 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "True" +msgstr "" + +msgid "False" +msgstr "" + +msgid "Use sync rule settings" +msgstr "" + msgid "MSF Settings" msgstr "" +msgid "Run Analytics" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx index c329d0990..5b9dae30d 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -1,13 +1,57 @@ import { ConfirmationDialog } from "d2-ui-components"; -import React from "react"; +import React, { useMemo, useState } from "react"; import i18n from "../../../../../locales"; +import Dropdown from "../../../core/components/dropdown/Dropdown"; + +type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; + +export interface MSFSettings { + runAnalytics: RunAnalyticsSettings; +} export interface MSFSettingsDialogProps { + msfSettings: MSFSettings; onClose(): void; - onSave(): void; + onSave(msfSettings: MSFSettings): void; } -export const MSFSettingsDialog: React.FC = ({ onClose, onSave }) => { +export const MSFSettingsDialog: React.FC = ({ + onClose, + onSave, + msfSettings, +}) => { + const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); + + const useSyncRuleItems = useMemo(() => { + return [ + { + id: "true", + name: i18n.t("True"), + }, + { + id: "false", + name: i18n.t("False"), + }, + { + id: "by-sync-rule-settings", + name: i18n.t("Use sync rule settings"), + }, + ]; + }, []); + + const handleSave = () => { + const msfSettings: MSFSettings = { + runAnalytics: + useSyncRule === "by-sync-rule-settings" + ? "by-sync-rule-settings" + : useSyncRule === "true" + ? true + : false, + }; + + onSave(msfSettings); + }; + return ( = ({ onClose, o fullWidth={true} title={i18n.t("MSF Settings")} onCancel={onClose} - onSave={() => onSave()} + onSave={() => handleSave()} cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} - > + > + + ); }; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 053bc42cc..445d58b1e 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -7,7 +7,10 @@ import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; import { useAppContext } from "../../../react/core/contexts/AppContext"; -import { MSFSettingsDialog } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +import { + MSFSettings, + MSFSettingsDialog, +} from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; export const MSFHomePage: React.FC = () => { const classes = useStyles(); @@ -16,6 +19,9 @@ export const MSFHomePage: React.FC = () => { const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); const [period, setPeriod] = useState(Period.createDefault()); + const [msfSettings, setMsfSettings] = useState({ + runAnalytics: "by-sync-rule-settings", + }); const [globalAdmin, setGlobalAdmin] = useState(false); const { api } = useAppContext(); @@ -52,8 +58,9 @@ export const MSFHomePage: React.FC = () => { setShowMSFSettingsDialog(false); }; - const handleSaveMSFSettings = () => { + const handleSaveMSFSettings = (msfSettings: MSFSettings) => { setShowMSFSettingsDialog(false); + setMsfSettings(msfSettings); }; return ( @@ -140,6 +147,7 @@ export const MSFHomePage: React.FC = () => { {showMSFSettingsDialog && ( From daa0d4ea707a1958525006e875f14bbed19c1fa0 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 11:58:45 +0100 Subject: [PATCH 059/163] Refactor sync rule entity into clean architecture --- i18n/en.pot | 13 +- i18n/es.po | 11 +- i18n/fr.po | 11 +- i18n/pt.po | 11 +- src/data/rules/RulesD2ApiRepository.ts | 53 +++ src/data/storage/Namespaces.ts | 2 +- .../__tests__/integration/helpers.ts | 2 +- .../common/factories/RepositoryFactory.ts | 7 + src/domain/modules/entities/MetadataModule.ts | 7 +- src/domain/modules/entities/Module.ts | 2 +- .../reports/entities/SynchronizationReport.ts | 6 +- .../reports/usecases/ListSyncReportUseCase.ts | 4 +- .../rules/entities/SynchronizationRule.ts} | 319 ++++++------------ .../__tests__/SynchronizationRule.spec.ts} | 38 ++- .../rules/repositories/RulesRepository.ts | 13 + .../rules/usecases/DeleteSyncRuleUseCase.ts | 30 ++ .../rules/usecases/GetSyncRuleUseCase.ts | 12 + .../rules/usecases/ListSyncRuleUseCase.ts | 110 ++++++ .../rules/usecases/SaveSyncRuleUseCase.ts | 15 + .../entities/SynchronizationBuilder.ts | 42 +++ .../entities/SynchronizationRule.ts | 14 - .../usecases/GenericSyncUseCase.ts | 14 +- .../usecases/PrepareSyncUseCase.ts | 2 +- src/presentation/CompositionRoot.ts | 18 +- .../DeletedObjectsTable.tsx | 12 +- .../PullRequestCreationDialog.tsx | 2 +- .../components/sync-dialog/SyncDialog.tsx | 8 +- .../SyncParamsSelector.tsx | 6 +- .../core/components/sync-wizard/Steps.ts | 10 +- .../components/sync-wizard/SyncWizard.tsx | 6 +- .../sync-wizard/common/GeneralInfoStep.tsx | 4 +- .../sync-wizard/data/EventsSelectionStep.tsx | 4 +- src/presentation/webapp/Root.tsx | 17 +- .../webapp/core/pages/history/HistoryPage.tsx | 11 +- .../core/pages/manual-sync/ManualSyncPage.tsx | 12 +- .../SyncRulesCreationPage.tsx | 21 +- .../sync-rules-list/SyncRulesListPage.tsx | 110 +++--- src/scheduler/scheduler.ts | 9 +- src/types/d2-ui-components.ts | 14 - src/types/synchronization.ts | 15 - src/utils/old-validations.ts | 7 +- src/utils/permissions.ts | 15 +- src/utils/synchronization.ts | 4 +- 43 files changed, 583 insertions(+), 460 deletions(-) create mode 100644 src/data/rules/RulesD2ApiRepository.ts rename src/{models/syncRule.ts => domain/rules/entities/SynchronizationRule.ts} (67%) rename src/{models/__tests__/syncRule.spec.ts => domain/rules/entities/__tests__/SynchronizationRule.spec.ts} (94%) create mode 100644 src/domain/rules/repositories/RulesRepository.ts create mode 100644 src/domain/rules/usecases/DeleteSyncRuleUseCase.ts create mode 100644 src/domain/rules/usecases/GetSyncRuleUseCase.ts create mode 100644 src/domain/rules/usecases/ListSyncRuleUseCase.ts create mode 100644 src/domain/rules/usecases/SaveSyncRuleUseCase.ts create mode 100644 src/domain/synchronization/entities/SynchronizationBuilder.ts delete mode 100644 src/domain/synchronization/entities/SynchronizationRule.ts diff --git a/i18n/en.pot b/i18n/en.pot index 99e8cde33..9cbfc830c 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-12-04T08:16:36.678Z\n" -"PO-Revision-Date: 2020-12-04T08:16:36.678Z\n" +"POT-Creation-Date: 2020-12-04T10:59:47.983Z\n" +"PO-Revision-Date: 2020-12-04T10:59:47.983Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -95,6 +95,9 @@ msgstr "" msgid "Version is too small (next version is {{validExample}})" msgstr "" +msgid "deleted" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1681,12 +1684,6 @@ msgstr "" msgid "Deleting Sync Rules" msgstr "" -msgid "deleted" -msgstr "" - -msgid "Failed to delete some rules" -msgstr "" - msgid "Successfully deleted {{count}} rules" msgid_plural "Successfully deleted {{count}} rules" msgstr[0] "" diff --git a/i18n/es.po b/i18n/es.po index 52ba0b0d8..ac627a947 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-12-04T08:16:36.678Z\n" +"POT-Creation-Date: 2020-12-04T10:59:47.983Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -95,6 +95,9 @@ msgstr "" msgid "Version is too small (next version is {{validExample}})" msgstr "" +msgid "deleted" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1688,12 +1691,6 @@ msgstr "" msgid "Deleting Sync Rules" msgstr "" -msgid "deleted" -msgstr "" - -msgid "Failed to delete some rules" -msgstr "" - msgid "Successfully deleted {{count}} rules" msgid_plural "Successfully deleted {{count}} rules" msgstr[0] "" diff --git a/i18n/fr.po b/i18n/fr.po index b951b0393..8dfbcdd39 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-12-04T08:16:36.678Z\n" +"POT-Creation-Date: 2020-12-04T10:59:47.983Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -95,6 +95,9 @@ msgstr "" msgid "Version is too small (next version is {{validExample}})" msgstr "" +msgid "deleted" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1684,12 +1687,6 @@ msgstr "" msgid "Deleting Sync Rules" msgstr "" -msgid "deleted" -msgstr "" - -msgid "Failed to delete some rules" -msgstr "" - msgid "Successfully deleted {{count}} rules" msgid_plural "Successfully deleted {{count}} rules" msgstr[0] "" diff --git a/i18n/pt.po b/i18n/pt.po index b951b0393..8dfbcdd39 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-12-04T08:16:36.678Z\n" +"POT-Creation-Date: 2020-12-04T10:59:47.983Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -95,6 +95,9 @@ msgstr "" msgid "Version is too small (next version is {{validExample}})" msgstr "" +msgid "deleted" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1684,12 +1687,6 @@ msgstr "" msgid "Deleting Sync Rules" msgstr "" -msgid "deleted" -msgstr "" - -msgid "Failed to delete some rules" -msgstr "" - msgid "Successfully deleted {{count}} rules" msgid_plural "Successfully deleted {{count}} rules" msgstr[0] "" diff --git a/src/data/rules/RulesD2ApiRepository.ts b/src/data/rules/RulesD2ApiRepository.ts new file mode 100644 index 000000000..b1effb34b --- /dev/null +++ b/src/data/rules/RulesD2ApiRepository.ts @@ -0,0 +1,53 @@ +import { Instance } from "../../domain/instance/entities/Instance"; +import { + SynchronizationRule, + SynchronizationRuleData, +} from "../../domain/rules/entities/SynchronizationRule"; +import { RulesRepository } from "../../domain/rules/repositories/RulesRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; +import { Namespace } from "../storage/Namespaces"; +import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; + +export class RulesD2ApiRepository implements RulesRepository { + private storageClient: StorageClient; + + constructor(instance: Instance) { + this.storageClient = new StorageDataStoreClient(instance); + } + + public async getById(id: string): Promise { + const data = await this.storageClient.getObjectInCollection( + Namespace.RULES, + id + ); + + return data ? SynchronizationRule.build(data) : undefined; + } + + public async getSyncResults(id: string): Promise { + const data = await this.storageClient.getObject( + `${Namespace.RULES}-${id}` + ); + + return data ?? []; + } + + public async list(): Promise { + const stores = await this.storageClient.listObjectsInCollection( + Namespace.RULES + ); + + return stores.map(data => SynchronizationRule.build(data)); + } + + public async save(report: SynchronizationRule): Promise { + await this.storageClient.saveObjectInCollection( + Namespace.RULES, + report.toObject() + ); + } + + public async delete(id: string): Promise { + await this.storageClient.removeObjectInCollection(Namespace.RULES, id); + } +} diff --git a/src/data/storage/Namespaces.ts b/src/data/storage/Namespaces.ts index 18908a80e..39462d156 100644 --- a/src/data/storage/Namespaces.ts +++ b/src/data/storage/Namespaces.ts @@ -20,7 +20,7 @@ export const NamespaceProperties: Record = { [Namespace.IMPORTEDPACKAGES]: ["contents"], [Namespace.INSTANCES]: ["metadataMapping"], [Namespace.MAPPINGS]: ["mappingDictionary"], - [Namespace.RULES]: [], + [Namespace.RULES]: ["builder"], [Namespace.HISTORY]: [], [Namespace.NOTIFICATIONS]: ["payload"], [Namespace.CONFIG]: [], diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index 500c9b9e9..a30f55be1 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -13,7 +13,7 @@ import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiReposito import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; -import { SynchronizationBuilder } from "./../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; export function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 25b91f179..ff7ed504a 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -16,6 +16,7 @@ import { } from "../../metadata/repositories/MetadataRepository"; import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; import { ReportsRepositoryConstructor } from "../../reports/repositories/ReportsRepository"; +import { RulesRepositoryConstructor } from "../../rules/repositories/RulesRepository"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; @@ -110,6 +111,11 @@ export class RepositoryFactory { public reportsRepository(instance: Instance) { return this.get(Repositories.ReportsRepository, [instance]); } + + @cache() + public rulesRepository(instance: Instance) { + return this.get(Repositories.RulesRepository, [instance]); + } } type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; @@ -126,4 +132,5 @@ export const Repositories = { TransformationRepository: "transformationsRepository", FileRepository: "fileRepository", ReportsRepository: "reportsRepository", + RulesRepository: "rulesRepository", } as const; diff --git a/src/domain/modules/entities/MetadataModule.ts b/src/domain/modules/entities/MetadataModule.ts index f9f32da6c..b0f216ea7 100644 --- a/src/domain/modules/entities/MetadataModule.ts +++ b/src/domain/modules/entities/MetadataModule.ts @@ -1,10 +1,7 @@ import _ from "lodash"; import { D2Model } from "../../../models/dhis/default"; -import { - ExcludeIncludeRules, - MetadataIncludeExcludeRules, - SynchronizationBuilder, -} from "../../../types/synchronization"; +import { ExcludeIncludeRules, MetadataIncludeExcludeRules } from "../../../types/synchronization"; +import { SynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; import { extractChildrenFromRules, extractParentsFromRule, diff --git a/src/domain/modules/entities/Module.ts b/src/domain/modules/entities/Module.ts index 22fdd4590..50bd43099 100644 --- a/src/domain/modules/entities/Module.ts +++ b/src/domain/modules/entities/Module.ts @@ -1,6 +1,6 @@ import { generateUid } from "d2/uid"; import _ from "lodash"; -import { SynchronizationBuilder } from "../../../types/synchronization"; +import { SynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; import { NamedRef, Ref, SharedRef } from "../../common/entities/Ref"; import { SharingSetting } from "../../common/entities/SharingSetting"; import { ModelValidation, validateModel, ValidationError } from "../../common/entities/Validations"; diff --git a/src/domain/reports/entities/SynchronizationReport.ts b/src/domain/reports/entities/SynchronizationReport.ts index 3559d6d7a..c1d82adff 100644 --- a/src/domain/reports/entities/SynchronizationReport.ts +++ b/src/domain/reports/entities/SynchronizationReport.ts @@ -8,13 +8,13 @@ export class SynchronizationReport implements SynchronizationReportData { private results: SynchronizationResult[] | null; public status: SynchronizationReportStatus; public types: string[]; + public deletedSyncRuleLabel?: string | undefined; public readonly id: string; public readonly date?: Date | undefined; public readonly user: string; public readonly syncRule?: string | undefined; public readonly packageImport?: boolean | undefined; - public readonly deletedSyncRuleLabel?: string | undefined; public readonly type: SynchronizationType; public readonly dataStats?: AggregatedDataStats[] | EventsDataStats[] | undefined; @@ -63,6 +63,10 @@ export class SynchronizationReport implements SynchronizationReportData { this.types = types; } + public setDeletedSyncRuleLabel(label: string): void { + this.deletedSyncRuleLabel = label; + } + public addSyncResult(...result: SynchronizationResult[]): void { this.results = _.unionBy( [...result], diff --git a/src/domain/reports/usecases/ListSyncReportUseCase.ts b/src/domain/reports/usecases/ListSyncReportUseCase.ts index 1bae4512d..8f5986ab2 100644 --- a/src/domain/reports/usecases/ListSyncReportUseCase.ts +++ b/src/domain/reports/usecases/ListSyncReportUseCase.ts @@ -9,7 +9,7 @@ export interface ListSyncReportUseCaseParams { pageSize?: number; page?: number; sorting?: { field: keyof SynchronizationReport; order: "asc" | "desc" }; - filters: { statusFilter?: string; syncRuleFilter?: string; type?: string; search?: string }; + filters?: { statusFilter?: string; syncRuleFilter?: string; type?: string; search?: string }; } export interface ListSyncReportUseCaseResult { @@ -25,7 +25,7 @@ export class ListSyncReportUseCase implements UseCase { pageSize = 25, page = 1, sorting = { field: "id", order: "asc" }, - filters, + filters = {}, }: ListSyncReportUseCaseParams): Promise { const rawData = await this.repositoryFactory.reportsRepository(this.localInstance).list(); diff --git a/src/models/syncRule.ts b/src/domain/rules/entities/SynchronizationRule.ts similarity index 67% rename from src/models/syncRule.ts rename to src/domain/rules/entities/SynchronizationRule.ts index 481a7e466..1a6c85a17 100644 --- a/src/models/syncRule.ts +++ b/src/domain/rules/entities/SynchronizationRule.ts @@ -2,78 +2,37 @@ import cronstrue from "cronstrue"; import { generateUid } from "d2/uid"; import _ from "lodash"; import moment from "moment"; -import { - DataSyncAggregation, - DataSynchronizationParams, - DataSyncPeriod, -} from "../domain/aggregated/types"; -import { SharingSetting } from "../domain/common/entities/SharingSetting"; -import { SynchronizationRule } from "../domain/synchronization/entities/SynchronizationRule"; -import { SynchronizationType } from "../domain/synchronization/entities/SynchronizationType"; -import { D2Api, Ref } from "../types/d2-api"; -import { SyncRuleTableFilters, TableList, TablePagination } from "../types/d2-ui-components"; +import { D2Model } from "../../../models/dhis/default"; import { ExcludeIncludeRules, MetadataIncludeExcludeRules, MetadataSynchronizationParams, +} from "../../../types/synchronization"; +import { + defaultSynchronizationBuilder, SynchronizationBuilder, -} from "../types/synchronization"; -import { extractChildrenFromRules, extractParentsFromRule } from "../utils/metadataIncludeExclude"; -import { getUserInfo, isGlobalAdmin, UserInfo } from "../utils/permissions"; -import { OldValidation } from "../utils/old-validations"; -import isValidCronExpression from "../utils/validCronExpression"; +} from "../../synchronization/entities/SynchronizationBuilder"; import { - deleteData, - deleteDataStore, - getDataById, - getDataStore, - getPaginatedData, - saveData, - saveDataStore, -} from "./dataStore"; -import { D2Model } from "./dhis/default"; -import { FilterRule } from "../domain/metadata/entities/FilterRule"; - -const dataStoreKey = "rules"; - -const defaultSynchronizationBuilder: SynchronizationBuilder = { - originInstance: "LOCAL", - targetInstances: [], - metadataIds: [], - filterRules: [], - excludedIds: [], - metadataTypes: [], - dataParams: { - strategy: "NEW_AND_UPDATES", - allAttributeCategoryOptions: true, - dryRun: false, - allEvents: true, - enableAggregation: undefined, - aggregationType: undefined, - }, - syncParams: { - importStrategy: "CREATE_AND_UPDATE", - enableMapping: false, - includeSharingSettings: true, - removeOrgUnitReferences: false, - useDefaultIncludeExclude: true, - atomicMode: "ALL", - mergeMode: "MERGE", - importMode: "COMMIT", - }, -}; - -type DetailsKeys = "builder"; - -type SynchronizationRuleMain = Omit & - Pick; - -type SynchronizationRuleDetails = Pick; - -export default class SyncRule { - private readonly syncRule: SynchronizationRule; - - constructor(syncRule: SynchronizationRule) { + extractChildrenFromRules, + extractParentsFromRule, +} from "../../../utils/metadataIncludeExclude"; +import { OldValidation } from "../../../utils/old-validations"; +import { UserInfo } from "../../../utils/permissions"; +import isValidCronExpression from "../../../utils/validCronExpression"; +import { + DataSyncAggregation, + DataSynchronizationParams, + DataSyncPeriod, +} from "../../aggregated/types"; +import { SharedRef } from "../../common/entities/Ref"; +import { SharingSetting } from "../../common/entities/SharingSetting"; +import { FilterRule } from "../../metadata/entities/FilterRule"; +import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; + +export class SynchronizationRule { + private readonly syncRule: SynchronizationRuleData; + + constructor(syncRule: SynchronizationRuleData) { this.syncRule = _.pick(syncRule, [ "id", "name", @@ -96,13 +55,13 @@ export default class SyncRule { if (!this.syncRule.id) this.syncRule.id = generateUid(); } - public replicate(): SyncRule { + public replicate(): SynchronizationRule { return this.updateName(`Copy of ${this.syncRule.name}`) .update({ lastExecuted: undefined }) .updateId(generateUid()); } - public toObject(): SynchronizationRule { + public toObject(): SynchronizationRuleData { return _.clone(this.syncRule); } @@ -255,8 +214,8 @@ export default class SyncRule { return this.syncRule.builder?.dataParams ?? {}; } - public static create(type: SynchronizationType = "metadata"): SyncRule { - return new SyncRule({ + public static create(type: SynchronizationType = "metadata"): SynchronizationRule { + return new SynchronizationRule({ id: "", name: "", code: "", @@ -280,83 +239,12 @@ export default class SyncRule { }); } - public static createOnDemand(type: SynchronizationType = "metadata"): SyncRule { - return SyncRule.create(type).updateName("__MANUAL__"); - } - - public static build(syncRule: SynchronizationRule | undefined): SyncRule { - return syncRule ? new SyncRule(syncRule) : this.create(); + public static createOnDemand(type: SynchronizationType = "metadata"): SynchronizationRule { + return SynchronizationRule.create(type).updateName("__MANUAL__"); } - public static async get(api: D2Api, id: string): Promise { - const syncRuleData = await getDataById(api, dataStoreKey, id); - if (!syncRuleData) throw new Error(`SyncRule not found: ${id}`); - const detailsKey = this.getDetailsKey(syncRuleData); - const defaultDetails: SynchronizationRuleDetails = { - builder: defaultSynchronizationBuilder, - }; - const detailsData = await getDataStore(api, detailsKey, defaultDetails); - const dataWithMapping = { ...syncRuleData, builder: detailsData.builder }; - return this.build(dataWithMapping); - } - - public static async list( - api: D2Api, - filters: SyncRuleTableFilters, - pagination: TablePagination - ): Promise { - const { - targetInstanceFilter = null, - enabledFilter = null, - lastExecutedFilter = null, - type = null, - } = filters || {}; - const { page = 1, pageSize = 20, paging = true, sorting } = pagination || {}; - - const globalAdmin = await isGlobalAdmin(api); - const userInfo = await getUserInfo(api); - - const data = await getPaginatedData(api, dataStoreKey, filters, { paging: false, sorting }); - const filteredObjects = _(data.objects as SynchronizationRuleMain[]) - .map(syncRuleMain => ({ - ...syncRuleMain, - builder: { - ...defaultSynchronizationBuilder, - targetInstances: syncRuleMain.targetInstances, - }, - })) - .filter(data => { - const rule = SyncRule.build(data); - return _.isNull(type) || rule.type === type; - }) - .filter(data => { - const rule = SyncRule.build(data); - return globalAdmin || rule.isVisibleToUser(userInfo); - }) - .filter(rule => - targetInstanceFilter ? rule.targetInstances.includes(targetInstanceFilter) : true - ) - .filter(rule => { - if (!enabledFilter) return true; - return ( - (rule.enabled && enabledFilter === "enabled") || - (!rule.enabled && enabledFilter === "disabled") - ); - }) - .filter(rule => - lastExecutedFilter && rule.lastExecuted - ? moment(lastExecutedFilter).isSameOrBefore(rule.lastExecuted, "date") - : true - ) - .value(); - - const total = filteredObjects.length; - const pageCount = paging ? Math.ceil(filteredObjects.length / pageSize) : 1; - const firstItem = paging ? (page - 1) * pageSize : 0; - const lastItem = paging ? firstItem + pageSize : total; - const objects = _.slice(filteredObjects, firstItem, lastItem); - - return { objects, pager: { page, pageCount, total } }; + public static build(syncRule: SynchronizationRuleData | undefined): SynchronizationRule { + return syncRule ? new SynchronizationRule(syncRule) : this.create(); } public toBuilder(): SynchronizationBuilder { @@ -372,46 +260,46 @@ export default class SyncRule { ]); } - public updateId(id: string): SyncRule { + public updateId(id: string): SynchronizationRule { return this.update({ id }); } - public updateName(name: string): SyncRule { + public updateName(name: string): SynchronizationRule { return this.update({ name }); } - public updateCode(code: string): SyncRule { + public updateCode(code: string): SynchronizationRule { return this.update({ code }); } - public updateDescription(description: string): SyncRule { + public updateDescription(description: string): SynchronizationRule { return this.update({ description }); } - public updateMetadataIds(metadataIds: string[]): SyncRule { + public updateMetadataIds(metadataIds: string[]): SynchronizationRule { const data = _(_.cloneDeep(this.syncRule)) .set(["builder", "metadataIds"], metadataIds) .set(["builder", "syncParams", "useDefaultIncludeExclude"], true) .set(["builder", "syncParams", "metadataIncludeExcludeRules"], {}) .value(); - return SyncRule.build(data); + return SynchronizationRule.build(data); } - public updateFilterRules(filterRules: FilterRule[]): SyncRule { + public updateFilterRules(filterRules: FilterRule[]): SynchronizationRule { return this.updateBuilder({ filterRules }); } - public markToUseDefaultIncludeExclude(): SyncRule { + public markToUseDefaultIncludeExclude(): SynchronizationRule { const data = _(_.cloneDeep(this.syncRule)) .set(["builder", "syncParams", "useDefaultIncludeExclude"], true) .set(["builder", "syncParams", "metadataIncludeExcludeRules"], {}) .value(); - return SyncRule.build(data); + return SynchronizationRule.build(data); } - public markToNotUseDefaultIncludeExclude(models: Array): SyncRule { + public markToNotUseDefaultIncludeExclude(models: Array): SynchronizationRule { const rules: MetadataIncludeExcludeRules = models.reduce( (accumulator: any, model: typeof D2Model) => ({ ...accumulator, @@ -428,10 +316,13 @@ export default class SyncRule { .set(["builder", "syncParams", "metadataIncludeExcludeRules"], rules) .value(); - return SyncRule.build(data); + return SynchronizationRule.build(data); } - public moveRuleFromExcludeToInclude(type: string, rulesToInclude: string[]): SyncRule { + public moveRuleFromExcludeToInclude( + type: string, + rulesToInclude: string[] + ): SynchronizationRule { const { includeRules: oldIncludeRules, excludeRules: oldExcludeRules, @@ -458,7 +349,10 @@ export default class SyncRule { return this.updateIncludeExcludeRules(type, excludeIncludeRules); } - public moveRuleFromIncludeToExclude(type: string, rulesToExclude: string[]): SyncRule { + public moveRuleFromIncludeToExclude( + type: string, + rulesToExclude: string[] + ): SynchronizationRule { const { includeRules: oldIncludeRules, excludeRules: oldExcludeRules, @@ -490,7 +384,7 @@ export default class SyncRule { private updateIncludeExcludeRules( type: string, excludeIncludeRules: ExcludeIncludeRules - ): SyncRule { + ): SynchronizationRule { const rules = { ...this.metadataIncludeExcludeRules, [type]: excludeIncludeRules, @@ -500,14 +394,14 @@ export default class SyncRule { .set(["builder", "syncParams", "metadataIncludeExcludeRules"], rules) .value(); - return SyncRule.build(data); + return SynchronizationRule.build(data); } - public update(partialRule: Partial): SyncRule { - return SyncRule.build({ ...this.syncRule, ...partialRule }); + public update(partialRule: Partial): SynchronizationRule { + return SynchronizationRule.build({ ...this.syncRule, ...partialRule }); } - public updateBuilder(partialBuilder: Partial): SyncRule { + public updateBuilder(partialBuilder: Partial): SynchronizationRule { return this.update({ builder: { ...defaultSynchronizationBuilder, @@ -519,7 +413,7 @@ export default class SyncRule { public updateBuilderDataParams( partialDataParams: Partial - ): SyncRule { + ): SynchronizationRule { return this.updateBuilder({ dataParams: { ...this.syncRule.builder?.dataParams, @@ -528,8 +422,8 @@ export default class SyncRule { }); } - public updateMetadataTypes(metadataTypes: string[]): SyncRule { - return SyncRule.build({ + public updateMetadataTypes(metadataTypes: string[]): SynchronizationRule { + return SynchronizationRule.build({ ...this.syncRule, builder: { ...this.syncRule.builder, @@ -538,46 +432,48 @@ export default class SyncRule { }); } - public updateExcludedIds(excludedIds: string[]): SyncRule { + public updateExcludedIds(excludedIds: string[]): SynchronizationRule { return this.updateBuilder({ excludedIds }); } - public updateDataSyncAttributeCategoryOptions(attributeCategoryOptions?: string[]): SyncRule { + public updateDataSyncAttributeCategoryOptions( + attributeCategoryOptions?: string[] + ): SynchronizationRule { return this.updateBuilderDataParams({ attributeCategoryOptions }); } public updateDataSyncAllAttributeCategoryOptions( allAttributeCategoryOptions?: boolean - ): SyncRule { + ): SynchronizationRule { return this.updateBuilderDataParams({ allAttributeCategoryOptions }); } - public updateDataSyncOrgUnitPaths(orgUnitPaths: string[]): SyncRule { + public updateDataSyncOrgUnitPaths(orgUnitPaths: string[]): SynchronizationRule { return this.updateBuilderDataParams({ orgUnitPaths }); } - public updateDataSyncPeriod(period?: DataSyncPeriod): SyncRule { + public updateDataSyncPeriod(period?: DataSyncPeriod): SynchronizationRule { return this.updateBuilderDataParams({ period }); } - public updateDataSyncStartDate(startDate?: Date): SyncRule { + public updateDataSyncStartDate(startDate?: Date): SynchronizationRule { return this.updateBuilderDataParams({ startDate }); } - public updateDataSyncEndDate(endDate?: Date): SyncRule { + public updateDataSyncEndDate(endDate?: Date): SynchronizationRule { return this.updateBuilderDataParams({ endDate }); } - public updateDataSyncEvents(events?: string[]): SyncRule { + public updateDataSyncEvents(events?: string[]): SynchronizationRule { return this.updateBuilderDataParams({ events }); } - public updateDataSyncAllEvents(allEvents?: boolean): SyncRule { + public updateDataSyncAllEvents(allEvents?: boolean): SynchronizationRule { return this.updateBuilderDataParams({ allEvents }); } - public updateDataSyncEnableAggregation(enableAggregation?: boolean): SyncRule { - return SyncRule.build({ + public updateDataSyncEnableAggregation(enableAggregation?: boolean): SynchronizationRule { + return SynchronizationRule.build({ ...this.syncRule, builder: { ...this.syncRule.builder, @@ -589,8 +485,10 @@ export default class SyncRule { }); } - public updateDataSyncAggregationType(aggregationType?: DataSyncAggregation): SyncRule { - return SyncRule.build({ + public updateDataSyncAggregationType( + aggregationType?: DataSyncAggregation + ): SynchronizationRule { + return SynchronizationRule.build({ ...this.syncRule, builder: { ...this.syncRule.builder, @@ -602,27 +500,27 @@ export default class SyncRule { }); } - public updateTargetInstances(targetInstances: string[]): SyncRule { + public updateTargetInstances(targetInstances: string[]): SynchronizationRule { return this.updateBuilder({ targetInstances }); } - public updateSyncParams(syncParams: MetadataSynchronizationParams): SyncRule { + public updateSyncParams(syncParams: MetadataSynchronizationParams): SynchronizationRule { return this.updateBuilder({ syncParams }); } - public updateDataParams(dataParams: DataSynchronizationParams): SyncRule { + public updateDataParams(dataParams: DataSynchronizationParams): SynchronizationRule { return this.updateBuilder({ dataParams }); } - public updateEnabled(enabled: boolean): SyncRule { + public updateEnabled(enabled: boolean): SynchronizationRule { return this.update({ enabled }); } - public updateFrequency(frequency: string): SyncRule { + public updateFrequency(frequency: string): SynchronizationRule { return this.update({ frequency }); } - public updateLastExecuted(lastExecuted: Date): SyncRule { + public updateLastExecuted(lastExecuted: Date): SynchronizationRule { return this.update({ lastExecuted }); } @@ -653,44 +551,6 @@ export default class SyncRule { return isUserOwner || isPublic || hasUserAccess || hasGroupAccess; } - private static getDetailsKey(syncRule: SyncRule): string { - return dataStoreKey + "-" + syncRule.id; - } - - public async save(api: D2Api): Promise { - const userInfo = await getUserInfo(api); - const user = _.pick(userInfo, ["id", "name"]); - const exists = !!this.syncRule.id; - const syncRule = exists - ? this.syncRule - : { ...this.syncRule, id: generateUid(), created: new Date(), user }; - - if (exists) await this.remove(api); - - const detailsKey = SyncRule.getDetailsKey(syncRule); - const builder = syncRule.builder ?? defaultSynchronizationBuilder; - const detailsData: SynchronizationRuleDetails = { builder }; - await saveDataStore(api, detailsKey, detailsData); - const syncRuleMain: SynchronizationRuleMain = { - ..._.omit(syncRule, ["builder"]), - targetInstances: builder.targetInstances, - }; - - await saveData(api, dataStoreKey, { - ...syncRuleMain, - lastUpdated: new Date(), - lastUpdatedBy: user, - }); - - return new SyncRule(syncRule); - } - - public async remove(api: D2Api): Promise { - await deleteData(api, dataStoreKey, this.syncRule); - const detailsKey = SyncRule.getDetailsKey(this.syncRule); - await deleteDataStore(api, detailsKey); - } - private get usesFilterRules(): boolean { return this.type === "metadata"; } @@ -808,7 +668,18 @@ export default class SyncRule { } public async isValid(): Promise { - const validation = await this.validate(); + const validation = this.validate(); return _.flatten(Object.values(validation)).length === 0; } } + +export interface SynchronizationRuleData extends SharedRef { + code?: string; + created: Date; + description?: string; + builder: SynchronizationBuilder; + enabled: boolean; + lastExecuted?: Date; + frequency?: string; + type: SynchronizationType; +} diff --git a/src/models/__tests__/syncRule.spec.ts b/src/domain/rules/entities/__tests__/SynchronizationRule.spec.ts similarity index 94% rename from src/models/__tests__/syncRule.spec.ts rename to src/domain/rules/entities/__tests__/SynchronizationRule.spec.ts index 132bc97af..c2d7f694c 100644 --- a/src/models/__tests__/syncRule.spec.ts +++ b/src/domain/rules/entities/__tests__/SynchronizationRule.spec.ts @@ -1,5 +1,9 @@ -import SyncRule from "../syncRule"; -import { DataElementModel, IndicatorModel, OrganisationUnitModel } from "../dhis/metadata"; +import { + DataElementModel, + IndicatorModel, + OrganisationUnitModel, +} from "../../../../models/dhis/metadata"; +import { SynchronizationRule } from "../SynchronizationRule"; const indicatorIncludeExcludeRules = { includeRules: [ @@ -16,14 +20,14 @@ const indicatorIncludeExcludeRules = { describe("SyncRule", () => { describe("create", () => { it("should return a SyncRule with a empty name", () => { - const syncRule = SyncRule.create("metadata"); + const syncRule = SynchronizationRule.create("metadata"); expect(syncRule.name).toBe(""); }); }); describe("createOnDemand", () => { it("should return a SyncRule with a name", () => { - const syncRule = SyncRule.createOnDemand("metadata"); + const syncRule = SynchronizationRule.createOnDemand("metadata"); expect(syncRule.name).not.toBe(""); }); }); @@ -31,15 +35,15 @@ describe("SyncRule", () => { describe("isValid", () => { describe("metadata", () => { it("should return false when is created using create method", async () => { - const isValid = await SyncRule.create("metadata").isValid(); + const isValid = await SynchronizationRule.create("metadata").isValid(); expect(isValid).toEqual(false); }); it("should return false when is created using createOnDemand method", async () => { - const isValid = await SyncRule.createOnDemand("metadata").isValid(); + const isValid = await SynchronizationRule.createOnDemand("metadata").isValid(); expect(isValid).toEqual(false); }); it("should return true when is metadata sync rule and contains name, instances and metadataIds", async () => { - const syncRule = SyncRule.create("metadata") + const syncRule = SynchronizationRule.create("metadata") .updateName("SyncRule test") .updateMetadataIds(["zXvNvFtGwDu"]) .updateTargetInstances(["fP3MMoWv6qp"]); @@ -47,7 +51,7 @@ describe("SyncRule", () => { expect(isValid).toEqual(true); }); it("should return false when does not contains metadataIds", async () => { - const syncRule = SyncRule.create("metadata") + const syncRule = SynchronizationRule.create("metadata") .updateName("SyncRule test") .updateTargetInstances(["fP3MMoWv6qp"]); const isValid = await syncRule.isValid(); @@ -57,15 +61,15 @@ describe("SyncRule", () => { describe("events", () => { it("should return false when is created using create method", async () => { - const isValid = await SyncRule.create("events").isValid(); + const isValid = await SynchronizationRule.create("events").isValid(); expect(isValid).toEqual(false); }); it("should return false when is created using createOnDemand method", async () => { - const isValid = await SyncRule.createOnDemand("events").isValid(); + const isValid = await SynchronizationRule.createOnDemand("events").isValid(); expect(isValid).toEqual(false); }); it("should return true when contains name, instances and organisationUnits", async () => { - const syncRule = SyncRule.create("events") + const syncRule = SynchronizationRule.create("events") .updateName("SyncRule test") .updateMetadataIds(["dataElement"]) .updateDataSyncAllEvents(true) @@ -77,7 +81,7 @@ describe("SyncRule", () => { expect(isValid).toEqual(true); }); it("should return false when does not contains organisationUnits", async () => { - const syncRule = SyncRule.create("events") + const syncRule = SynchronizationRule.create("events") .updateName("SyncRule test") .updateTargetInstances(["fP3MMoWv6qp"]); const isValid = await syncRule.isValid(); @@ -513,8 +517,10 @@ describe("SyncRule", () => { }); }); -function givenASyncRuleWithMetadataIncludeExcludeRules(dependantRulesInExclude = false): SyncRule { - const initialSyncRule = SyncRule.create("metadata") +function givenASyncRuleWithMetadataIncludeExcludeRules( + dependantRulesInExclude = false +): SynchronizationRule { + const initialSyncRule = SynchronizationRule.create("metadata") .updateMetadataIds(["id1", "id2"]) .markToNotUseDefaultIncludeExclude([OrganisationUnitModel, IndicatorModel]); @@ -539,8 +545,8 @@ function givenASyncRuleWithMetadataIncludeExcludeRules(dependantRulesInExclude = } } -function givenASyncRuleWithoutMetadataIncludeExcludeRules(): SyncRule { - return SyncRule.create("metadata").updateMetadataIds(["id1", "id2"]); +function givenASyncRuleWithoutMetadataIncludeExcludeRules(): SynchronizationRule { + return SynchronizationRule.create("metadata").updateMetadataIds(["id1", "id2"]); } export {}; diff --git a/src/domain/rules/repositories/RulesRepository.ts b/src/domain/rules/repositories/RulesRepository.ts new file mode 100644 index 000000000..c638f93e7 --- /dev/null +++ b/src/domain/rules/repositories/RulesRepository.ts @@ -0,0 +1,13 @@ +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationRule } from "../entities/SynchronizationRule"; + +export interface RulesRepositoryConstructor { + new (instance: Instance): RulesRepository; +} + +export interface RulesRepository { + getById(id: string): Promise; + list(): Promise; + save(report: SynchronizationRule): Promise; + delete(id: string): Promise; +} diff --git a/src/domain/rules/usecases/DeleteSyncRuleUseCase.ts b/src/domain/rules/usecases/DeleteSyncRuleUseCase.ts new file mode 100644 index 000000000..1ce3cff16 --- /dev/null +++ b/src/domain/rules/usecases/DeleteSyncRuleUseCase.ts @@ -0,0 +1,30 @@ +import i18n from "../../../locales"; +import { promiseMap } from "../../../utils/common"; +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; + +export class DeleteSyncRuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(id: string): Promise { + const rule = await this.repositoryFactory.rulesRepository(this.localInstance).getById(id); + if (!rule) return; + + await this.repositoryFactory.rulesRepository(this.localInstance).delete(id); + + const deletedRuleLabel = `${rule.name} (${i18n.t("deleted")})`; + + const syncReports = await this.repositoryFactory + .reportsRepository(this.localInstance) + .list(); + + const syncRuleReports = syncReports.filter(({ syncRule }) => syncRule === id); + + await promiseMap(syncRuleReports, async report => { + report.setDeletedSyncRuleLabel(deletedRuleLabel); + + await this.repositoryFactory.reportsRepository(this.localInstance).save(report); + }); + } +} diff --git a/src/domain/rules/usecases/GetSyncRuleUseCase.ts b/src/domain/rules/usecases/GetSyncRuleUseCase.ts new file mode 100644 index 000000000..007b1e1a1 --- /dev/null +++ b/src/domain/rules/usecases/GetSyncRuleUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationRule } from "../entities/SynchronizationRule"; + +export class GetSyncRuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(id: string): Promise { + return this.repositoryFactory.rulesRepository(this.localInstance).getById(id); + } +} diff --git a/src/domain/rules/usecases/ListSyncRuleUseCase.ts b/src/domain/rules/usecases/ListSyncRuleUseCase.ts new file mode 100644 index 000000000..2c22fc643 --- /dev/null +++ b/src/domain/rules/usecases/ListSyncRuleUseCase.ts @@ -0,0 +1,110 @@ +import _ from "lodash"; +import moment from "moment"; +import { getD2APiFromInstance } from "../../../utils/d2-utils"; +import { getUserInfo, isGlobalAdmin } from "../../../utils/permissions"; +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { defaultSynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; +import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; +import { SynchronizationRule } from "../entities/SynchronizationRule"; + +export interface ListSyncRuleUseCaseParams { + paging?: boolean; + pageSize?: number; + page?: number; + sorting?: { field: keyof SynchronizationRule; order: "asc" | "desc" }; + filters?: { + targetInstanceFilter?: string; + enabledFilter?: string; + lastExecutedFilter?: Date | null; + type?: SynchronizationType; + search?: string; + }; +} + +export interface ListSyncRuleUseCaseResult { + rows: SynchronizationRule[]; + pager: { total: number; page: number; pageCount: number }; +} + +export class ListSyncRuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute({ + paging = true, + pageSize = 25, + page = 1, + sorting = { field: "id", order: "asc" }, + filters = {}, + }: ListSyncRuleUseCaseParams): Promise { + const rawData = await this.repositoryFactory.rulesRepository(this.localInstance).list(); + + const { + targetInstanceFilter = null, + enabledFilter = null, + lastExecutedFilter = null, + type = null, + search, + } = filters; + + const filteredData = search + ? _.filter(rawData, item => + _(item) + .values() + .map(value => (typeof value === "string" ? value : undefined)) + .compact() + .some(field => field.toLowerCase().includes(search.toLowerCase())) + ) + : rawData; + + const { field, order } = sorting; + const sortedData = _.orderBy( + filteredData, + [data => _.toLower(data[field] as string)], + [order] + ); + + // TODO: FIXME Move this to config repository + const globalAdmin = await isGlobalAdmin(getD2APiFromInstance(this.localInstance)); + const userInfo = await getUserInfo(getD2APiFromInstance(this.localInstance)); + + const filteredObjects = _(sortedData) + .map(rule => + rule.updateBuilder({ + ...defaultSynchronizationBuilder, + targetInstances: rule.targetInstances, + }) + ) + .filter(rule => { + return _.isNull(type) || rule.type === type; + }) + .filter(rule => { + return globalAdmin || rule.isVisibleToUser(userInfo); + }) + .filter(rule => + targetInstanceFilter ? rule.targetInstances.includes(targetInstanceFilter) : true + ) + .filter(rule => { + if (!enabledFilter) return true; + return ( + (rule.enabled && enabledFilter === "enabled") || + (!rule.enabled && enabledFilter === "disabled") + ); + }) + .filter(rule => + lastExecutedFilter && rule.lastExecuted + ? moment(lastExecutedFilter).isSameOrBefore(rule.lastExecuted, "date") + : true + ) + .value(); + + const total = filteredObjects.length; + const pageCount = paging ? Math.ceil(filteredObjects.length / pageSize) : 1; + const firstItem = paging ? (page - 1) * pageSize : 0; + const lastItem = paging ? firstItem + pageSize : total; + const rows = _.slice(filteredObjects, firstItem, lastItem); + + return { rows, pager: { page, pageCount, total } }; + } +} diff --git a/src/domain/rules/usecases/SaveSyncRuleUseCase.ts b/src/domain/rules/usecases/SaveSyncRuleUseCase.ts new file mode 100644 index 000000000..22061406f --- /dev/null +++ b/src/domain/rules/usecases/SaveSyncRuleUseCase.ts @@ -0,0 +1,15 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationRule } from "../entities/SynchronizationRule"; + +export class SaveSyncRuleUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(report: SynchronizationRule): Promise { + const user = await this.repositoryFactory.instanceRepository(this.localInstance).getUser(); + const persistedReport = report.update({ lastUpdated: new Date(), lastUpdatedBy: user }); + + await this.repositoryFactory.rulesRepository(this.localInstance).save(persistedReport); + } +} diff --git a/src/domain/synchronization/entities/SynchronizationBuilder.ts b/src/domain/synchronization/entities/SynchronizationBuilder.ts new file mode 100644 index 000000000..dc02cf870 --- /dev/null +++ b/src/domain/synchronization/entities/SynchronizationBuilder.ts @@ -0,0 +1,42 @@ +import { DataSynchronizationParams } from "../../aggregated/types"; +import { FilterRule } from "../../metadata/entities/FilterRule"; +import { MetadataSynchronizationParams } from "../../../types/synchronization"; + +export interface SynchronizationBuilder { + originInstance: string; + targetInstances: string[]; + metadataIds: string[]; + filterRules?: FilterRule[]; + excludedIds: string[]; + metadataTypes?: string[]; + syncRule?: string; + syncParams?: MetadataSynchronizationParams; + dataParams?: DataSynchronizationParams; +} + +export const defaultSynchronizationBuilder: SynchronizationBuilder = { + originInstance: "LOCAL", + targetInstances: [], + metadataIds: [], + filterRules: [], + excludedIds: [], + metadataTypes: [], + dataParams: { + strategy: "NEW_AND_UPDATES", + allAttributeCategoryOptions: true, + dryRun: false, + allEvents: true, + enableAggregation: undefined, + aggregationType: undefined, + }, + syncParams: { + importStrategy: "CREATE_AND_UPDATE", + enableMapping: false, + includeSharingSettings: true, + removeOrgUnitReferences: false, + useDefaultIncludeExclude: true, + atomicMode: "ALL", + mergeMode: "MERGE", + importMode: "COMMIT", + }, +}; diff --git a/src/domain/synchronization/entities/SynchronizationRule.ts b/src/domain/synchronization/entities/SynchronizationRule.ts deleted file mode 100644 index 0a42163ac..000000000 --- a/src/domain/synchronization/entities/SynchronizationRule.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SynchronizationBuilder } from "../../../types/synchronization"; -import { SharedRef } from "../../common/entities/Ref"; -import { SynchronizationType } from "./SynchronizationType"; - -export interface SynchronizationRule extends SharedRef { - code?: string; - created: Date; - description?: string; - builder: SynchronizationBuilder; - enabled: boolean; - lastExecuted?: Date; - frequency?: string; - type: SynchronizationType; -} diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 89146e2cf..7a99ed778 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -2,8 +2,7 @@ import { D2Api } from "d2-api/2.30"; import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import i18n from "../../../locales"; -import SyncRule from "../../../models/syncRule"; -import { SynchronizationBuilder } from "../../../types/synchronization"; +import { SynchronizationBuilder } from "../entities/SynchronizationBuilder"; import { cache } from "../../../utils/cache"; import { promiseMap } from "../../../utils/common"; import { getD2APiFromInstance } from "../../../utils/d2-utils"; @@ -240,9 +239,14 @@ export abstract class GenericSyncUseCase { // Phase 4: Update sync rule last executed date if (syncRule) { - const oldRule = await SyncRule.get(this.api, syncRule); - const updatedRule = oldRule.updateLastExecuted(new Date()); - await updatedRule.save(this.api); + const oldRule = await this.repositoryFactory + .rulesRepository(this.localInstance) + .getById(syncRule); + + if (oldRule) { + const updatedRule = oldRule.updateLastExecuted(new Date()); + await this.repositoryFactory.rulesRepository(this.localInstance).save(updatedRule); + } } // Phase 5: Update parent task status diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 3767b3249..81456cd1c 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -1,11 +1,11 @@ import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; -import { SynchronizationBuilder } from "../../../types/synchronization"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; import { MetadataResponsible } from "../../metadata/entities/MetadataResponsible"; +import { SynchronizationBuilder } from "../entities/SynchronizationBuilder"; import { SynchronizationType } from "../entities/SynchronizationType"; export type PrepareSyncError = "PULL_REQUEST" | "PULL_REQUEST_RESPONSIBLE" | "INSTANCE_NOT_FOUND"; diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 35c8837ba..01746d352 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -6,6 +6,7 @@ import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepositor import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; +import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; import { StorageDataStoreClient } from "../data/storage/StorageDataStoreClient"; import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; @@ -67,6 +68,10 @@ import { GetSyncReportUseCase } from "../domain/reports/usecases/GetSyncReportUs import { GetSyncResultsUseCase } from "../domain/reports/usecases/GetSyncResultsUseCase"; import { ListSyncReportUseCase } from "../domain/reports/usecases/ListSyncReportUseCase"; import { SaveSyncReportUseCase } from "../domain/reports/usecases/SaveSyncReportUseCase"; +import { DeleteSyncRuleUseCase } from "../domain/rules/usecases/DeleteSyncRuleUseCase"; +import { GetSyncRuleUseCase } from "../domain/rules/usecases/GetSyncRuleUseCase"; +import { ListSyncRuleUseCase } from "../domain/rules/usecases/ListSyncRuleUseCase"; +import { SaveSyncRuleUseCase } from "../domain/rules/usecases/SaveSyncRuleUseCase"; import { DownloadFileUseCase } from "../domain/storage/usecases/DownloadFileUseCase"; import { DeleteStoreUseCase } from "../domain/stores/usecases/DeleteStoreUseCase"; import { GetStoreUseCase } from "../domain/stores/usecases/GetStoreUseCase"; @@ -76,7 +81,7 @@ import { SetStoreAsDefaultUseCase } from "../domain/stores/usecases/SetStoreAsDe import { ValidateStoreUseCase } from "../domain/stores/usecases/ValidateStoreUseCase"; import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/CreatePullRequestUseCase"; import { PrepareSyncUseCase } from "../domain/synchronization/usecases/PrepareSyncUseCase"; -import { SynchronizationBuilder } from "../types/synchronization"; +import { SynchronizationBuilder } from "../domain/synchronization/entities/SynchronizationBuilder"; import { cache } from "../utils/cache"; export class CompositionRoot { @@ -93,6 +98,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); this.repositoryFactory.bind(Repositories.FileRepository, FileD2Repository); this.repositoryFactory.bind(Repositories.ReportsRepository, ReportsD2ApiRepository); + this.repositoryFactory.bind(Repositories.RulesRepository, RulesD2ApiRepository); this.repositoryFactory.bind( Repositories.MetadataRepository, MetadataJSONRepository, @@ -317,6 +323,16 @@ export class CompositionRoot { getSyncResults: new GetSyncResultsUseCase(this.repositoryFactory, this.localInstance), }); } + + @cache() + public get rules() { + return getExecute({ + list: new ListSyncRuleUseCase(this.repositoryFactory, this.localInstance), + save: new SaveSyncRuleUseCase(this.repositoryFactory, this.localInstance), + delete: new DeleteSyncRuleUseCase(this.repositoryFactory, this.localInstance), + get: new GetSyncRuleUseCase(this.repositoryFactory, this.localInstance), + }); + } } function getExecute, Key extends keyof UseCases>( diff --git a/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx b/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx index 5017b029d..a7c26d36b 100644 --- a/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx +++ b/src/presentation/react/core/components/delete-objects-table/DeletedObjectsTable.tsx @@ -1,24 +1,24 @@ import SyncIcon from "@material-ui/icons/Sync"; import { + DatePicker, ObjectsTable, ObjectsTableDetailField, ReferenceObject, TableColumn, TableState, - DatePicker, } from "d2-ui-components"; +import moment from "moment"; import React, { useEffect, useState } from "react"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; +import i18n from "../../../../../locales"; import DeletedObject from "../../../../../models/deletedObjects"; -import SyncRule from "../../../../../models/syncRule"; import { MetadataType } from "../../../../../utils/d2"; -import moment from "moment"; import { useAppContext } from "../../contexts/AppContext"; -import i18n from "../../../../../locales"; export interface DeletedObjectsTableProps { openSynchronizationDialog: () => void; - syncRule: SyncRule; - onChange: (syncRule: SyncRule) => void; + syncRule: SynchronizationRule; + onChange: (syncRule: SynchronizationRule) => void; } const DeletedObjectsTable: React.FC = ({ diff --git a/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx b/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx index e2a8a95d7..d1c6f185d 100644 --- a/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx +++ b/src/presentation/react/core/components/pull-request-creation-dialog/PullRequestCreationDialog.tsx @@ -14,7 +14,7 @@ import { NamedRef } from "../../../../../domain/common/entities/Ref"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../../locales"; -import { SynchronizationBuilder } from "../../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../../domain/synchronization/entities/SynchronizationBuilder"; import { useAppContext } from "../../contexts/AppContext"; export interface PullRequestCreation { diff --git a/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx b/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx index b74949698..2a146c467 100644 --- a/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx +++ b/src/presentation/react/core/components/sync-dialog/SyncDialog.tsx @@ -1,16 +1,16 @@ import DialogContent from "@material-ui/core/DialogContent"; import { ConfirmationDialog } from "d2-ui-components"; import React, { useEffect, useState } from "react"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; import SyncWizard from "../sync-wizard/SyncWizard"; interface SyncDialogProps { title: string; isOpen: boolean; - syncRule: SyncRule; - task: (syncRule: SyncRule) => void; - onChange(syncRule: SyncRule): void; + syncRule: SynchronizationRule; + task: (syncRule: SynchronizationRule) => void; + onChange(syncRule: SynchronizationRule): void; onClose: (importResponse?: any) => void; } diff --git a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx index 4e98471bb..ee33321f0 100644 --- a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx +++ b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx @@ -1,14 +1,14 @@ import { makeStyles, Typography } from "@material-ui/core"; import React from "react"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; import RadioButtonGroup from "../radio-button-group/RadioButtonGroup"; import { Toggle } from "../toggle/Toggle"; interface SyncParamsSelectorProps { generateNewUidDisabled?: boolean; - syncRule: SyncRule; - onChange(newParams: SyncRule): void; + syncRule: SynchronizationRule; + onChange(newParams: SynchronizationRule): void; } const useStyles = makeStyles({ diff --git a/src/presentation/react/core/components/sync-wizard/Steps.ts b/src/presentation/react/core/components/sync-wizard/Steps.ts index dea4c8e4f..00c14576c 100644 --- a/src/presentation/react/core/components/sync-wizard/Steps.ts +++ b/src/presentation/react/core/components/sync-wizard/Steps.ts @@ -1,8 +1,9 @@ import { WizardStep } from "d2-ui-components"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; import GeneralInfoStep from "./common/GeneralInfoStep"; import InstanceSelectionStep from "./common/InstanceSelectionStep"; +import MetadataFilterRulesStep from "./common/MetadataFilterRulesStep"; import MetadataSelectionStep from "./common/MetadataSelectionStep"; import SchedulerStep from "./common/SchedulerStep"; import SummaryStep from "./common/SummaryStep"; @@ -12,17 +13,16 @@ import EventsSelectionStep from "./data/EventsSelectionStep"; import OrganisationUnitsSelectionStep from "./data/OrganisationUnitsSelectionStep"; import PeriodSelectionStep from "./data/PeriodSelectionStep"; import MetadataIncludeExcludeStep from "./metadata/MetadataIncludeExcludeStep"; -import MetadataFilterRulesStep from "./common/MetadataFilterRulesStep"; export interface SyncWizardStep extends WizardStep { validationKeys: string[]; showOnSyncDialog?: boolean; - hidden?: (syncRule: SyncRule) => boolean; + hidden?: (syncRule: SynchronizationRule) => boolean; } export interface SyncWizardStepProps { - syncRule: SyncRule; - onChange: (syncRule: SyncRule) => void; + syncRule: SynchronizationRule; + onChange: (syncRule: SynchronizationRule) => void; onCancel: () => void; } diff --git a/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx b/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx index 7d8b66a4e..3dbe0ec74 100644 --- a/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx +++ b/src/presentation/react/core/components/sync-wizard/SyncWizard.tsx @@ -2,16 +2,16 @@ import { Wizard, WizardStep } from "d2-ui-components"; import _ from "lodash"; import React, { useEffect, useRef } from "react"; import { useLocation } from "react-router-dom"; -import SyncRule from "../../../../../models/syncRule"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import { getValidationMessages } from "../../../../../utils/old-validations"; import { getMetadata } from "../../../../../utils/synchronization"; import { useAppContext } from "../../contexts/AppContext"; import { aggregatedSteps, deletedSteps, eventsSteps, metadataSteps } from "./Steps"; interface SyncWizardProps { - syncRule: SyncRule; + syncRule: SynchronizationRule; isDialog?: boolean; - onChange?(syncRule: SyncRule): void; + onChange?(syncRule: SynchronizationRule): void; onCancel?(): void; } diff --git a/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx b/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx index b443dbaa4..f422fafa0 100644 --- a/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/GeneralInfoStep.tsx @@ -1,9 +1,9 @@ import { makeStyles, TextField } from "@material-ui/core"; import React, { useCallback, useState } from "react"; import { Instance } from "../../../../../../domain/instance/entities/Instance"; +import { SynchronizationRule } from "../../../../../../domain/rules/entities/SynchronizationRule"; import { Store } from "../../../../../../domain/stores/entities/Store"; import i18n from "../../../../../../locales"; -import SyncRule from "../../../../../../models/syncRule"; import { Dictionary } from "../../../../../../types/utils"; import { getValidationMessages } from "../../../../../../utils/old-validations"; import { @@ -18,7 +18,7 @@ export const GeneralInfoStep = ({ syncRule, onChange }: SyncWizardStepProps) => const [errors, setErrors] = useState>({}); const onChangeField = useCallback( - (field: keyof SyncRule) => { + (field: keyof SynchronizationRule) => { return (event: React.ChangeEvent<{ value: unknown }>) => { const newRule = syncRule.update({ [field]: event.target.value }); const messages = getValidationMessages(newRule, [field]); diff --git a/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx index 6ba97876a..bdfe9ef23 100644 --- a/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/data/EventsSelectionStep.tsx @@ -4,8 +4,8 @@ import _ from "lodash"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ProgramEvent } from "../../../../../../domain/events/entities/ProgramEvent"; import { DataElement, Program } from "../../../../../../domain/metadata/entities/MetadataEntities"; +import { SynchronizationRule } from "../../../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../../../locales"; -import SyncRule from "../../../../../../models/syncRule"; import { useAppContext } from "../../../contexts/AppContext"; import Dropdown from "../../dropdown/Dropdown"; import { Toggle } from "../../toggle/Toggle"; @@ -22,7 +22,7 @@ type CustomProgram = Program & { export default function EventsSelectionStep({ syncRule, onChange }: SyncWizardStepProps) { const { compositionRoot } = useAppContext(); - const [memoizedSyncRule] = useState(syncRule); + const [memoizedSyncRule] = useState(syncRule); const [objects, setObjects] = useState(); const [programs, setPrograms] = useState([]); const [programFilter, changeProgramFilter] = useState(""); diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx index ef0b3a233..44a18ef59 100644 --- a/src/presentation/webapp/Root.tsx +++ b/src/presentation/webapp/Root.tsx @@ -1,9 +1,13 @@ import React from "react"; import { HashRouter, Switch } from "react-router-dom"; +import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; +import * as permissions from "../../utils/permissions"; import RouteWithSession from "../react/core/components/auth/RouteWithSession"; import RouteWithSessionAndAuth from "../react/core/components/auth/RouteWithSessionAndAuth"; -import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; +import { useAppContext } from "../react/core/contexts/AppContext"; import HistoryPage from "./core/pages/history/HistoryPage"; +import HomePage from "./core/pages/home/HomePage"; +import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; import InstanceListPage from "./core/pages/instance-list/InstanceListPage"; import InstanceMappingLandingPage from "./core/pages/instance-mapping/InstanceMappingLandingPage"; import InstanceMappingPage from "./core/pages/instance-mapping/InstanceMappingPage"; @@ -18,10 +22,6 @@ import SyncRulesCreationPage, { SyncRulesCreationParams, } from "./core/pages/sync-rules-creation/SyncRulesCreationPage"; import SyncRulesPage from "./core/pages/sync-rules-list/SyncRulesListPage"; -import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; -import { useAppContext } from "../react/core/contexts/AppContext"; -import * as permissions from "../../utils/permissions"; -import HomePage from "./core/pages/home/HomePage"; import { MSFHomePage } from "./msf-aggregate-data/pages/MSFHomePage"; export type AppVariant = @@ -32,7 +32,7 @@ export type AppVariant = const Root: React.FC = () => { const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; - const { api } = useAppContext(); + const { api, compositionRoot } = useAppContext(); return ( @@ -65,10 +65,11 @@ const Root: React.FC = () => { { + authorize={async props => { const { id } = props.match.params as SyncRulesCreationParams; + const syncRule = await compositionRoot.rules.get(id); - return permissions.verifyUserHasAccessToSyncRule(api, id); + return permissions.verifyUserHasAccessToSyncRule(api, syncRule); }} render={() => } /> diff --git a/src/presentation/webapp/core/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx index d1f8088f7..6b6c0dd2d 100644 --- a/src/presentation/webapp/core/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -17,10 +17,9 @@ import _ from "lodash"; import React, { useCallback, useEffect, useState } from "react"; import { Link, useHistory, useParams } from "react-router-dom"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; -import { SynchronizationRule } from "../../../../../domain/synchronization/entities/SynchronizationRule"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; import { promiseMap } from "../../../../../utils/common"; import { getValueForCollection } from "../../../../../utils/d2-ui-components"; import { isAppConfigurator } from "../../../../../utils/permissions"; @@ -113,10 +112,12 @@ const HistoryPage: React.FC = () => { ); useEffect(() => { - SyncRule.list(api, { type }, { paging: false }).then(({ objects }) => - setSyncRules(objects) - ); + compositionRoot.rules + .list({ filters: { type }, paging: false }) + .then(({ rows }) => setSyncRules(rows)); + if (id) compositionRoot.reports.get(id).then(setSyncReport); + isAppConfigurator(api).then(setAppConfigurator); }, [api, id, type, compositionRoot]); diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 0794f66ce..b6801afc0 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -11,6 +11,7 @@ import { useHistory, useParams } from "react-router-dom"; import { Ref } from "../../../../../domain/common/entities/Ref"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; import { Store } from "../../../../../domain/stores/entities/Store"; import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../../locales"; @@ -27,7 +28,6 @@ import { DataElementGroupModel, DataElementGroupSetModel, } from "../../../../../models/dhis/metadata"; -import SyncRule from "../../../../../models/syncRule"; import { MetadataType } from "../../../../../utils/d2"; import { isAppConfigurator } from "../../../../../utils/permissions"; import DeletedObjectsTable from "../../../../react/core/components/delete-objects-table/DeletedObjectsTable"; @@ -88,7 +88,9 @@ const ManualSyncPage: React.FC = () => { const { type } = useParams() as { type: SynchronizationType }; const { title, models } = config[type]; - const [syncRule, updateSyncRule] = useState(SyncRule.createOnDemand(type)); + const [syncRule, updateSyncRule] = useState( + SynchronizationRule.createOnDemand(type) + ); const [appConfigurator, updateAppConfigurator] = useState(false); const [syncReport, setSyncReport] = useState(null); const [syncDialogOpen, setSyncDialogOpen] = useState(false); @@ -97,7 +99,7 @@ const ManualSyncPage: React.FC = () => { const [pullRequestProps, setPullRequestProps] = useState(); const [dialogProps, updateDialog] = useState(null); - const updateSyncRuleFromDialog = useCallback((newSyncRule: SyncRule) => { + const updateSyncRuleFromDialog = useCallback((newSyncRule: SynchronizationRule) => { const id = newSyncRule.targetInstances[0]; setDestinationInstanceBase(id ? { id } : undefined); updateSyncRule(newSyncRule); @@ -122,7 +124,7 @@ const ManualSyncPage: React.FC = () => { const updateSelection = useCallback( (selection: string[], exclusion: string[]) => { updateSyncRule(({ originInstance, targetInstances }) => - SyncRule.createOnDemand(type) + SynchronizationRule.createOnDemand(type) .updateBuilder({ originInstance }) .updateTargetInstances(targetInstances) .updateMetadataIds(selection) @@ -156,7 +158,7 @@ const ManualSyncPage: React.FC = () => { setSyncDialogOpen(false); }; - const handleSynchronization = async (syncRule: SyncRule) => { + const handleSynchronization = async (syncRule: SynchronizationRule) => { loading.show(true, i18n.t(`Synchronizing ${syncRule.type}`)); const result = await compositionRoot.sync.prepare(syncRule.type, syncRule.toBuilder()); diff --git a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx index 687f21c78..991bfb187 100644 --- a/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-creation/SyncRulesCreationPage.tsx @@ -1,13 +1,13 @@ import { ConfirmationDialog, useLoading } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory, useLocation, useParams } from "react-router-dom"; +import { SynchronizationRule } from "../../../../../domain/rules/entities/SynchronizationRule"; +import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; +import i18n from "../../../../../locales"; import PageHeader from "../../../../react/core/components/page-header/PageHeader"; import SyncWizard from "../../../../react/core/components/sync-wizard/SyncWizard"; import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; import { useAppContext } from "../../../../react/core/contexts/AppContext"; -import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; -import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; export interface SyncRulesCreationParams { id: string; @@ -17,12 +17,15 @@ export interface SyncRulesCreationParams { const SyncRulesCreation: React.FC = () => { const history = useHistory(); - const location = useLocation<{ syncRule?: SyncRule }>(); + const location = useLocation<{ syncRule?: SynchronizationRule }>(); const loading = useLoading(); const { id, action, type } = useParams() as SyncRulesCreationParams; - const { api } = useAppContext(); + const { compositionRoot } = useAppContext(); + const [dialogOpen, updateDialogOpen] = useState(false); - const [syncRule, updateSyncRule] = useState(location.state?.syncRule ?? SyncRule.create(type)); + const [syncRule, updateSyncRule] = useState( + location.state?.syncRule ?? SynchronizationRule.create(type) + ); const isEdit = action === "edit" && !!id; const title = !isEdit @@ -44,12 +47,12 @@ const SyncRulesCreation: React.FC = () => { useEffect(() => { if (isEdit && !!id) { loading.show(true, "Loading sync rule"); - SyncRule.get(api, id).then(syncRule => { - updateSyncRule(syncRule); + compositionRoot.rules.get(id).then(syncRule => { + updateSyncRule(syncRule ?? SynchronizationRule.create(type)); loading.reset(); }); } - }, [api, loading, isEdit, id]); + }, [compositionRoot, loading, isEdit, id, type]); return ( diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index e6df1d1e9..5a45c3739 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -17,15 +17,17 @@ import { useSnackbar, } from "d2-ui-components"; import _ from "lodash"; -import { Moment } from "moment"; import React, { useEffect, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Instance } from "../../../../../domain/instance/entities/Instance"; import { SynchronizationReport } from "../../../../../domain/reports/entities/SynchronizationReport"; -import { SynchronizationRule } from "../../../../../domain/synchronization/entities/SynchronizationRule"; +import { + SynchronizationRule, + SynchronizationRuleData, +} from "../../../../../domain/rules/entities/SynchronizationRule"; import { SynchronizationType } from "../../../../../domain/synchronization/entities/SynchronizationType"; import i18n from "../../../../../locales"; -import SyncRule from "../../../../../models/syncRule"; +import { promiseMap } from "../../../../../utils/common"; import { getValueForCollection } from "../../../../../utils/d2-ui-components"; import { getValidationMessages } from "../../../../../utils/old-validations"; import { @@ -76,7 +78,7 @@ const SyncRulesPage: React.FC = () => { const { type } = useParams() as { type: SynchronizationType }; const { title } = config[type]; - const [rows, setRows] = useState([]); + const [rows, setRows] = useState([]); const [refreshKey, setRefreshKey] = useState(0); const [selection, updateSelection] = useState([]); @@ -84,22 +86,21 @@ const SyncRulesPage: React.FC = () => { const [search, setSearchFilter] = useState(""); const [targetInstanceFilter, setTargetInstanceFilter] = useState(""); const [enabledFilter, setEnabledFilter] = useState(""); - const [lastExecutedFilter, setLastExecutedFilter] = useState(null); + const [lastExecutedFilter, setLastExecutedFilter] = useState(null); const [syncReport, setSyncReport] = useState(null); const [sharingSettingsObject, setSharingSettingsObject] = useState(null); const [pullRequestProps, setPullRequestProps] = useState(); const [dialogProps, updateDialog] = useState(null); useEffect(() => { - SyncRule.list( - api, - { type, targetInstanceFilter, enabledFilter, lastExecutedFilter, search }, - { paging: false } - ).then(({ objects }) => { - setRows(objects.map(SyncRule.build)); - }); + compositionRoot.rules + .list({ + filters: { type, targetInstanceFilter, enabledFilter, lastExecutedFilter, search }, + paging: false, + }) + .then(({ rows }) => setRows(rows)); }, [ - api, + compositionRoot, refreshKey, type, search, @@ -123,24 +124,24 @@ const SyncRulesPage: React.FC = () => { isAppExecutor(api).then(setAppExecutor); }, [api, compositionRoot]); - const getTargetInstances = (rule: SyncRule) => { + const getTargetInstances = (rule: SynchronizationRule) => { return _(rule.targetInstances) .map(id => allInstances.find(instance => instance.id === id)) .compact() .map(({ name }) => ({ name })); }; - const getReadableFrequency = (rule: SyncRule) => { + const getReadableFrequency = (rule: SynchronizationRule) => { return rule.longFrequency; }; - const columns: TableColumn[] = [ + const columns: TableColumn[] = [ { name: "name", text: i18n.t("Name"), sortable: true }, { name: "targetInstances", text: i18n.t("Destination instances"), sortable: false, - getValue: (ruleData: SyncRule) => + getValue: (ruleData: SynchronizationRule) => getTargetInstances(ruleData) .map(e => e.name) .join(", "), @@ -164,7 +165,7 @@ const SyncRulesPage: React.FC = () => { }, ]; - const details: ObjectsTableDetailField[] = [ + const details: ObjectsTableDetailField[] = [ { name: "name", text: i18n.t("Name") }, { name: "description", text: i18n.t("Description") }, { @@ -188,9 +189,11 @@ const SyncRulesPage: React.FC = () => { const downloadJSON = async (ids: string[]) => { const id = _.first(ids); if (!id) return; - loading.show(true, "Generating JSON file"); - const rule = await SyncRule.get(api, id); + const rule = await compositionRoot.rules.get(id); + if (!rule) return; + + loading.show(true, "Generating JSON file"); const sync = compositionRoot.sync[rule.type](rule.toBuilder()); const payload = await sync.buildPayload(); @@ -205,36 +208,11 @@ const SyncRulesPage: React.FC = () => { const confirmDelete = async () => { loading.show(true, i18n.t("Deleting Sync Rules")); - const results = []; - for (const id of toDelete) { - const rule = await SyncRule.get(api, id); - const deletedRuleLabel = `${rule.name} (${i18n.t("deleted")})`; - - results.push(await rule.remove(api)); - - // TODO: Fully refactor with SyncRule - const syncReports = await compositionRoot.reports.list({ - filters: { type: rule.type, syncRuleFilter: id }, - paging: false, - }); - - for (const syncReportData of syncReports.rows) { - const syncReport = SynchronizationReport.build({ - ...syncReportData, - deletedSyncRuleLabel: deletedRuleLabel, - }); + await promiseMap(toDelete, id => compositionRoot.rules.delete(id)); - await compositionRoot.reports.save(syncReport); - } - } - - if (_.some(results, ["status", false])) { - snackbar.error(i18n.t("Failed to delete some rules")); - } else { - snackbar.success( - i18n.t("Successfully deleted {{count}} rules", { count: toDelete.length }) - ); - } + snackbar.success( + i18n.t("Successfully deleted {{count}} rules", { count: toDelete.length }) + ); loading.reset(); setToDelete([]); @@ -256,7 +234,9 @@ const SyncRulesPage: React.FC = () => { const replicateRule = async (ids: string[]) => { const id = _.first(ids); if (!id) return; - const rule = await SyncRule.get(api, id); + + const rule = await compositionRoot.rules.get(id); + if (!rule) return; history.push({ pathname: `/sync-rules/${type}/new`, @@ -268,7 +248,9 @@ const SyncRulesPage: React.FC = () => { const id = _.first(ids); if (!id) return; - const rule = await SyncRule.get(api, id); + const rule = await compositionRoot.rules.get(id); + if (!rule) return; + const { builder, id: syncRule, type = "metadata" } = rule; loading.show(true, i18n.t("Synchronizing {{name}}", rule)); @@ -347,7 +329,9 @@ const SyncRulesPage: React.FC = () => { const toggleEnable = async (ids: string[]) => { const id = _.first(ids); if (!id) return; - const oldSyncRule = await SyncRule.get(api, id); + + const oldSyncRule = await compositionRoot.rules.get(id); + if (!oldSyncRule) return; const syncRule = oldSyncRule.updateEnabled(!oldSyncRule.enabled); const errors = getValidationMessages(syncRule); @@ -356,7 +340,7 @@ const SyncRulesPage: React.FC = () => { autoHideDuration: null, }); } else { - await syncRule.save(api); + await compositionRoot.rules.save(syncRule); snackbar.success(i18n.t("Successfully updated sync rule")); setRefreshKey(Math.random()); } @@ -365,7 +349,9 @@ const SyncRulesPage: React.FC = () => { const openSharingSettings = async (ids: string[]) => { const id = _.first(ids); if (!id) return; - const syncRule = await SyncRule.get(api, id); + + const syncRule = await compositionRoot.rules.get(id); + if (!syncRule) return; setSharingSettingsObject({ object: syncRule.toObject(), @@ -373,7 +359,7 @@ const SyncRulesPage: React.FC = () => { }); }; - const verifyUserHasAccess = (rules: SyncRule[], condition = false) => { + const verifyUserHasAccess = (rules: SynchronizationRule[], condition = false) => { if (globalAdmin) return true; for (const rule of rules) { @@ -383,11 +369,11 @@ const SyncRulesPage: React.FC = () => { return condition; }; - const verifyUserCanEdit = (rules: SyncRule[]) => { + const verifyUserCanEdit = (rules: SynchronizationRule[]) => { return verifyUserHasAccess(rules, appConfigurator); }; - const verifyUserCanEditSharingSettings = (rules: SyncRule[]) => { + const verifyUserCanEditSharingSettings = (rules: SynchronizationRule[]) => { return verifyUserHasAccess(rules, appConfigurator); }; @@ -399,7 +385,7 @@ const SyncRulesPage: React.FC = () => { return appConfigurator; }; - const actions: TableAction[] = [ + const actions: TableAction[] = [ { name: "details", text: i18n.t("Details"), @@ -480,8 +466,10 @@ const SyncRulesPage: React.FC = () => { }, }; - const syncRule = SyncRule.build(newSharingSettings.object as SynchronizationRule); - await syncRule.save(api); + const syncRule = SynchronizationRule.build( + newSharingSettings.object as SynchronizationRuleData + ); + await compositionRoot.rules.save(syncRule); setSharingSettingsObject(newSharingSettings); }; @@ -520,7 +508,7 @@ const SyncRulesPage: React.FC = () => { return ( - + rows={rows} columns={columns} details={details} diff --git a/src/scheduler/scheduler.ts b/src/scheduler/scheduler.ts index 48bcbe291..05bafaa6e 100644 --- a/src/scheduler/scheduler.ts +++ b/src/scheduler/scheduler.ts @@ -3,8 +3,7 @@ import _ from "lodash"; import { getLogger } from "log4js"; import moment from "moment"; import schedule from "node-schedule"; -import { SynchronizationRule } from "../domain/synchronization/entities/SynchronizationRule"; -import SyncRule from "../models/syncRule"; +import { SynchronizationRule } from "../domain/rules/entities/SynchronizationRule"; import { CompositionRoot } from "../presentation/CompositionRoot"; import { D2Api } from "../types/d2-api"; @@ -12,7 +11,9 @@ export default class Scheduler { constructor(private api: D2Api, private compositionRoot: CompositionRoot) {} private synchronizationTask = async (id: string): Promise => { - const rule = await SyncRule.get(this.api, id); + const rule = await this.compositionRoot.rules.get(id); + if (!rule) return; + const { name, frequency, builder, id: syncRule, type = "metadata" } = rule; const logger = getLogger(name); @@ -57,7 +58,7 @@ export default class Scheduler { }; private fetchTask = async (): Promise => { - const { objects: rules } = await SyncRule.list(this.api, {}, { paging: false }); + const { rows: rules } = await this.compositionRoot.rules.list({ paging: false }); const jobs = _.filter(rules, rule => rule.enabled); const enabledJobIds = jobs.map(({ id }) => id); diff --git a/src/types/d2-ui-components.ts b/src/types/d2-ui-components.ts index 95a311239..871cde803 100644 --- a/src/types/d2-ui-components.ts +++ b/src/types/d2-ui-components.ts @@ -1,5 +1,4 @@ import { Moment } from "moment"; -import { SynchronizationType } from "../domain/synchronization/entities/SynchronizationType"; export interface TableList { objects: any[]; @@ -20,19 +19,6 @@ export interface TableFilters { metadataType?: string; } -export interface SyncReportTableFilters extends TableFilters { - type: string; - statusFilter?: string; - syncRuleFilter?: string; -} - -export interface SyncRuleTableFilters extends TableFilters { - targetInstanceFilter?: string; - enabledFilter?: string; - lastExecutedFilter?: Moment | null; - type?: SynchronizationType; -} - export interface TablePagination { page?: number; pageSize?: number; diff --git a/src/types/synchronization.ts b/src/types/synchronization.ts index ea289021e..b31fe2746 100644 --- a/src/types/synchronization.ts +++ b/src/types/synchronization.ts @@ -1,23 +1,8 @@ import { DataSynchronizationParams } from "../domain/aggregated/types"; -import { FilterRule } from "../domain/metadata/entities/FilterRule"; import { MetadataEntities } from "../domain/metadata/entities/MetadataEntities"; import { SynchronizationReport } from "../domain/reports/entities/SynchronizationReport"; import { MetadataImportParams } from "./d2"; -//TODO: Review this to move it to domain - -export interface SynchronizationBuilder { - originInstance: string; - targetInstances: string[]; - metadataIds: string[]; - filterRules?: FilterRule[]; - excludedIds: string[]; - metadataTypes?: string[]; - syncRule?: string; - syncParams?: MetadataSynchronizationParams; - dataParams?: DataSynchronizationParams; -} - export interface MetadataIncludeExcludeRules { [metadataType: string]: ExcludeIncludeRules; } diff --git a/src/utils/old-validations.ts b/src/utils/old-validations.ts index a1eadc647..09e47469d 100644 --- a/src/utils/old-validations.ts +++ b/src/utils/old-validations.ts @@ -1,6 +1,6 @@ import _ from "lodash"; +import { SynchronizationRule } from "../domain/rules/entities/SynchronizationRule"; import i18n from "../locales"; -import SyncRule from "../models/syncRule"; // TODO: This should be migrated to use the new ValidationError[] export interface OldValidation { @@ -25,7 +25,10 @@ const translations: { [key: string]: (namespace: object) => string } = { }; // TODO: This should be migrated to use the new ValidationError[] -export function getValidationMessages(model: SyncRule, validationKeys: string[] | null = null) { +export function getValidationMessages( + model: SynchronizationRule, + validationKeys: string[] | null = null +) { const validationObj = model.validate(); return _(validationObj) diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index a15c99ec7..61e4a40bf 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -1,8 +1,7 @@ import axios from "axios"; -import { D2Api } from "../types/d2-api"; import memoize from "nano-memoize"; -import SyncRule from "../models/syncRule"; -import { Maybe } from "../types/utils"; +import { SynchronizationRule } from "../domain/rules/entities/SynchronizationRule"; +import { D2Api } from "../types/d2-api"; const AppRoles: { [key: string]: { @@ -109,17 +108,17 @@ export const isAppExecutor = async (api: D2Api) => { return globalAdmin || !!userRoles.find((role: any) => role.name === name); }; -export const verifyUserHasAccessToSyncRule = async (api: D2Api, syncRuleUId: Maybe) => { +export const verifyUserHasAccessToSyncRule = async ( + api: D2Api, + syncRule: SynchronizationRule | undefined +) => { const globalAdmin = await isGlobalAdmin(api); if (globalAdmin) return true; const appConfigurator = await isAppConfigurator(api); const userInfo = await getUserInfo(api); - if (!syncRuleUId) return appConfigurator; - - const syncRule = await SyncRule.get(api, syncRuleUId); - const syncRuleVisibleToUser = syncRuleUId ? syncRule.isVisibleToUser(userInfo, "WRITE") : true; + const syncRuleVisibleToUser = syncRule?.isVisibleToUser(userInfo, "WRITE") ?? true; return appConfigurator && syncRuleVisibleToUser; }; diff --git a/src/utils/synchronization.ts b/src/utils/synchronization.ts index 5d9bb5607..ad3859a73 100644 --- a/src/utils/synchronization.ts +++ b/src/utils/synchronization.ts @@ -6,8 +6,8 @@ import { MetadataMappingDictionary, } from "../domain/mapping/entities/MetadataMapping"; import { CategoryOptionCombo } from "../domain/metadata/entities/MetadataEntities"; +import { SynchronizationRule } from "../domain/rules/entities/SynchronizationRule"; import i18n from "../locales"; -import SyncRule from "../models/syncRule"; import { D2Api } from "../types/d2-api"; import { buildObject } from "../types/utils"; import "../utils/lodash-mixins"; @@ -62,7 +62,7 @@ export const availablePeriods = buildObject<{ export type PeriodType = keyof typeof availablePeriods; -export function requestJSONDownload(payload: object, syncRule: SyncRule) { +export function requestJSONDownload(payload: object, syncRule: SynchronizationRule) { const json = JSON.stringify(payload, null, 4); const blob = new Blob([json], { type: "application/json" }); const ruleName = _.kebabCase(_.toLower(syncRule.name)); From 617a171ce73aff3d5a7cc04a615310f06e5c279d Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 4 Dec 2020 12:59:00 +0100 Subject: [PATCH 060/163] Update old components to TS --- .../{SchedulerStep.jsx => SchedulerStep.tsx} | 11 +++--- .../{SummaryStep.jsx => SummaryStep.tsx} | 36 +++++++++++-------- 2 files changed, 28 insertions(+), 19 deletions(-) rename src/presentation/react/core/components/sync-wizard/common/{SchedulerStep.jsx => SchedulerStep.tsx} (89%) rename src/presentation/react/core/components/sync-wizard/common/{SummaryStep.jsx => SummaryStep.tsx} (94%) diff --git a/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx b/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.tsx similarity index 89% rename from src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx rename to src/presentation/react/core/components/sync-wizard/common/SchedulerStep.tsx index 6247d9801..675bb1a92 100644 --- a/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.jsx +++ b/src/presentation/react/core/components/sync-wizard/common/SchedulerStep.tsx @@ -1,10 +1,13 @@ +//@ts-ignore import { DropDown, TextField } from "@dhis2/d2-ui-core"; +//@ts-ignore import { FormBuilder } from "@dhis2/d2-ui-forms"; import PropTypes from "prop-types"; import React from "react"; import i18n from "../../../../../../locales"; import isValidCronExpression from "../../../../../../utils/validCronExpression"; import { Toggle } from "../../toggle/Toggle"; +import { SyncWizardStepProps } from "../Steps"; const cronExpressions = [ { displayName: i18n.t("Every day"), id: "0 0 0 ? * *" }, @@ -14,10 +17,10 @@ const cronExpressions = [ { displayName: i18n.t("Every year"), id: "0 0 0 1 1 ?" }, ]; -const SchedulerStep = ({ syncRule, onChange }) => { +const SchedulerStep = ({ syncRule, onChange }: SyncWizardStepProps) => { const selectedCron = cronExpressions.find(({ id }) => id === syncRule.frequency); - const updateFields = (field, value) => { + const updateFields = (field: string, value: any) => { if (field === "enabled") { onChange(syncRule.updateEnabled(value)); } else if (field === "frequency" || field === "frequencyDropdown") { @@ -39,7 +42,7 @@ const SchedulerStep = ({ syncRule, onChange }) => { }, { name: "frequencyDropdown", - value: selectedCron?.value ?? "", + value: selectedCron?.id ?? "", component: DropDown, props: { hintText: syncRule.readableFrequency || i18n.t("Select frequency template"), @@ -62,7 +65,7 @@ const SchedulerStep = ({ syncRule, onChange }) => { validators: [ { message: i18n.t("Cron expression must be valid"), - validator(value) { + validator(value: string | undefined) { return !value || isValidCronExpression(value); }, }, diff --git a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx similarity index 94% rename from src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx rename to src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx index 89334cedb..e19df2dbc 100644 --- a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.jsx +++ b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx @@ -4,6 +4,8 @@ import _ from "lodash"; import moment from "moment"; import React, { useEffect, useMemo, useState } from "react"; import { useHistory } from "react-router-dom"; +import { Instance } from "../../../../../../domain/instance/entities/Instance"; +import { filterRuleToString } from "../../../../../../domain/metadata/entities/FilterRule"; import { includeExcludeRulesFriendlyNames } from "../../../../../../domain/metadata/entities/MetadataFriendlyNames"; import { cleanOrgUnitPaths } from "../../../../../../domain/synchronization/utils"; import i18n from "../../../../../../locales"; @@ -15,10 +17,10 @@ import { } from "../../../../../../utils/synchronization"; import { useAppContext } from "../../../contexts/AppContext"; import { buildAggregationItems } from "../data/AggregationStep"; +import { SyncWizardStepProps } from "../Steps"; import { buildInstanceOptions } from "./InstanceSelectionStep"; -import { filterRuleToString } from "../../../../../../domain/metadata/entities/FilterRule"; -const LiEntry = ({ label, value, children }) => { +const LiEntry: React.FC<{ label: string; value?: string }> = ({ label, value, children }) => { return (
  • {label} @@ -41,7 +43,7 @@ const useStyles = makeStyles({ }, }); -const SaveStep = ({ syncRule, onCancel }) => { +const SaveStep = ({ syncRule, onCancel }: SyncWizardStepProps) => { const { api, compositionRoot } = useAppContext(); const snackbar = useSnackbar(); @@ -51,8 +53,8 @@ const SaveStep = ({ syncRule, onCancel }) => { const [cancelDialogOpen, setCancelDialogOpen] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [metadata, updateMetadata] = useState({}); - const [targetInstances, setTargetInstances] = useState([]); + const [metadata, updateMetadata] = useState>({}); + const [targetInstances, setTargetInstances] = useState([]); const instanceOptions = buildInstanceOptions(targetInstances); const openCancelDialog = () => setCancelDialogOpen(true); @@ -70,8 +72,9 @@ const SaveStep = ({ syncRule, onCancel }) => { if (errors.length > 0) { snackbar.error(errors.join("\n")); } else { - const newSyncRule = await syncRule.updateName(name).save(api); - history.push(`/sync-rules/${newSyncRule.type}/edit/${newSyncRule.id}`); + const newSyncRule = syncRule.updateName(name); + await compositionRoot.rules.save(newSyncRule); + history.push(`/sync-rules/${syncRule.type}/edit/${newSyncRule.id}`); onCancel(); } @@ -156,6 +159,7 @@ const SaveStep = ({ syncRule, onCancel }) => { items.length > 0 && (
      @@ -175,14 +179,16 @@ const SaveStep = ({ syncRule, onCancel }) => { })} >
        - {_.sortBy(syncRule.filterRules, fr => fr.type).map(filterRule => { - return ( - - ); - })} + {_.sortBy(syncRule.filterRules, fr => fr.metadataType).map( + filterRule => { + return ( + + ); + } + )}
      )} From 7adc05f7feddb0c8f492fa104ec9a1e48199cf81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Fri, 4 Dec 2020 13:24:35 +0100 Subject: [PATCH 061/163] Execute event sync rules and show progress --- i18n/en.pot | 19 ++++++- i18n/es.po | 17 +++++- i18n/fr.po | 17 +++++- i18n/pt.po | 17 +++++- .../sync-rules-list/SyncRulesListPage.tsx | 6 +- .../msf-aggregate-data/pages/MSFHomePage.tsx | 25 +++++---- .../pages/MSFHomePagePresenter.ts | 55 +++++++++++++++++++ 7 files changed, 138 insertions(+), 18 deletions(-) create mode 100644 src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts diff --git a/i18n/en.pot b/i18n/en.pot index 6c285422c..86a7ad6e8 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-12-04T10:44:16.777Z\n" -"PO-Revision-Date: 2020-12-04T10:44:16.777Z\n" +"POT-Creation-Date: 2020-12-04T12:19:04.582Z\n" +"PO-Revision-Date: 2020-12-04T12:19:04.582Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1751,6 +1751,21 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "Starting Aggregate Data..." +msgstr "" + +msgid "Finished Aggregate Data" +msgstr "" + +msgid "Starting Sync Rule {{name}} ..." +msgstr "" + +msgid "Finished Sync Rule {{name}}" +msgstr "" + +msgid "Finished Sync Rule {{name}} with errors" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 3522964fa..aa1923ae3 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-12-04T09:29:33.239Z\n" +"POT-Creation-Date: 2020-12-04T12:19:04.582Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1758,6 +1758,21 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "Starting Aggregate Data..." +msgstr "" + +msgid "Finished Aggregate Data" +msgstr "" + +msgid "Starting Sync Rule {{name}} ..." +msgstr "" + +msgid "Finished Sync Rule {{name}}" +msgstr "" + +msgid "Finished Sync Rule {{name}} with errors" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 10dd81bb3..b62522ab0 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-12-04T09:29:33.239Z\n" +"POT-Creation-Date: 2020-12-04T12:19:04.582Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1754,6 +1754,21 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "Starting Aggregate Data..." +msgstr "" + +msgid "Finished Aggregate Data" +msgstr "" + +msgid "Starting Sync Rule {{name}} ..." +msgstr "" + +msgid "Finished Sync Rule {{name}}" +msgstr "" + +msgid "Finished Sync Rule {{name}} with errors" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 10dd81bb3..b62522ab0 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-12-04T09:29:33.239Z\n" +"POT-Creation-Date: 2020-12-04T12:19:04.582Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1754,6 +1754,21 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "Starting Aggregate Data..." +msgstr "" + +msgid "Finished Aggregate Data" +msgstr "" + +msgid "Starting Sync Rule {{name}} ..." +msgstr "" + +msgid "Finished Sync Rule {{name}}" +msgstr "" + +msgid "Finished Sync Rule {{name}} with errors" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index e6df1d1e9..724b9a6e3 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -198,8 +198,8 @@ const SyncRulesPage: React.FC = () => { loading.reset(); }; - const backHome = () => { - history.push("/"); + const back = () => { + history.goBack(); }; const confirmDelete = async () => { @@ -519,7 +519,7 @@ const SyncRulesPage: React.FC = () => { return ( - + rows={rows} columns={columns} diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 445d58b1e..febd26e3b 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,8 +1,8 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; -import i18n from "d2-ui-components/locales"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { Period } from "../../../../domain/common/entities/Period"; +import i18n from "../../../../locales"; import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; @@ -11,25 +11,34 @@ import { MSFSettings, MSFSettingsDialog, } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +import { executeAggregateData } from "./MSFHomePagePresenter"; export const MSFHomePage: React.FC = () => { const classes = useStyles(); const history = useHistory(); + const { api, compositionRoot } = useAppContext(); + const [syncProgress, setSyncProgress] = useState([]); const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); const [period, setPeriod] = useState(Period.createDefault()); + const [msfSettings, setMsfSettings] = useState({ runAnalytics: "by-sync-rule-settings", }); const [globalAdmin, setGlobalAdmin] = useState(false); - const { api } = useAppContext(); useEffect(() => { isGlobalAdmin(api).then(setGlobalAdmin); }, [api]); - const handleAggregateData = () => {}; + const handleAggregateData = () => { + executeAggregateData(api, compositionRoot, progress => { + console.log({ syncProcess: syncProgress }); + setSyncProgress(progress); + }); + }; + const handleAdvancedSettings = () => { setShowPeriodDialog(true); }; @@ -84,13 +93,9 @@ export const MSFHomePage: React.FC = () => { {i18n.t("Synchronization Progress")} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} - {"Synchronizing Sync Rule 1 ..."} + {syncProgress.map((trace, index) => ( + {trace} + ))} diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts new file mode 100644 index 000000000..dda7d04a2 --- /dev/null +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -0,0 +1,55 @@ +import SyncRule from "../../../../models/syncRule"; +import { CompositionRoot } from "../../../CompositionRoot"; +import { D2Api } from "../../../../types/d2-api"; +import i18n from "../../../../locales"; + +//TODO: maybe convert to class and presenter to use MVP, MVI pattern +export async function executeAggregateData( + api: D2Api, + compositionRoot: CompositionRoot, + onProgressChange: (progress: string[]) => void +) { + const eventSyncRules = ( + await SyncRule.list(api, { type: "events" }, { paging: false }) + ).objects.slice(0, 2); + + let syncProgress: string[] = [i18n.t(`Starting Aggregate Data...`)]; + + onProgressChange(syncProgress); + + const onSyncRuleProgressChange = (event: string) => { + syncProgress = [...syncProgress, event]; + onProgressChange(syncProgress); + }; + + for (const syncRule of eventSyncRules) { + await executeSyncRule(api, compositionRoot, syncRule.id, onSyncRuleProgressChange); + } + + onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); +} + +const executeSyncRule = async ( + api: D2Api, + compositionRoot: CompositionRoot, + id: string, + onProgressChange: (event: string) => void +): Promise => { + const rule = await SyncRule.get(api, id); + const { name, builder, id: syncRule, type = "metadata" } = rule; + + onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); + + const sync = compositionRoot.sync[type]({ ...builder, syncRule }); + + for await (const { message, syncReport, done } of sync.execute()) { + if (message) onProgressChange(message); + if (syncReport) await compositionRoot.reports.save(syncReport); + + if (done && syncReport) { + onProgressChange(i18n.t(`Finished Sync Rule {{name}}`, { name })); + } else if (done) { + onProgressChange(i18n.t(`Finished Sync Rule {{name}} with errors`, { name })); + } + } +}; From 999f44d93c0b9f78ed0b1c1deccaab993204d65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 7 Dec 2020 08:32:07 +0100 Subject: [PATCH 062/163] Add use sync rules periods checkbox --- i18n/en.pot | 7 +- i18n/es.po | 5 +- i18n/fr.po | 5 +- i18n/pt.po | 5 +- .../PeriodSelectionDialog.tsx | 74 +++++++++++++------ 5 files changed, 70 insertions(+), 26 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 86a7ad6e8..22d0223c5 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-12-04T12:19:04.582Z\n" -"PO-Revision-Date: 2020-12-04T12:19:04.582Z\n" +"POT-Creation-Date: 2020-12-07T07:29:23.728Z\n" +"PO-Revision-Date: 2020-12-07T07:29:23.728Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -761,6 +761,9 @@ msgstr "" msgid "End date" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "You need to provide a subject" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index aa1923ae3..222f931d8 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-12-04T12:19:04.582Z\n" +"POT-Creation-Date: 2020-12-07T07:29:23.728Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -765,6 +765,9 @@ msgstr "" msgid "End date" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "You need to provide a subject" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index b62522ab0..2479b2f5d 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-12-04T12:19:04.582Z\n" +"POT-Creation-Date: 2020-12-07T07:29:23.728Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -762,6 +762,9 @@ msgstr "" msgid "End date" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "You need to provide a subject" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index b62522ab0..2479b2f5d 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-12-04T12:19:04.582Z\n" +"POT-Creation-Date: 2020-12-07T07:29:23.728Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -762,6 +762,9 @@ msgstr "" msgid "End date" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "You need to provide a subject" msgstr "" diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx index 353521404..3313d653c 100644 --- a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx @@ -1,4 +1,4 @@ -import { Box, makeStyles, Theme } from "@material-ui/core"; +import { Box, Checkbox, FormControlLabel, makeStyles, Theme } from "@material-ui/core"; import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; import React, { useState } from "react"; import { Period } from "../../../../../domain/common/entities/Period"; @@ -7,9 +7,9 @@ import PeriodSelection, { ObjectWithPeriod } from "../period-selection/PeriodSel export interface PeriodSelectionDialogProps { title?: string; - period: Period; + period?: Period; onClose(): void; - onSave(period: Period): void; + onSave(period?: Period): void; } export const PeriodSelectionDialog: React.FC = ({ @@ -20,23 +20,39 @@ export const PeriodSelectionDialog: React.FC = ({ }) => { const classes = useStyles(); const snackbar = useSnackbar(); - const [objectWithPeriod, setObjectWithPeriod] = useState({ - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }); + const [objectWithPeriod, setObjectWithPeriod] = useState( + period + ? { + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + } + : undefined + ); + + const handleCheckBoxChange = (event: React.ChangeEvent) => { + if (event.target.checked) { + setObjectWithPeriod(undefined); + } else { + setObjectWithPeriod({ period: "ALL" }); + } + }; const handleSave = () => { - const periodValidation = Period.create({ - type: objectWithPeriod.period, - startDate: objectWithPeriod.startDate, - endDate: objectWithPeriod.endDate, - }); + if (objectWithPeriod) { + const periodValidation = Period.create({ + type: objectWithPeriod.period, + startDate: objectWithPeriod.startDate, + endDate: objectWithPeriod.endDate, + }); - periodValidation.match({ - error: errors => snackbar.error(errors.map(error => error.description).join("\n")), - success: period => onSave(period), - }); + periodValidation.match({ + error: errors => snackbar.error(errors.map(error => error.description).join("\n")), + success: period => onSave(period), + }); + } else { + onSave(undefined); + } }; return ( @@ -51,11 +67,24 @@ export const PeriodSelectionDialog: React.FC = ({ saveText={i18n.t("Save")} > - + } + label={i18n.t("Use sync rules periods")} /> + + {objectWithPeriod && ( + + )} ); @@ -68,4 +97,7 @@ const useStyles = makeStyles((theme: Theme) => ({ periodContent: { margin: theme.spacing(2), }, + check: { + marginLeft: theme.spacing(3), + }, })); From 6ed144db5612da7611370bf34dca42989b84ee5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 7 Dec 2020 08:32:51 +0100 Subject: [PATCH 063/163] Merge selected period in sync rules before to execute --- .../msf-aggregate-data/pages/MSFHomePage.tsx | 13 ++++--- .../pages/MSFHomePagePresenter.ts | 35 ++++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index febd26e3b..dea6787c5 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -21,7 +21,7 @@ export const MSFHomePage: React.FC = () => { const [syncProgress, setSyncProgress] = useState([]); const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); - const [period, setPeriod] = useState(Period.createDefault()); + const [period, setPeriod] = useState(); const [msfSettings, setMsfSettings] = useState({ runAnalytics: "by-sync-rule-settings", @@ -33,10 +33,13 @@ export const MSFHomePage: React.FC = () => { }, [api]); const handleAggregateData = () => { - executeAggregateData(api, compositionRoot, progress => { - console.log({ syncProcess: syncProgress }); - setSyncProgress(progress); - }); + executeAggregateData( + api, + compositionRoot, + msfSettings, + progress => setSyncProgress(progress), + period + ); }; const handleAdvancedSettings = () => { diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index dda7d04a2..c5f8e847e 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -2,12 +2,16 @@ import SyncRule from "../../../../models/syncRule"; import { CompositionRoot } from "../../../CompositionRoot"; import { D2Api } from "../../../../types/d2-api"; import i18n from "../../../../locales"; +import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +import { Period } from "../../../../domain/common/entities/Period"; //TODO: maybe convert to class and presenter to use MVP, MVI pattern export async function executeAggregateData( api: D2Api, compositionRoot: CompositionRoot, - onProgressChange: (progress: string[]) => void + msfSettings: MSFSettings, + onProgressChange: (progress: string[]) => void, + period?: Period ) { const eventSyncRules = ( await SyncRule.list(api, { type: "events" }, { paging: false }) @@ -23,7 +27,14 @@ export async function executeAggregateData( }; for (const syncRule of eventSyncRules) { - await executeSyncRule(api, compositionRoot, syncRule.id, onSyncRuleProgressChange); + await executeSyncRule( + api, + compositionRoot, + msfSettings, + syncRule.id, + onSyncRuleProgressChange, + period + ); } onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); @@ -32,15 +43,31 @@ export async function executeAggregateData( const executeSyncRule = async ( api: D2Api, compositionRoot: CompositionRoot, + _msfSettings: MSFSettings, id: string, - onProgressChange: (event: string) => void + onProgressChange: (event: string) => void, + period?: Period ): Promise => { const rule = await SyncRule.get(api, id); const { name, builder, id: syncRule, type = "metadata" } = rule; + const newBuilder = period + ? { + ...builder, + dataParams: { + ...builder.dataParams, + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }, + } + : builder; + + console.log({ newBuilder }); + onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); - const sync = compositionRoot.sync[type]({ ...builder, syncRule }); + const sync = compositionRoot.sync[type]({ ...newBuilder, syncRule }); for await (const { message, syncReport, done } of sync.execute()) { if (message) onProgressChange(message); From ed336e311bb439010440ee5dcd1949530ef07719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 7 Dec 2020 08:38:54 +0100 Subject: [PATCH 064/163] Center MSFSettingsDialog content --- i18n/en.pot | 4 ++-- .../msf-Settings/MSFSettingsDialog.tsx | 24 +++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 22d0223c5..e84ec1e8c 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-12-07T07:29:23.728Z\n" -"PO-Revision-Date: 2020-12-07T07:29:23.728Z\n" +"POT-Creation-Date: 2020-12-07T07:33:10.638Z\n" +"PO-Revision-Date: 2020-12-07T07:33:10.638Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx index 5b9dae30d..c9dde429b 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -1,3 +1,4 @@ +import { Box, makeStyles } from "@material-ui/core"; import { ConfirmationDialog } from "d2-ui-components"; import React, { useMemo, useState } from "react"; import i18n from "../../../../../locales"; @@ -21,6 +22,7 @@ export const MSFSettingsDialog: React.FC = ({ msfSettings, }) => { const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); + const classes = useStyles(); const useSyncRuleItems = useMemo(() => { return [ @@ -63,13 +65,21 @@ export const MSFSettingsDialog: React.FC = ({ cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > - + + + ); }; + +const useStyles = makeStyles(() => ({ + root: { + margin: "0 auto", + }, +})); From 1cfca7dd1df1dc43b73fa0042b7513d0e36c699f Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 7 Dec 2020 09:22:57 +0100 Subject: [PATCH 065/163] Fix timestamp --- .../reports/entities/SynchronizationReport.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/domain/reports/entities/SynchronizationReport.ts b/src/domain/reports/entities/SynchronizationReport.ts index 3559d6d7a..3b6c909d8 100644 --- a/src/domain/reports/entities/SynchronizationReport.ts +++ b/src/domain/reports/entities/SynchronizationReport.ts @@ -10,7 +10,7 @@ export class SynchronizationReport implements SynchronizationReportData { public types: string[]; public readonly id: string; - public readonly date?: Date | undefined; + public readonly date: Date; public readonly user: string; public readonly syncRule?: string | undefined; public readonly packageImport?: boolean | undefined; @@ -18,10 +18,10 @@ export class SynchronizationReport implements SynchronizationReportData { public readonly type: SynchronizationType; public readonly dataStats?: AggregatedDataStats[] | EventsDataStats[] | undefined; - private constructor(syncReport: SynchronizationReportData) { + private constructor(syncReport: PartialBy) { this.results = null; - this.id = syncReport.id; - this.date = syncReport.date; + this.id = syncReport.id ?? generateUid(); + this.date = syncReport.date ?? new Date(); this.user = syncReport.user; this.status = syncReport.status; this.types = syncReport.types; @@ -38,7 +38,6 @@ export class SynchronizationReport implements SynchronizationReportData { packageImport?: boolean ): SynchronizationReport { return new SynchronizationReport({ - id: generateUid(), user, status: "READY" as SynchronizationReportStatus, types: [], @@ -48,11 +47,9 @@ export class SynchronizationReport implements SynchronizationReportData { } public static build( - syncReport?: PartialBy + data?: PartialBy ): SynchronizationReport { - return syncReport - ? new SynchronizationReport({ id: generateUid(), ...syncReport }) - : this.create(); + return data ? new SynchronizationReport(data) : this.create(); } public setStatus(status: SynchronizationReportStatus): void { From 0cabbdfaa0addcc481bff7d3653039dbbe7e8246 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 7 Dec 2020 09:28:45 +0100 Subject: [PATCH 066/163] Fix sync summary not opening up --- src/data/reports/ReportsD2ApiRepository.ts | 5 +++++ src/domain/reports/entities/SynchronizationReport.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/data/reports/ReportsD2ApiRepository.ts b/src/data/reports/ReportsD2ApiRepository.ts index e1c54c843..557c8cd2f 100644 --- a/src/data/reports/ReportsD2ApiRepository.ts +++ b/src/data/reports/ReportsD2ApiRepository.ts @@ -46,6 +46,11 @@ export class ReportsD2ApiRepository implements ReportsRepository { Namespace.HISTORY, report.toObject() ); + + await this.storageClient.saveObject( + `${Namespace.HISTORY}-${report.id}`, + report.getResults() + ); } public async delete(id: string): Promise { diff --git a/src/domain/reports/entities/SynchronizationReport.ts b/src/domain/reports/entities/SynchronizationReport.ts index 3b6c909d8..f9b80c95f 100644 --- a/src/domain/reports/entities/SynchronizationReport.ts +++ b/src/domain/reports/entities/SynchronizationReport.ts @@ -86,6 +86,10 @@ export class SynchronizationReport implements SynchronizationReportData { dataStats: this.dataStats, }; } + + public getResults(): SynchronizationResult[] { + return this.results ?? []; + } } export interface SynchronizationReportData { From 2812522be8f8749d51c5118b541789ccc7b8532c Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 7 Dec 2020 10:02:15 +0100 Subject: [PATCH 067/163] Fix target instances filter --- src/domain/rules/entities/SynchronizationRule.ts | 7 +++++-- src/domain/rules/usecases/ListSyncRuleUseCase.ts | 7 ------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/domain/rules/entities/SynchronizationRule.ts b/src/domain/rules/entities/SynchronizationRule.ts index 1a6c85a17..a1987fed6 100644 --- a/src/domain/rules/entities/SynchronizationRule.ts +++ b/src/domain/rules/entities/SynchronizationRule.ts @@ -40,6 +40,7 @@ export class SynchronizationRule { "created", "description", "builder", + "targetInstances", "enabled", "frequency", "lastExecuted", @@ -158,7 +159,7 @@ export class SynchronizationRule { } public get targetInstances(): string[] { - return this.syncRule.builder?.targetInstances ?? []; + return this.syncRule.targetInstances; } public get enabled(): boolean { @@ -223,6 +224,7 @@ export class SynchronizationRule { description: "", type: type, builder: defaultSynchronizationBuilder, + targetInstances: [], enabled: false, lastUpdated: new Date(), lastUpdatedBy: { @@ -501,7 +503,7 @@ export class SynchronizationRule { } public updateTargetInstances(targetInstances: string[]): SynchronizationRule { - return this.updateBuilder({ targetInstances }); + return this.update({ targetInstances }).updateBuilder({ targetInstances }); } public updateSyncParams(syncParams: MetadataSynchronizationParams): SynchronizationRule { @@ -678,6 +680,7 @@ export interface SynchronizationRuleData extends SharedRef { created: Date; description?: string; builder: SynchronizationBuilder; + targetInstances: string[]; enabled: boolean; lastExecuted?: Date; frequency?: string; diff --git a/src/domain/rules/usecases/ListSyncRuleUseCase.ts b/src/domain/rules/usecases/ListSyncRuleUseCase.ts index 2c22fc643..e65bd1eec 100644 --- a/src/domain/rules/usecases/ListSyncRuleUseCase.ts +++ b/src/domain/rules/usecases/ListSyncRuleUseCase.ts @@ -5,7 +5,6 @@ import { getUserInfo, isGlobalAdmin } from "../../../utils/permissions"; import { UseCase } from "../../common/entities/UseCase"; import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { defaultSynchronizationBuilder } from "../../synchronization/entities/SynchronizationBuilder"; import { SynchronizationType } from "../../synchronization/entities/SynchronizationType"; import { SynchronizationRule } from "../entities/SynchronizationRule"; @@ -70,12 +69,6 @@ export class ListSyncRuleUseCase implements UseCase { const userInfo = await getUserInfo(getD2APiFromInstance(this.localInstance)); const filteredObjects = _(sortedData) - .map(rule => - rule.updateBuilder({ - ...defaultSynchronizationBuilder, - targetInstances: rule.targetInstances, - }) - ) .filter(rule => { return _.isNull(type) || rule.type === type; }) From 887fb3b032cf206e89064456f3fce9384f7671d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 7 Dec 2020 10:32:53 +0100 Subject: [PATCH 068/163] Align content in dialogs to left --- i18n/en.pot | 4 +- .../PeriodSelectionDialog.tsx | 49 ++++++------------- .../msf-Settings/MSFSettingsDialog.tsx | 24 +++------ 3 files changed, 25 insertions(+), 52 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index e84ec1e8c..ac57a82ff 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-12-07T07:33:10.638Z\n" -"PO-Revision-Date: 2020-12-07T07:33:10.638Z\n" +"POT-Creation-Date: 2020-12-07T09:08:07.926Z\n" +"PO-Revision-Date: 2020-12-07T09:08:07.926Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx index 3313d653c..1b4de8e1b 100644 --- a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ b/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx @@ -1,4 +1,4 @@ -import { Box, Checkbox, FormControlLabel, makeStyles, Theme } from "@material-ui/core"; +import { Checkbox, FormControlLabel } from "@material-ui/core"; import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; import React, { useState } from "react"; import { Period } from "../../../../../domain/common/entities/Period"; @@ -18,7 +18,6 @@ export const PeriodSelectionDialog: React.FC = ({ onSave, period, }) => { - const classes = useStyles(); const snackbar = useSnackbar(); const [objectWithPeriod, setObjectWithPeriod] = useState( period @@ -66,38 +65,22 @@ export const PeriodSelectionDialog: React.FC = ({ cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > - - - } - label={i18n.t("Use sync rules periods")} - /> - - {objectWithPeriod && ( - - )} - + } + label={i18n.t("Use sync rules periods")} + /> + + {objectWithPeriod && ( + + )} ); }; - -const useStyles = makeStyles((theme: Theme) => ({ - periodContainer: { - margin: "0 auto", - }, - periodContent: { - margin: theme.spacing(2), - }, - check: { - marginLeft: theme.spacing(3), - }, -})); diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx index c9dde429b..5b9dae30d 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -1,4 +1,3 @@ -import { Box, makeStyles } from "@material-ui/core"; import { ConfirmationDialog } from "d2-ui-components"; import React, { useMemo, useState } from "react"; import i18n from "../../../../../locales"; @@ -22,7 +21,6 @@ export const MSFSettingsDialog: React.FC = ({ msfSettings, }) => { const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); - const classes = useStyles(); const useSyncRuleItems = useMemo(() => { return [ @@ -65,21 +63,13 @@ export const MSFSettingsDialog: React.FC = ({ cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > - - - + ); }; - -const useStyles = makeStyles(() => ({ - root: { - margin: "0 auto", - }, -})); From 8733347247c18f34818d65a709cf565215fd3365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 7 Dec 2020 11:51:25 +0100 Subject: [PATCH 069/163] Add data param runAnalytics in builder --- i18n/en.pot | 4 ++-- .../SyncParamsSelector.tsx | 19 +++++++++++++++++++ src/types/d2.ts | 1 + 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ac57a82ff..1627892fb 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-12-07T09:08:07.926Z\n" -"PO-Revision-Date: 2020-12-07T09:08:07.926Z\n" +"POT-Creation-Date: 2020-12-07T09:44:53.049Z\n" +"PO-Revision-Date: 2020-12-07T09:44:53.049Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx index 4e98471bb..19090fb3e 100644 --- a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx +++ b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx @@ -102,6 +102,15 @@ const SyncParamsSelector: React.FC = ({ } }; + const changeRunAnalytics = (runAnalytics: boolean) => { + onChange( + syncRule.updateDataParams({ + ...dataParams, + runAnalytics, + }) + ); + }; + return ( @@ -194,6 +203,16 @@ const SyncParamsSelector: React.FC = ({ } />
  • + + {(syncRule.type === "events" || syncRule.type === "aggregated") && ( +
    + +
    + )}
    ); }; diff --git a/src/types/d2.ts b/src/types/d2.ts index 59118498b..d67a56cf3 100644 --- a/src/types/d2.ts +++ b/src/types/d2.ts @@ -64,4 +64,5 @@ export interface DataImportParams { skipExistingCheck?: boolean; strategy?: "NEW_AND_UPDATES" | "NEW" | "UPDATES" | "DELETES"; format?: "json" | "xml" | "csv" | "pdf" | "adx"; + runAnalytics?: boolean; } From e9f46161e8f28c3682d914bf952b9f77453a787d Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 7 Dec 2020 12:32:00 +0100 Subject: [PATCH 070/163] Add dummy settings button --- i18n/en.pot | 10 ++++++++-- i18n/es.po | 8 +++++++- i18n/fr.po | 8 +++++++- i18n/pt.po | 8 +++++++- .../webapp/core/pages/home/HomePage.tsx | 17 +++++++++++++++-- 5 files changed, 44 insertions(+), 7 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 1627892fb..62b89fcc4 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-12-07T09:44:53.049Z\n" -"PO-Revision-Date: 2020-12-07T09:44:53.049Z\n" +"POT-Creation-Date: 2020-12-07T11:31:58.789Z\n" +"PO-Revision-Date: 2020-12-07T11:31:58.789Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -868,6 +868,9 @@ msgstr "" msgid "Dry Run" msgstr "" +msgid "Run Analytics before sync" +msgstr "" + msgid "Type" msgstr "" @@ -1387,6 +1390,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Settings" +msgstr "" + msgid "Admin Dashboard" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 222f931d8..4a0b96f51 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-12-07T07:29:23.728Z\n" +"POT-Creation-Date: 2020-12-07T11:31:58.789Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -873,6 +873,9 @@ msgstr "" msgid "Dry Run" msgstr "" +msgid "Run Analytics before sync" +msgstr "" + msgid "Type" msgstr "" @@ -1394,6 +1397,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Settings" +msgstr "" + msgid "Admin Dashboard" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 2479b2f5d..f4001a068 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-12-07T07:29:23.728Z\n" +"POT-Creation-Date: 2020-12-07T11:31:58.789Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -870,6 +870,9 @@ msgstr "" msgid "Dry Run" msgstr "" +msgid "Run Analytics before sync" +msgstr "" + msgid "Type" msgstr "" @@ -1390,6 +1393,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Settings" +msgstr "" + msgid "Admin Dashboard" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 2479b2f5d..f4001a068 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-12-07T07:29:23.728Z\n" +"POT-Creation-Date: 2020-12-07T11:31:58.789Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -870,6 +870,9 @@ msgstr "" msgid "Dry Run" msgstr "" +msgid "Run Analytics before sync" +msgstr "" + msgid "Type" msgstr "" @@ -1390,6 +1393,9 @@ msgid "" "metadata requests coming from other DHIS instances." msgstr "" +msgid "Settings" +msgstr "" + msgid "Admin Dashboard" msgstr "" diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index da41506e2..21e744532 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -1,4 +1,4 @@ -import { Badge, Icon } from "@material-ui/core"; +import { Badge, Icon, IconButton, makeStyles, Tooltip } from "@material-ui/core"; import _ from "lodash"; import React, { useEffect, useMemo, useState } from "react"; import { useHistory } from "react-router-dom"; @@ -8,9 +8,9 @@ import { isAppExecutor, shouldShowDeletedObjects, } from "../../../../../utils/permissions"; -import { useAppContext } from "../../../../react/core/contexts/AppContext"; import { Card, Landing } from "../../../../react/core/components/landing/Landing"; import { TestWrapper } from "../../../../react/core/components/test-wrapper/TestWrapper"; +import { useAppContext } from "../../../../react/core/contexts/AppContext"; import { AppVariant } from "../../../Root"; const appVariantConfiguration: Record = { @@ -43,6 +43,7 @@ const LandingPage: React.FC = ({ type }) => { const { api, compositionRoot } = useAppContext(); const history = useHistory(); + const classes = useStyles(); const [showDeletedObjects, setShowDeletedObjects] = useState(false); const [appConfigurator, setAppConfigurator] = useState(false); @@ -244,6 +245,12 @@ const LandingPage: React.FC = ({ type }) => { return ( + + + settings + + + = ({ type }) => { ); }; +const useStyles = makeStyles({ + settingsButton: { + float: "right", + }, +}); + export default LandingPage; From 5d134645e146b5d9a7eb55b0c8b03e0e3fbe9a07 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 7 Dec 2020 13:26:04 +0100 Subject: [PATCH 071/163] Update route paths --- src/presentation/webapp/Root.tsx | 83 +++++++++++++------ .../webapp/core/pages/history/HistoryPage.tsx | 2 +- .../webapp/core/pages/home/HomePage.tsx | 2 +- .../pages/instance-list/InstanceListPage.tsx | 2 +- .../core/pages/manual-sync/ManualSyncPage.tsx | 2 +- .../ModulePackageListPage.tsx | 2 +- .../NotificationsListPage.tsx | 2 +- .../ResponsiblesListPage.tsx | 2 +- .../store-creation/StoreCreationPage.tsx | 2 +- .../core/pages/store-list/StoreListPage.tsx | 2 +- .../sync-rules-list/SyncRulesListPage.tsx | 2 +- 11 files changed, 68 insertions(+), 35 deletions(-) diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx index ef0b3a233..a6e228ede 100644 --- a/src/presentation/webapp/Root.tsx +++ b/src/presentation/webapp/Root.tsx @@ -1,9 +1,13 @@ import React from "react"; -import { HashRouter, Switch } from "react-router-dom"; +import { HashRouter, Redirect, Switch } from "react-router-dom"; +import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; +import * as permissions from "../../utils/permissions"; import RouteWithSession from "../react/core/components/auth/RouteWithSession"; import RouteWithSessionAndAuth from "../react/core/components/auth/RouteWithSessionAndAuth"; -import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; +import { useAppContext } from "../react/core/contexts/AppContext"; import HistoryPage from "./core/pages/history/HistoryPage"; +import HomePage from "./core/pages/home/HomePage"; +import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; import InstanceListPage from "./core/pages/instance-list/InstanceListPage"; import InstanceMappingLandingPage from "./core/pages/instance-mapping/InstanceMappingLandingPage"; import InstanceMappingPage from "./core/pages/instance-mapping/InstanceMappingPage"; @@ -18,21 +22,11 @@ import SyncRulesCreationPage, { SyncRulesCreationParams, } from "./core/pages/sync-rules-creation/SyncRulesCreationPage"; import SyncRulesPage from "./core/pages/sync-rules-list/SyncRulesListPage"; -import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; -import { useAppContext } from "../react/core/contexts/AppContext"; -import * as permissions from "../../utils/permissions"; -import HomePage from "./core/pages/home/HomePage"; import { MSFHomePage } from "./msf-aggregate-data/pages/MSFHomePage"; -export type AppVariant = - | "core-app" - | "data-metadata-app" - | "module-package-app" - | "msf-aggregate-data-app"; - const Root: React.FC = () => { - const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; const { api } = useAppContext(); + const appVariant = getAppVariant(); return ( @@ -107,21 +101,60 @@ const Root: React.FC = () => { render={() => } /> - ( - - )} - /> - - {appVariant === "msf-aggregate-data-app" && ( - } /> - )} + ); }; +const VariantRoutes: React.FC<{ variant: AppVariant }> = ({ variant }) => { + switch (variant) { + case "msf-aggregate-data-app": + return ( + + } /> + + } + /> + + + + ); + default: + return ( + + } + /> + + + + ); + } +}; + +const getAppVariant = (): AppVariant => { + const variant = process.env.REACT_APP_PRESENTATION_VARIANT; + + return isAppVariant(variant) ? variant : "core-app"; +}; + +const isAppVariant = (variant?: string): variant is AppVariant => { + return ( + !!variant && + ["core-app", "data-metadata-app", "module-package-app", "msf-aggregate-data-app"].includes( + variant + ) + ); +}; + +export type AppVariant = + | "core-app" + | "data-metadata-app" + | "module-package-app" + | "msf-aggregate-data-app"; + export default Root; diff --git a/src/presentation/webapp/core/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx index d1f8088f7..f80406eac 100644 --- a/src/presentation/webapp/core/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -91,7 +91,7 @@ const HistoryPage: React.FC = () => { const [statusFilter, updateStatusFilter] = useState(""); const [syncRuleFilter, updateSyncRuleFilter] = useState(""); - const goBack = () => history.goBack(); + const goBack = () => history.push("/"); const updateTable = useCallback( (tableState?: TableState) => { diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index 21e744532..074cc5ae6 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -61,7 +61,7 @@ const LandingPage: React.FC = ({ type }) => { }, [api, compositionRoot]); const backHome = () => { - history.goBack(); + history.push("/"); }; const allCards: Card[] = useMemo( diff --git a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx index 1f5c248a4..dd90e5349 100644 --- a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx @@ -141,7 +141,7 @@ const InstanceListPage = () => { }; const backHome = () => { - history.push("/"); + history.push("/dashboard"); }; const updateTable = (state: TableState) => { diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 0794f66ce..106965042 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -117,7 +117,7 @@ const ManualSyncPage: React.FC = () => { isAppConfigurator(api).then(updateAppConfigurator); }, [api, updateAppConfigurator]); - const goBack = () => history.goBack(); + const goBack = () => history.push("/dashboard"); const updateSelection = useCallback( (selection: string[], exclusion: string[]) => { diff --git a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index f7c67c34a..8b870f3eb 100644 --- a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -51,7 +51,7 @@ export const ModulePackageListPage: React.FC = () => { }, [compositionRoot]); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const create = useCallback(() => { diff --git a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index ac986a9da..bcec65608 100644 --- a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -48,7 +48,7 @@ export const NotificationsListPage: React.FC = () => { const [syncReport, setSyncReport] = useState(); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const changeUnreadCheckbox = useCallback((event: React.ChangeEvent) => { diff --git a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx index 52420fe89..c91d0256b 100644 --- a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx +++ b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx @@ -23,7 +23,7 @@ export const ResponsiblesListPage: React.FC = () => { const [appConfigurator, updateAppConfigurator] = useState(false); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const updateRemoteInstance = useCallback( diff --git a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx index fe6f0a6fc..b1b19c08c 100644 --- a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx @@ -56,7 +56,7 @@ const StoreCreationPage: React.FC = () => { }; const close = useCallback(() => { - history.goBack(); + history.push("/dashboard"); }, [history]); const validateError = useCallback((error?: GitHubError): string => { diff --git a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx index df39fbc08..9ba6e8460 100644 --- a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx @@ -37,7 +37,7 @@ export const StoreListPage: React.FC = () => { getStores().then(setRows); }, [getStores, objectsTableKey]); - const backHome = () => history.push("/"); + const backHome = () => history.push("/dashboard"); const handleCreateStore = () => history.push(`/stores/new`); diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index 724b9a6e3..1975368f7 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -199,7 +199,7 @@ const SyncRulesPage: React.FC = () => { }; const back = () => { - history.goBack(); + history.push("/dashboard"); }; const confirmDelete = async () => { From 4ad777089876483abae28857b41ae7fcf7508d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 06:58:31 +0100 Subject: [PATCH 072/163] Add run analytics in instance selection step: - Aggregated and events --- i18n/en.pot | 10 +++++----- i18n/es.po | 8 ++++---- i18n/fr.po | 8 ++++---- i18n/pt.po | 8 ++++---- .../synchronization/usecases/GenericSyncUseCase.ts | 13 ++++++++++++- .../pages/MSFHomePagePresenter.ts | 2 +- 6 files changed, 30 insertions(+), 19 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 96bb0c53c..b1c25421c 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-12-07T10:59:14.950Z\n" -"PO-Revision-Date: 2020-12-07T10:59:14.950Z\n" +"POT-Creation-Date: 2020-12-08T05:56:58.516Z\n" +"PO-Revision-Date: 2020-12-08T05:56:58.516Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -98,6 +98,9 @@ msgstr "" msgid "deleted" msgstr "" +msgid "Analytics execution finished on {{name}}" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1435,9 +1438,6 @@ msgstr "" msgid "Saving..." msgstr "" -msgid "Analytics execution finished on {{name}}" -msgstr "" - msgid "Deleting Instances" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 4209d0364..03a538037 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-12-07T10:59:14.950Z\n" +"POT-Creation-Date: 2020-12-07T12:11:28.395Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -98,6 +98,9 @@ msgstr "" msgid "deleted" msgstr "" +msgid "Analytics execution finished on {{name}}" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1442,9 +1445,6 @@ msgstr "" msgid "Saving..." msgstr "" -msgid "Analytics execution finished on {{name}}" -msgstr "" - msgid "Deleting Instances" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 4cb06bdd0..6a4d439fc 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-12-07T10:59:14.950Z\n" +"POT-Creation-Date: 2020-12-07T12:11:28.395Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -98,6 +98,9 @@ msgstr "" msgid "deleted" msgstr "" +msgid "Analytics execution finished on {{name}}" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1438,9 +1441,6 @@ msgstr "" msgid "Saving..." msgstr "" -msgid "Analytics execution finished on {{name}}" -msgstr "" - msgid "Deleting Instances" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 4cb06bdd0..6a4d439fc 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-12-07T10:59:14.950Z\n" +"POT-Creation-Date: 2020-12-07T12:11:28.395Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -98,6 +98,9 @@ msgstr "" msgid "deleted" msgstr "" +msgid "Analytics execution finished on {{name}}" +msgstr "" + msgid "Preparing synchronization" msgstr "" @@ -1438,9 +1441,6 @@ msgstr "" msgid "Saving..." msgstr "" -msgid "Analytics execution finished on {{name}}" -msgstr "" - msgid "Deleting Instances" msgstr "" diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 7a99ed778..b17d82636 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -29,6 +29,7 @@ import { SynchronizationStatus, } from "../../reports/entities/SynchronizationResult"; import { SynchronizationType } from "../entities/SynchronizationType"; +import { executeAnalytics } from "../../../utils/analytics"; export type SyncronizationClass = | typeof MetadataSyncUseCase @@ -186,8 +187,18 @@ export abstract class GenericSyncUseCase { } public async *execute() { - const { targetInstances: targetInstanceIds, syncRule } = this.builder; + const { targetInstances: targetInstanceIds, syncRule, dataParams } = this.builder; + const origin = await this.getOriginInstance(); + + if (dataParams && dataParams.runAnalytics) { + for await (const message of executeAnalytics(origin)) { + yield { message }; + } + + yield { message: i18n.t("Analytics execution finished on {{name}}", origin) }; + } + yield { message: i18n.t("Preparing synchronization") }; // Build instance list diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index c7361e7df..720414b12 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -11,7 +11,7 @@ export async function executeAggregateData( period?: Period ) { const eventSyncRules = ( - await compositionRoot.rules.list({ filters: { type: "events" }, paging: false }) + await compositionRoot.rules.list({ filters: { type: "events" }, paging: false }) ).rows.slice(0, 2); let syncProgress: string[] = [i18n.t(`Starting Aggregate Data...`)]; From fc2477fc965c52e9c90d23a7080fed095431123a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 07:21:11 +0100 Subject: [PATCH 073/163] Update route paths --- i18n/en.pot | 4 +- src/presentation/webapp/Root.tsx | 83 +++++++++++++------ .../webapp/core/pages/history/HistoryPage.tsx | 2 +- .../webapp/core/pages/home/HomePage.tsx | 2 +- .../pages/instance-list/InstanceListPage.tsx | 2 +- .../core/pages/manual-sync/ManualSyncPage.tsx | 2 +- .../ModulePackageListPage.tsx | 2 +- .../NotificationsListPage.tsx | 2 +- .../ResponsiblesListPage.tsx | 2 +- .../store-creation/StoreCreationPage.tsx | 2 +- .../core/pages/store-list/StoreListPage.tsx | 2 +- .../sync-rules-list/SyncRulesListPage.tsx | 2 +- 12 files changed, 70 insertions(+), 37 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index ac57a82ff..1314c30d4 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-12-07T09:08:07.926Z\n" -"PO-Revision-Date: 2020-12-07T09:08:07.926Z\n" +"POT-Creation-Date: 2020-12-08T06:20:05.518Z\n" +"PO-Revision-Date: 2020-12-08T06:20:05.518Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx index ef0b3a233..a6e228ede 100644 --- a/src/presentation/webapp/Root.tsx +++ b/src/presentation/webapp/Root.tsx @@ -1,9 +1,13 @@ import React from "react"; -import { HashRouter, Switch } from "react-router-dom"; +import { HashRouter, Redirect, Switch } from "react-router-dom"; +import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; +import * as permissions from "../../utils/permissions"; import RouteWithSession from "../react/core/components/auth/RouteWithSession"; import RouteWithSessionAndAuth from "../react/core/components/auth/RouteWithSessionAndAuth"; -import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; +import { useAppContext } from "../react/core/contexts/AppContext"; import HistoryPage from "./core/pages/history/HistoryPage"; +import HomePage from "./core/pages/home/HomePage"; +import InstanceCreationPage from "./core/pages/instance-creation/InstanceCreationPage"; import InstanceListPage from "./core/pages/instance-list/InstanceListPage"; import InstanceMappingLandingPage from "./core/pages/instance-mapping/InstanceMappingLandingPage"; import InstanceMappingPage from "./core/pages/instance-mapping/InstanceMappingPage"; @@ -18,21 +22,11 @@ import SyncRulesCreationPage, { SyncRulesCreationParams, } from "./core/pages/sync-rules-creation/SyncRulesCreationPage"; import SyncRulesPage from "./core/pages/sync-rules-list/SyncRulesListPage"; -import { SynchronizationType } from "../../domain/synchronization/entities/SynchronizationType"; -import { useAppContext } from "../react/core/contexts/AppContext"; -import * as permissions from "../../utils/permissions"; -import HomePage from "./core/pages/home/HomePage"; import { MSFHomePage } from "./msf-aggregate-data/pages/MSFHomePage"; -export type AppVariant = - | "core-app" - | "data-metadata-app" - | "module-package-app" - | "msf-aggregate-data-app"; - const Root: React.FC = () => { - const appVariant = process.env.REACT_APP_PRESENTATION_VARIANT as AppVariant; const { api } = useAppContext(); + const appVariant = getAppVariant(); return ( @@ -107,21 +101,60 @@ const Root: React.FC = () => { render={() => } /> - ( - - )} - /> - - {appVariant === "msf-aggregate-data-app" && ( - } /> - )} + ); }; +const VariantRoutes: React.FC<{ variant: AppVariant }> = ({ variant }) => { + switch (variant) { + case "msf-aggregate-data-app": + return ( + + } /> + + } + /> + + + + ); + default: + return ( + + } + /> + + + + ); + } +}; + +const getAppVariant = (): AppVariant => { + const variant = process.env.REACT_APP_PRESENTATION_VARIANT; + + return isAppVariant(variant) ? variant : "core-app"; +}; + +const isAppVariant = (variant?: string): variant is AppVariant => { + return ( + !!variant && + ["core-app", "data-metadata-app", "module-package-app", "msf-aggregate-data-app"].includes( + variant + ) + ); +}; + +export type AppVariant = + | "core-app" + | "data-metadata-app" + | "module-package-app" + | "msf-aggregate-data-app"; + export default Root; diff --git a/src/presentation/webapp/core/pages/history/HistoryPage.tsx b/src/presentation/webapp/core/pages/history/HistoryPage.tsx index d1f8088f7..bc5bf3413 100644 --- a/src/presentation/webapp/core/pages/history/HistoryPage.tsx +++ b/src/presentation/webapp/core/pages/history/HistoryPage.tsx @@ -91,7 +91,7 @@ const HistoryPage: React.FC = () => { const [statusFilter, updateStatusFilter] = useState(""); const [syncRuleFilter, updateSyncRuleFilter] = useState(""); - const goBack = () => history.goBack(); + const goBack = () => history.push("/dashboard"); const updateTable = useCallback( (tableState?: TableState) => { diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index da41506e2..1c46a09c6 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -60,7 +60,7 @@ const LandingPage: React.FC = ({ type }) => { }, [api, compositionRoot]); const backHome = () => { - history.goBack(); + history.push("/"); }; const allCards: Card[] = useMemo( diff --git a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx index 1f5c248a4..dd90e5349 100644 --- a/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx +++ b/src/presentation/webapp/core/pages/instance-list/InstanceListPage.tsx @@ -141,7 +141,7 @@ const InstanceListPage = () => { }; const backHome = () => { - history.push("/"); + history.push("/dashboard"); }; const updateTable = (state: TableState) => { diff --git a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx index 0794f66ce..106965042 100644 --- a/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx +++ b/src/presentation/webapp/core/pages/manual-sync/ManualSyncPage.tsx @@ -117,7 +117,7 @@ const ManualSyncPage: React.FC = () => { isAppConfigurator(api).then(updateAppConfigurator); }, [api, updateAppConfigurator]); - const goBack = () => history.goBack(); + const goBack = () => history.push("/dashboard"); const updateSelection = useCallback( (selection: string[], exclusion: string[]) => { diff --git a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx index f7c67c34a..8b870f3eb 100644 --- a/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx +++ b/src/presentation/webapp/core/pages/module-package-list/ModulePackageListPage.tsx @@ -51,7 +51,7 @@ export const ModulePackageListPage: React.FC = () => { }, [compositionRoot]); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const create = useCallback(() => { diff --git a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx index ac986a9da..bcec65608 100644 --- a/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx +++ b/src/presentation/webapp/core/pages/notifications-list/NotificationsListPage.tsx @@ -48,7 +48,7 @@ export const NotificationsListPage: React.FC = () => { const [syncReport, setSyncReport] = useState(); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const changeUnreadCheckbox = useCallback((event: React.ChangeEvent) => { diff --git a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx index 52420fe89..c91d0256b 100644 --- a/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx +++ b/src/presentation/webapp/core/pages/responsibles-list/ResponsiblesListPage.tsx @@ -23,7 +23,7 @@ export const ResponsiblesListPage: React.FC = () => { const [appConfigurator, updateAppConfigurator] = useState(false); const backHome = useCallback(() => { - history.push("/"); + history.push("/dashboard"); }, [history]); const updateRemoteInstance = useCallback( diff --git a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx index fe6f0a6fc..b1b19c08c 100644 --- a/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx +++ b/src/presentation/webapp/core/pages/store-creation/StoreCreationPage.tsx @@ -56,7 +56,7 @@ const StoreCreationPage: React.FC = () => { }; const close = useCallback(() => { - history.goBack(); + history.push("/dashboard"); }, [history]); const validateError = useCallback((error?: GitHubError): string => { diff --git a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx index df39fbc08..9ba6e8460 100644 --- a/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx +++ b/src/presentation/webapp/core/pages/store-list/StoreListPage.tsx @@ -37,7 +37,7 @@ export const StoreListPage: React.FC = () => { getStores().then(setRows); }, [getStores, objectsTableKey]); - const backHome = () => history.push("/"); + const backHome = () => history.push("/dashboard"); const handleCreateStore = () => history.push(`/stores/new`); diff --git a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx index 724b9a6e3..1975368f7 100644 --- a/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx +++ b/src/presentation/webapp/core/pages/sync-rules-list/SyncRulesListPage.tsx @@ -199,7 +199,7 @@ const SyncRulesPage: React.FC = () => { }; const back = () => { - history.goBack(); + history.push("/dashboard"); }; const confirmDelete = async () => { From 10946ea190e9277bc68dda5475afccb48b6563e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 08:21:37 +0100 Subject: [PATCH 074/163] Set run analytics settings by global instance --- i18n/en.pot | 4 ++-- .../components/msf-Settings/MSFSettingsDialog.tsx | 6 +++--- .../webapp/msf-aggregate-data/pages/MSFHomePage.tsx | 9 +++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 0b3fcabab..6388dc87f 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-12-08T06:28:32.239Z\n" -"PO-Revision-Date: 2020-12-08T06:28:32.239Z\n" +"POT-Creation-Date: 2020-12-08T07:18:48.777Z\n" +"PO-Revision-Date: 2020-12-08T07:18:48.778Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx index 5b9dae30d..405b2b9a7 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -3,11 +3,11 @@ import React, { useMemo, useState } from "react"; import i18n from "../../../../../locales"; import Dropdown from "../../../core/components/dropdown/Dropdown"; -type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; +export type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; -export interface MSFSettings { +export type MSFSettings = { runAnalytics: RunAnalyticsSettings; -} +}; export interface MSFSettingsDialogProps { msfSettings: MSFSettings; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 8c15ec7af..b31b93b47 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -32,6 +32,15 @@ export const MSFHomePage: React.FC = () => { isGlobalAdmin(api).then(setGlobalAdmin); }, [api]); + useEffect(() => { + const isGlobalInstance = !window.location.host.includes("localhost"); + const msfSettings: MSFSettings = isGlobalInstance + ? { runAnalytics: false } + : { runAnalytics: "by-sync-rule-settings" }; + + setMsfSettings(msfSettings); + }, []); + const handleAggregateData = () => { executeAggregateData( compositionRoot, From 3d57afec813d707116054b5574a52e75eef66b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 08:41:35 +0100 Subject: [PATCH 075/163] Retrieve sync rules first --- i18n/en.pot | 4 +- .../pages/MSFHomePagePresenter.ts | 45 ++++++++++++------- 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 6388dc87f..a37322f1b 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-12-08T07:18:48.777Z\n" -"PO-Revision-Date: 2020-12-08T07:18:48.778Z\n" +"POT-Creation-Date: 2020-12-08T07:36:33.077Z\n" +"PO-Revision-Date: 2020-12-08T07:36:33.077Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 720414b12..17e80e006 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -1,5 +1,8 @@ +import _ from "lodash"; import { Period } from "../../../../domain/common/entities/Period"; +import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../locales"; +import { promiseMap } from "../../../../utils/common"; import { CompositionRoot } from "../../../CompositionRoot"; import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; @@ -10,9 +13,7 @@ export async function executeAggregateData( onProgressChange: (progress: string[]) => void, period?: Period ) { - const eventSyncRules = ( - await compositionRoot.rules.list({ filters: { type: "events" }, paging: false }) - ).rows.slice(0, 2); + const eventSyncRules = await getSyncRules(compositionRoot); let syncProgress: string[] = [i18n.t(`Starting Aggregate Data...`)]; @@ -27,7 +28,7 @@ export async function executeAggregateData( await executeSyncRule( compositionRoot, msfSettings, - syncRule.id, + syncRule, onSyncRuleProgressChange, period ); @@ -36,28 +37,26 @@ export async function executeAggregateData( onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); } -const executeSyncRule = async ( +async function executeSyncRule( compositionRoot: CompositionRoot, _msfSettings: MSFSettings, - id: string, + rule: SynchronizationRule, onProgressChange: (event: string) => void, period?: Period -): Promise => { - const rule = await compositionRoot.rules.get(id); - if (!rule) return; +): Promise { const { name, builder, id: syncRule, type = "metadata" } = rule; const newBuilder = period ? { - ...builder, - dataParams: { - ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }, - } + ...builder, + dataParams: { + ...builder.dataParams, + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }, + } : builder; console.log({ newBuilder }); @@ -77,3 +76,15 @@ const executeSyncRule = async ( } } }; + +async function getSyncRules(compositionRoot: CompositionRoot,): Promise { + const rulesList = ( + await compositionRoot.rules.list({ filters: { type: "events" }, paging: false }) + ).rows.slice(0, 2); + + const rules = await promiseMap(rulesList, rule => { + return compositionRoot.rules.get(rule.id) + }); + + return _.compact(rules); +} From 7cc3b6e6e35ad0dc20d043a246ea9a8d8f334820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 11:55:15 +0100 Subject: [PATCH 076/163] Run analytics once time --- i18n/en.pot | 4 +- .../pages/MSFHomePagePresenter.ts | 65 ++++++++++++++----- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a37322f1b..c67c3ffcd 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-12-08T07:36:33.077Z\n" -"PO-Revision-Date: 2020-12-08T07:36:33.077Z\n" +"POT-Creation-Date: 2020-12-08T10:54:24.320Z\n" +"PO-Revision-Date: 2020-12-08T10:54:24.320Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 17e80e006..b050f3c0c 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -2,6 +2,7 @@ import _ from "lodash"; import { Period } from "../../../../domain/common/entities/Period"; import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../locales"; +import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; import { CompositionRoot } from "../../../CompositionRoot"; import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; @@ -13,18 +14,36 @@ export async function executeAggregateData( onProgressChange: (progress: string[]) => void, period?: Period ) { - const eventSyncRules = await getSyncRules(compositionRoot); - let syncProgress: string[] = [i18n.t(`Starting Aggregate Data...`)]; onProgressChange(syncProgress); const onSyncRuleProgressChange = (event: string) => { - syncProgress = [...syncProgress, event]; - onProgressChange(syncProgress); + const lastEvent = syncProgress[syncProgress.length - 1]; + + if (lastEvent !== event) { + syncProgress = [...syncProgress, event]; + onProgressChange(syncProgress); + } }; - for (const syncRule of eventSyncRules) { + const eventSyncRules = await getSyncRules(compositionRoot); + + const someRuleRunAnalytics = eventSyncRules.some( + rule => rule.builder.dataParams?.runAnalytics ?? false + ); + + const rulesWithoutRunAnalylics = someRuleRunAnalytics + ? eventSyncRules.map(rule => + rule.updateBuilderDataParams({ ...rule.builder.dataParams, runAnalytics: false }) + ) + : eventSyncRules; + + if (someRuleRunAnalytics) { + await runAnalytics(compositionRoot, onSyncRuleProgressChange); + } + + for (const syncRule of rulesWithoutRunAnalylics) { await executeSyncRule( compositionRoot, msfSettings, @@ -44,19 +63,18 @@ async function executeSyncRule( onProgressChange: (event: string) => void, period?: Period ): Promise { - const { name, builder, id: syncRule, type = "metadata" } = rule; const newBuilder = period ? { - ...builder, - dataParams: { - ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }, - } + ...builder, + dataParams: { + ...builder.dataParams, + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }, + } : builder; console.log({ newBuilder }); @@ -75,16 +93,29 @@ async function executeSyncRule( onProgressChange(i18n.t(`Finished Sync Rule {{name}} with errors`, { name })); } } -}; +} -async function getSyncRules(compositionRoot: CompositionRoot,): Promise { +async function getSyncRules(compositionRoot: CompositionRoot): Promise { const rulesList = ( await compositionRoot.rules.list({ filters: { type: "events" }, paging: false }) ).rows.slice(0, 2); const rules = await promiseMap(rulesList, rule => { - return compositionRoot.rules.get(rule.id) + return compositionRoot.rules.get(rule.id); }); return _.compact(rules); } + +async function runAnalytics( + compositionRoot: CompositionRoot, + onProgressChange: (event: string) => void +) { + const localInstance = await compositionRoot.instances.getLocal(); + + for await (const message of executeAnalytics(localInstance)) { + onProgressChange(message); + } + + onProgressChange(i18n.t("Analytics execution finished on {{name}}", localInstance)); +} From 84c43d0a3d19bd716b6f00c1146d67744cb1f0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 12:33:28 +0100 Subject: [PATCH 077/163] Merge MSFSettings with Sync Rules: - Run analytics settings --- i18n/en.pot | 4 +-- .../pages/MSFHomePagePresenter.ts | 26 +++++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index c67c3ffcd..4d8127e66 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-12-08T10:54:24.320Z\n" -"PO-Revision-Date: 2020-12-08T10:54:24.320Z\n" +"POT-Creation-Date: 2020-12-08T11:28:20.809Z\n" +"PO-Revision-Date: 2020-12-08T11:28:20.809Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index b050f3c0c..4ba7cf5e1 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -29,28 +29,21 @@ export async function executeAggregateData( const eventSyncRules = await getSyncRules(compositionRoot); - const someRuleRunAnalytics = eventSyncRules.some( - rule => rule.builder.dataParams?.runAnalytics ?? false - ); + const runAnalyticsIsRequired = + msfSettings.runAnalytics === "by-sync-rule-settings" + ? eventSyncRules.some(rule => rule.builder.dataParams?.runAnalytics ?? false) + : msfSettings.runAnalytics; - const rulesWithoutRunAnalylics = someRuleRunAnalytics - ? eventSyncRules.map(rule => - rule.updateBuilderDataParams({ ...rule.builder.dataParams, runAnalytics: false }) - ) - : eventSyncRules; + const rulesWithoutRunAnalylics = eventSyncRules.map(rule => + rule.updateBuilderDataParams({ ...rule.builder.dataParams, runAnalytics: false }) + ); - if (someRuleRunAnalytics) { + if (runAnalyticsIsRequired) { await runAnalytics(compositionRoot, onSyncRuleProgressChange); } for (const syncRule of rulesWithoutRunAnalylics) { - await executeSyncRule( - compositionRoot, - msfSettings, - syncRule, - onSyncRuleProgressChange, - period - ); + await executeSyncRule(compositionRoot, syncRule, onSyncRuleProgressChange, period); } onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); @@ -58,7 +51,6 @@ export async function executeAggregateData( async function executeSyncRule( compositionRoot: CompositionRoot, - _msfSettings: MSFSettings, rule: SynchronizationRule, onProgressChange: (event: string) => void, period?: Period From 77d759648535275c8001af39b0ae4e6d96eca76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 12:56:46 +0100 Subject: [PATCH 078/163] Show run analytics option in sync summary --- i18n/en.pot | 4 ++-- .../core/components/sync-wizard/common/SummaryStep.tsx | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 4d8127e66..b1d8176b1 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-12-08T11:28:20.809Z\n" -"PO-Revision-Date: 2020-12-08T11:28:20.809Z\n" +"POT-Creation-Date: 2020-12-08T11:49:56.423Z\n" +"PO-Revision-Date: 2020-12-08T11:49:56.423Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx index e19df2dbc..2163d6511 100644 --- a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx @@ -427,6 +427,14 @@ const SaveStep = ({ syncRule, onCancel }: SyncWizardStepProps) => { value={syncRule.dataParams.dryRun ? i18n.t("Yes") : i18n.t("No")} /> +
      + +
    )} From 047994148de4fdf27ee19d10ee2a9edea4c542b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 8 Dec 2020 14:09:29 +0100 Subject: [PATCH 079/163] Show last analytics execution --- i18n/en.pot | 7 ++-- i18n/es.po | 5 ++- i18n/fr.po | 5 ++- i18n/pt.po | 5 ++- .../system-info/SystemInfoD2ApiRepository.ts | 20 +++++++++++ .../common/factories/RepositoryFactory.ts | 3 +- src/domain/system-info/entities/SystemInfo.ts | 3 ++ .../repositories/SystemInfoRepository.ts | 10 ++++++ .../usecases/GetSystemInfoUseCase.ts | 11 ++++++ src/presentation/CompositionRoot.ts | 12 +++++++ .../msf-aggregate-data/pages/MSFHomePage.tsx | 5 ++- .../pages/MSFHomePagePresenter.ts | 35 +++++++++++++------ src/utils/date.ts | 6 ++++ 13 files changed, 108 insertions(+), 19 deletions(-) create mode 100644 src/data/system-info/SystemInfoD2ApiRepository.ts create mode 100644 src/domain/system-info/entities/SystemInfo.ts create mode 100644 src/domain/system-info/repositories/SystemInfoRepository.ts create mode 100644 src/domain/system-info/usecases/GetSystemInfoUseCase.ts create mode 100644 src/utils/date.ts diff --git a/i18n/en.pot b/i18n/en.pot index b1d8176b1..795b165ca 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-12-08T11:49:56.423Z\n" -"PO-Revision-Date: 2020-12-08T11:49:56.423Z\n" +"POT-Creation-Date: 2020-12-08T13:01:55.076Z\n" +"PO-Revision-Date: 2020-12-08T13:01:55.076Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1769,6 +1769,9 @@ msgstr "" msgid "Finished Sync Rule {{name}} with errors" msgstr "" +msgid "never" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 03a538037..b97df1c6f 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-12-07T12:11:28.395Z\n" +"POT-Creation-Date: 2020-12-08T12:38:55.848Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1776,6 +1776,9 @@ msgstr "" msgid "Finished Sync Rule {{name}} with errors" msgstr "" +msgid "never" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 6a4d439fc..c8ea883b3 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-12-07T12:11:28.395Z\n" +"POT-Creation-Date: 2020-12-08T12:38:55.848Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1772,6 +1772,9 @@ msgstr "" msgid "Finished Sync Rule {{name}} with errors" msgstr "" +msgid "never" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 6a4d439fc..c8ea883b3 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-12-07T12:11:28.395Z\n" +"POT-Creation-Date: 2020-12-08T12:38:55.848Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1772,6 +1772,9 @@ msgstr "" msgid "Finished Sync Rule {{name}} with errors" msgstr "" +msgid "never" +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/data/system-info/SystemInfoD2ApiRepository.ts b/src/data/system-info/SystemInfoD2ApiRepository.ts new file mode 100644 index 000000000..985c6bac4 --- /dev/null +++ b/src/data/system-info/SystemInfoD2ApiRepository.ts @@ -0,0 +1,20 @@ + +import { Instance } from "../../domain/instance/entities/Instance"; +import { SystemInfo } from "../../domain/system-info/entities/SystemInfo"; +import { D2Api } from "../../types/d2-api"; +import { getD2APiFromInstance } from "../../utils/d2-utils"; +import { SystemSettingsRepository } from "../../domain/system-info/repositories/SystemInfoRepository"; + +export class SystemInfoD2ApiRepository implements SystemSettingsRepository { + + private api: D2Api; + + constructor(instance: Instance) { + this.api = getD2APiFromInstance(instance); + } + + public async get(): Promise { + const { lastAnalyticsTableSuccess } = await this.api.system.info.getData(); + return { lastAnalyticsTableSuccess: lastAnalyticsTableSuccess }; + } +} diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index ff7ed504a..9b31dbab0 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -28,7 +28,7 @@ import { type ClassType = new (...args: any[]) => any; export class RepositoryFactory { - constructor(private encryptionKey: string) {} + constructor(private encryptionKey: string) { } private repositories: Map = new Map(); // TODO: TS 4.1 `${RepositoryKeys}-${string}` @@ -133,4 +133,5 @@ export const Repositories = { FileRepository: "fileRepository", ReportsRepository: "reportsRepository", RulesRepository: "rulesRepository", + SystemInfoRepository: "systemInfoRepository", } as const; diff --git a/src/domain/system-info/entities/SystemInfo.ts b/src/domain/system-info/entities/SystemInfo.ts new file mode 100644 index 000000000..3b8a03cbc --- /dev/null +++ b/src/domain/system-info/entities/SystemInfo.ts @@ -0,0 +1,3 @@ +export interface SystemInfo { + lastAnalyticsTableSuccess?: Date | undefined +} \ No newline at end of file diff --git a/src/domain/system-info/repositories/SystemInfoRepository.ts b/src/domain/system-info/repositories/SystemInfoRepository.ts new file mode 100644 index 000000000..c957ab09a --- /dev/null +++ b/src/domain/system-info/repositories/SystemInfoRepository.ts @@ -0,0 +1,10 @@ +import { Instance } from "../../instance/entities/Instance"; +import { SystemInfo } from "../entities/SystemInfo"; + +export interface SystemSettingsRepositoryConstructor { + new(instance: Instance): SystemSettingsRepository; +} + +export interface SystemSettingsRepository { + get(): Promise; +} diff --git a/src/domain/system-info/usecases/GetSystemInfoUseCase.ts b/src/domain/system-info/usecases/GetSystemInfoUseCase.ts new file mode 100644 index 000000000..675810342 --- /dev/null +++ b/src/domain/system-info/usecases/GetSystemInfoUseCase.ts @@ -0,0 +1,11 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { SystemInfo } from "../entities/SystemInfo"; +import { SystemSettingsRepository } from "../repositories/SystemInfoRepository"; + +export class GetSystemInfoUseCase implements UseCase { + constructor(private systemSettingsRepository: SystemSettingsRepository) { } + + public async execute(): Promise { + return this.systemSettingsRepository.get(); + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 01746d352..6507402f4 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -83,6 +83,8 @@ import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/Cre import { PrepareSyncUseCase } from "../domain/synchronization/usecases/PrepareSyncUseCase"; import { SynchronizationBuilder } from "../domain/synchronization/entities/SynchronizationBuilder"; import { cache } from "../utils/cache"; +import { GetSystemInfoUseCase } from "../domain/system-info/usecases/GetSystemInfoUseCase"; +import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; export class CompositionRoot { private repositoryFactory: RepositoryFactory; @@ -99,6 +101,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.FileRepository, FileD2Repository); this.repositoryFactory.bind(Repositories.ReportsRepository, ReportsD2ApiRepository); this.repositoryFactory.bind(Repositories.RulesRepository, RulesD2ApiRepository); + this.repositoryFactory.bind(Repositories.SystemInfoRepository, SystemInfoD2ApiRepository); this.repositoryFactory.bind( Repositories.MetadataRepository, MetadataJSONRepository, @@ -290,6 +293,15 @@ export class CompositionRoot { }); } + @cache() + public get systemInfo() { + const systemInfoRepository = new SystemInfoD2ApiRepository(this.localInstance); + + return getExecute({ + get: new GetSystemInfoUseCase(systemInfoRepository), + }); + } + @cache() public get events() { const events = new EventsD2ApiRepository(this.localInstance); diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index b31b93b47..563590af2 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -11,7 +11,7 @@ import { MSFSettings, MSFSettingsDialog, } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; -import { executeAggregateData } from "./MSFHomePagePresenter"; +import { executeAggregateData, isGlobalInstance } from "./MSFHomePagePresenter"; export const MSFHomePage: React.FC = () => { const classes = useStyles(); @@ -33,8 +33,7 @@ export const MSFHomePage: React.FC = () => { }, [api]); useEffect(() => { - const isGlobalInstance = !window.location.host.includes("localhost"); - const msfSettings: MSFSettings = isGlobalInstance + const msfSettings: MSFSettings = isGlobalInstance() ? { runAnalytics: false } : { runAnalytics: "by-sync-rule-settings" }; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 4ba7cf5e1..a0332165d 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -4,6 +4,7 @@ import { SynchronizationRule } from "../../../../domain/rules/entities/Synchroni import i18n from "../../../../locales"; import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; +import { formatDateLong } from "../../../../utils/date"; import { CompositionRoot } from "../../../CompositionRoot"; import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; @@ -27,6 +28,12 @@ export async function executeAggregateData( } }; + if (isGlobalInstance && msfSettings.runAnalytics === false) { + const lastExecution = await getLastAnalyticsExecution(compositionRoot); + + onSyncRuleProgressChange(i18n.t("Run analytics is disabled, last analytics execution: {{lastExecution}}", { lastExecution })); + } + const eventSyncRules = await getSyncRules(compositionRoot); const runAnalyticsIsRequired = @@ -49,6 +56,10 @@ export async function executeAggregateData( onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); } +export function isGlobalInstance(): boolean { + return !window.location.host.includes("localhost") +} + async function executeSyncRule( compositionRoot: CompositionRoot, rule: SynchronizationRule, @@ -59,18 +70,16 @@ async function executeSyncRule( const newBuilder = period ? { - ...builder, - dataParams: { - ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }, - } + ...builder, + dataParams: { + ...builder.dataParams, + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }, + } : builder; - console.log({ newBuilder }); - onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); const sync = compositionRoot.sync[type]({ ...newBuilder, syncRule }); @@ -111,3 +120,9 @@ async function runAnalytics( onProgressChange(i18n.t("Analytics execution finished on {{name}}", localInstance)); } + +async function getLastAnalyticsExecution(compositionRoot: CompositionRoot): Promise { + const systemInfo = await compositionRoot.systemInfo.get(); + + return systemInfo.lastAnalyticsTableSuccess ? formatDateLong(systemInfo.lastAnalyticsTableSuccess) : i18n.t("never"); +} diff --git a/src/utils/date.ts b/src/utils/date.ts new file mode 100644 index 000000000..4465ef57e --- /dev/null +++ b/src/utils/date.ts @@ -0,0 +1,6 @@ +import moment from "moment"; + +export function formatDateLong(date: Date) { + const momentDate = moment(date); + return momentDate.format("YYYY-MM-DD HH:mm:ss"); +} \ No newline at end of file From 7a9c367c1bdf57a46c00ad292a293e7665c1c9fe Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 09:20:53 +0100 Subject: [PATCH 080/163] Prettify code --- .../system-info/SystemInfoD2ApiRepository.ts | 2 -- .../common/factories/RepositoryFactory.ts | 2 +- src/domain/system-info/entities/SystemInfo.ts | 4 +-- .../repositories/SystemInfoRepository.ts | 2 +- .../usecases/GetSystemInfoUseCase.ts | 2 +- .../pages/MSFHomePagePresenter.ts | 28 +++++++++++-------- src/utils/date.ts | 2 +- 7 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/data/system-info/SystemInfoD2ApiRepository.ts b/src/data/system-info/SystemInfoD2ApiRepository.ts index 985c6bac4..4df20fc0d 100644 --- a/src/data/system-info/SystemInfoD2ApiRepository.ts +++ b/src/data/system-info/SystemInfoD2ApiRepository.ts @@ -1,4 +1,3 @@ - import { Instance } from "../../domain/instance/entities/Instance"; import { SystemInfo } from "../../domain/system-info/entities/SystemInfo"; import { D2Api } from "../../types/d2-api"; @@ -6,7 +5,6 @@ import { getD2APiFromInstance } from "../../utils/d2-utils"; import { SystemSettingsRepository } from "../../domain/system-info/repositories/SystemInfoRepository"; export class SystemInfoD2ApiRepository implements SystemSettingsRepository { - private api: D2Api; constructor(instance: Instance) { diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 9b31dbab0..d4509f7d5 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -28,7 +28,7 @@ import { type ClassType = new (...args: any[]) => any; export class RepositoryFactory { - constructor(private encryptionKey: string) { } + constructor(private encryptionKey: string) {} private repositories: Map = new Map(); // TODO: TS 4.1 `${RepositoryKeys}-${string}` diff --git a/src/domain/system-info/entities/SystemInfo.ts b/src/domain/system-info/entities/SystemInfo.ts index 3b8a03cbc..f5c91d567 100644 --- a/src/domain/system-info/entities/SystemInfo.ts +++ b/src/domain/system-info/entities/SystemInfo.ts @@ -1,3 +1,3 @@ export interface SystemInfo { - lastAnalyticsTableSuccess?: Date | undefined -} \ No newline at end of file + lastAnalyticsTableSuccess?: Date | undefined; +} diff --git a/src/domain/system-info/repositories/SystemInfoRepository.ts b/src/domain/system-info/repositories/SystemInfoRepository.ts index c957ab09a..28074942c 100644 --- a/src/domain/system-info/repositories/SystemInfoRepository.ts +++ b/src/domain/system-info/repositories/SystemInfoRepository.ts @@ -2,7 +2,7 @@ import { Instance } from "../../instance/entities/Instance"; import { SystemInfo } from "../entities/SystemInfo"; export interface SystemSettingsRepositoryConstructor { - new(instance: Instance): SystemSettingsRepository; + new (instance: Instance): SystemSettingsRepository; } export interface SystemSettingsRepository { diff --git a/src/domain/system-info/usecases/GetSystemInfoUseCase.ts b/src/domain/system-info/usecases/GetSystemInfoUseCase.ts index 675810342..69faa098e 100644 --- a/src/domain/system-info/usecases/GetSystemInfoUseCase.ts +++ b/src/domain/system-info/usecases/GetSystemInfoUseCase.ts @@ -3,7 +3,7 @@ import { SystemInfo } from "../entities/SystemInfo"; import { SystemSettingsRepository } from "../repositories/SystemInfoRepository"; export class GetSystemInfoUseCase implements UseCase { - constructor(private systemSettingsRepository: SystemSettingsRepository) { } + constructor(private systemSettingsRepository: SystemSettingsRepository) {} public async execute(): Promise { return this.systemSettingsRepository.get(); diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index a0332165d..adf03f942 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -31,7 +31,11 @@ export async function executeAggregateData( if (isGlobalInstance && msfSettings.runAnalytics === false) { const lastExecution = await getLastAnalyticsExecution(compositionRoot); - onSyncRuleProgressChange(i18n.t("Run analytics is disabled, last analytics execution: {{lastExecution}}", { lastExecution })); + onSyncRuleProgressChange( + i18n.t("Run analytics is disabled, last analytics execution: {{lastExecution}}", { + lastExecution, + }) + ); } const eventSyncRules = await getSyncRules(compositionRoot); @@ -57,7 +61,7 @@ export async function executeAggregateData( } export function isGlobalInstance(): boolean { - return !window.location.host.includes("localhost") + return !window.location.host.includes("localhost"); } async function executeSyncRule( @@ -70,14 +74,14 @@ async function executeSyncRule( const newBuilder = period ? { - ...builder, - dataParams: { - ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }, - } + ...builder, + dataParams: { + ...builder.dataParams, + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }, + } : builder; onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); @@ -124,5 +128,7 @@ async function runAnalytics( async function getLastAnalyticsExecution(compositionRoot: CompositionRoot): Promise { const systemInfo = await compositionRoot.systemInfo.get(); - return systemInfo.lastAnalyticsTableSuccess ? formatDateLong(systemInfo.lastAnalyticsTableSuccess) : i18n.t("never"); + return systemInfo.lastAnalyticsTableSuccess + ? formatDateLong(systemInfo.lastAnalyticsTableSuccess) + : i18n.t("never"); } diff --git a/src/utils/date.ts b/src/utils/date.ts index 4465ef57e..a27d6ca80 100644 --- a/src/utils/date.ts +++ b/src/utils/date.ts @@ -3,4 +3,4 @@ import moment from "moment"; export function formatDateLong(date: Date) { const momentDate = moment(date); return momentDate.format("YYYY-MM-DD HH:mm:ss"); -} \ No newline at end of file +} From 484ca4240cc581c1ff549f794643acac76a3bd8b Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 09:22:17 +0100 Subject: [PATCH 081/163] Fix i18n with default namespace separator --- i18n/en.pot | 7 +++++-- i18n/es.po | 5 ++++- i18n/fr.po | 5 ++++- i18n/pt.po | 5 ++++- .../msf-aggregate-data/pages/MSFHomePagePresenter.ts | 1 + 5 files changed, 18 insertions(+), 5 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 795b165ca..be65f58eb 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-12-08T13:01:55.076Z\n" -"PO-Revision-Date: 2020-12-08T13:01:55.076Z\n" +"POT-Creation-Date: 2020-12-09T08:22:01.724Z\n" +"PO-Revision-Date: 2020-12-09T08:22:01.724Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1757,6 +1757,9 @@ msgstr "" msgid "Starting Aggregate Data..." msgstr "" +msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index b97df1c6f..898c2d6d4 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-12-08T12:38:55.848Z\n" +"POT-Creation-Date: 2020-12-09T08:22:01.724Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1764,6 +1764,9 @@ msgstr "" msgid "Starting Aggregate Data..." msgstr "" +msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index c8ea883b3..d5388dd9d 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-12-08T12:38:55.848Z\n" +"POT-Creation-Date: 2020-12-09T08:22:01.724Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1760,6 +1760,9 @@ msgstr "" msgid "Starting Aggregate Data..." msgstr "" +msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index c8ea883b3..d5388dd9d 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-12-08T12:38:55.848Z\n" +"POT-Creation-Date: 2020-12-09T08:22:01.724Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1760,6 +1760,9 @@ msgstr "" msgid "Starting Aggregate Data..." msgstr "" +msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index adf03f942..8a4042ed0 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -34,6 +34,7 @@ export async function executeAggregateData( onSyncRuleProgressChange( i18n.t("Run analytics is disabled, last analytics execution: {{lastExecution}}", { lastExecution, + nsSeparator: false, }) ); } From ce717e51c338c75fc132dcc4465c8af051be19b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 9 Dec 2020 09:44:13 +0100 Subject: [PATCH 082/163] Add sync result to sync progress --- i18n/en.pot | 4 +-- .../pages/MSFHomePagePresenter.ts | 27 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 2a74fd57c..dc601f3d8 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-12-09T06:00:19.875Z\n" -"PO-Revision-Date: 2020-12-09T06:00:19.875Z\n" +"POT-Creation-Date: 2020-12-09T08:43:07.131Z\n" +"PO-Revision-Date: 2020-12-09T08:43:07.131Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index adf03f942..335087f0c 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -1,6 +1,8 @@ import _ from "lodash"; import { Period } from "../../../../domain/common/entities/Period"; +import { PublicInstance } from "../../../../domain/instance/entities/Instance"; import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; +import { Store } from "../../../../domain/stores/entities/Store"; import i18n from "../../../../locales"; import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; @@ -93,6 +95,21 @@ async function executeSyncRule( if (syncReport) await compositionRoot.reports.save(syncReport); if (done && syncReport) { + syncReport.getResults().forEach(result => { + onProgressChange(`${i18n.t("Summary")}:`); + const origin = result.origin + ? `${i18n.t("Origin")}: ${getOriginName(result.origin)} ` + : ""; + const originPackage = result.originPackage + ? `${i18n.t("Origin package")}: ${result.originPackage.name}` + : ""; + const destination = `${i18n.t("Destination instance")}: ${result.instance.name}`; + onProgressChange(`${origin} ${originPackage} -> ${destination}`); + + const status = `${i18n.t("Status")}: ${_.startCase(_.toLower(result.status))}`; + const message = result.message ?? ""; + if (result.message) onProgressChange(`${status} - ${message}`); + }); onProgressChange(i18n.t(`Finished Sync Rule {{name}}`, { name })); } else if (done) { onProgressChange(i18n.t(`Finished Sync Rule {{name}} with errors`, { name })); @@ -132,3 +149,13 @@ async function getLastAnalyticsExecution(compositionRoot: CompositionRoot): Prom ? formatDateLong(systemInfo.lastAnalyticsTableSuccess) : i18n.t("never"); } + +const getOriginName = (source: PublicInstance | Store) => { + if ((source as Store).token) { + const store = source as Store; + return store.account + " - " + store.repository; + } else { + const instance = source as PublicInstance; + return instance.name; + } +}; From 8ac3a3097bebd26690cfe90f33220b0592eb46ce Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 09:50:05 +0100 Subject: [PATCH 083/163] Hide settings wheel for non-admin users --- .../webapp/core/pages/home/HomePage.tsx | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index 074cc5ae6..b254c1b1c 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -6,6 +6,7 @@ import i18n from "../../../../../locales"; import { isAppConfigurator, isAppExecutor, + isGlobalAdmin, shouldShowDeletedObjects, } from "../../../../../utils/permissions"; import { Card, Landing } from "../../../../react/core/components/landing/Landing"; @@ -46,14 +47,17 @@ const LandingPage: React.FC = ({ type }) => { const classes = useStyles(); const [showDeletedObjects, setShowDeletedObjects] = useState(false); + const [pendingNotifications, setPendingNotifications] = useState(0); + const [appConfigurator, setAppConfigurator] = useState(false); const [appExecutor, setAppExecutor] = useState(false); - const [pendingNotifications, setPendingNotifications] = useState(0); + const [globalAdmin, setGlobalAdmin] = useState(false); useEffect(() => { shouldShowDeletedObjects(api).then(setShowDeletedObjects); isAppConfigurator(api).then(setAppConfigurator); isAppExecutor(api).then(setAppExecutor); + isGlobalAdmin(api).then(setGlobalAdmin); compositionRoot.notifications.list().then(notifications => { const unread = notifications.filter(({ read }) => !read).length; setPendingNotifications(unread); @@ -245,11 +249,17 @@ const LandingPage: React.FC = ({ type }) => { return ( - - - settings - - + {globalAdmin ? ( + + + settings + + + ) : null} Date: Wed, 9 Dec 2020 09:50:22 +0100 Subject: [PATCH 084/163] Update constant client to be built from an instance --- src/data/storage/StorageConstantClient.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index e71e6edc8..4da0c7b42 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -1,6 +1,8 @@ import { generateUid } from "d2/uid"; +import { Instance } from "../../domain/instance/entities/Instance"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { D2Api } from "../../types/d2-api"; +import { getD2APiFromInstance } from "../../utils/d2-utils"; interface Constant { id: string; @@ -12,8 +14,11 @@ interface Constant { const defaultName = "Bulk Load Storage"; export class StorageConstantClient extends StorageClient { - constructor(private api: D2Api) { + private api: D2Api; + + constructor(instance: Instance) { super(); + this.api = getD2APiFromInstance(instance); } private buildDefault(key: string, value: T): Constant { From 708aae36572195f90505769b88641d6bb0d1cd56 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 09:50:31 +0100 Subject: [PATCH 085/163] Add initial ConfigRepository --- src/data/config/ConfigAppRepository.ts | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/data/config/ConfigAppRepository.ts diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts new file mode 100644 index 000000000..8a8246730 --- /dev/null +++ b/src/data/config/ConfigAppRepository.ts @@ -0,0 +1,51 @@ +import { Instance } from "../../domain/instance/entities/Instance"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; +import { cache, clear } from "../../utils/cache"; +import { Namespace } from "../storage/Namespaces"; +import { StorageConstantClient } from "../storage/StorageConstantClient"; +import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; + +interface ConfigRepository { + getStorageClient(instance: Instance): Promise; + changeStorageClient(instance: Instance, client: "dataStore" | "constant"): Promise; +} + +export class ConfigAppClient implements ConfigRepository { + constructor() {} + + @cache() + public async getStorageClient(instance: Instance): Promise { + const dataStoreClient = new StorageDataStoreClient(instance); + const constantClient = new StorageConstantClient(instance); + + const dataStoreConfig = await dataStoreClient.getObject(Namespace.CONFIG); + const constantConfig = await constantClient.getObject(Namespace.CONFIG); + + if (dataStoreConfig && constantConfig) { + // Decide what to do, clear constant maybe? + console.error("Two storages initialized"); + } + + return dataStoreConfig ? dataStoreClient : constantClient; + } + + public async changeStorageClient( + instance: Instance, + client: "dataStore" | "constant" + ): Promise { + const dataStoreClient = new StorageDataStoreClient(instance); + const constantClient = new StorageConstantClient(instance); + + const oldClient = client === "dataStore" ? constantClient : dataStoreClient; + const newClient = client === "dataStore" ? dataStoreClient : constantClient; + + console.log({ oldClient, newClient }); + + // Clear new client + // Copy old client data into new client + // Clear old client + + // Reset memoize + clear(this.getStorageClient, this); + } +} From 9ba25ad95e236baab0e666b1ae26105d25fae555 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 11:25:03 +0100 Subject: [PATCH 086/163] Add basic settings page --- i18n/en.pot | 16 ++++- i18n/es.po | 14 ++++- i18n/fr.po | 14 ++++- i18n/pt.po | 14 ++++- src/presentation/webapp/Root.tsx | 3 + .../webapp/core/pages/home/HomePage.tsx | 14 +++-- .../core/pages/settings/SettingsPage.tsx | 33 ++++++++++ .../storage/StorageSettingDropdown.tsx | 61 +++++++++++++++++++ 8 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 src/presentation/webapp/core/pages/settings/SettingsPage.tsx create mode 100644 src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 7e8a28928..79de43878 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-12-09T08:23:20.457Z\n" -"PO-Revision-Date: 2020-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T10:20:20.683Z\n" +"PO-Revision-Date: 2020-12-09T10:20:20.683Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1640,6 +1640,18 @@ msgstr "" msgid "Refresh" msgstr "" +msgid "Storage" +msgstr "" + +msgid "Data Store" +msgstr "" + +msgid "Metadata constant" +msgstr "" + +msgid "Application storage" +msgstr "" + msgid "Edit store" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6590b5458..689b3d36c 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T10:19:19.536Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1647,6 +1647,18 @@ msgstr "" msgid "Refresh" msgstr "" +msgid "Storage" +msgstr "" + +msgid "Data Store" +msgstr "" + +msgid "Metadata constant" +msgstr "" + +msgid "Application storage" +msgstr "" + msgid "Edit store" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 2359a787c..d65d79db9 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T10:19:19.536Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1643,6 +1643,18 @@ msgstr "" msgid "Refresh" msgstr "" +msgid "Storage" +msgstr "" + +msgid "Data Store" +msgstr "" + +msgid "Metadata constant" +msgstr "" + +msgid "Application storage" +msgstr "" + msgid "Edit store" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 2359a787c..d65d79db9 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T10:19:19.536Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1643,6 +1643,18 @@ msgstr "" msgid "Refresh" msgstr "" +msgid "Storage" +msgstr "" + +msgid "Data Store" +msgstr "" + +msgid "Metadata constant" +msgstr "" + +msgid "Application storage" +msgstr "" + msgid "Edit store" msgstr "" diff --git a/src/presentation/webapp/Root.tsx b/src/presentation/webapp/Root.tsx index 24a896997..9ce686696 100644 --- a/src/presentation/webapp/Root.tsx +++ b/src/presentation/webapp/Root.tsx @@ -16,6 +16,7 @@ import ModulePackageListPage from "./core/pages/module-package-list/ModulePackag import ModuleCreationPage from "./core/pages/modules-creation/ModuleCreationPage"; import NotificationsListPage from "./core/pages/notifications-list/NotificationsListPage"; import ResponsiblesListPage from "./core/pages/responsibles-list/ResponsiblesListPage"; +import { SettingsPage } from "./core/pages/settings/SettingsPage"; import StoreCreationPage from "./core/pages/store-creation/StoreCreationPage"; import StoreListPage from "./core/pages/store-list/StoreListPage"; import SyncRulesCreationPage, { @@ -102,6 +103,8 @@ const Root: React.FC = () => { render={() => } /> + } /> + diff --git a/src/presentation/webapp/core/pages/home/HomePage.tsx b/src/presentation/webapp/core/pages/home/HomePage.tsx index b254c1b1c..b794a1474 100644 --- a/src/presentation/webapp/core/pages/home/HomePage.tsx +++ b/src/presentation/webapp/core/pages/home/HomePage.tsx @@ -53,6 +53,14 @@ const LandingPage: React.FC = ({ type }) => { const [appExecutor, setAppExecutor] = useState(false); const [globalAdmin, setGlobalAdmin] = useState(false); + const backHome = () => { + history.push("/"); + }; + + const goToSettings = () => { + history.push("/settings"); + }; + useEffect(() => { shouldShowDeletedObjects(api).then(setShowDeletedObjects); isAppConfigurator(api).then(setAppConfigurator); @@ -64,10 +72,6 @@ const LandingPage: React.FC = ({ type }) => { }); }, [api, compositionRoot]); - const backHome = () => { - history.push("/"); - }; - const allCards: Card[] = useMemo( () => [ { @@ -255,7 +259,7 @@ const LandingPage: React.FC = ({ type }) => { title={i18n.t("Settings")} placement="left" > - + settings diff --git a/src/presentation/webapp/core/pages/settings/SettingsPage.tsx b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx new file mode 100644 index 000000000..f63c20c45 --- /dev/null +++ b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx @@ -0,0 +1,33 @@ +import { FormGroup, makeStyles } from "@material-ui/core"; +import React from "react"; +import { useHistory } from "react-router-dom"; +import i18n from "../../../../../locales"; +import PageHeader from "../../../../react/core/components/page-header/PageHeader"; +import { StorageSettingDropdown } from "./storage/StorageSettingDropdown"; + +export const SettingsPage: React.FC = () => { + const history = useHistory(); + const classes = useStyles(); + + const backHome = () => history.push("/dashboard"); + + return ( + + + +
    +

    {i18n.t("Storage")}

    + + + + +
    +
    + ); +}; + +const useStyles = makeStyles({ + content: { margin: "1rem", marginBottom: 35, marginLeft: 0 }, + title: { marginTop: 0 }, + container: { margin: "1rem" }, +}); diff --git a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx new file mode 100644 index 000000000..1ccb1c663 --- /dev/null +++ b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx @@ -0,0 +1,61 @@ +import { Icon, ListItem, ListItemIcon, ListItemText, Menu, MenuItem } from "@material-ui/core"; +import React, { useMemo, useState } from "react"; +import i18n from "../../../../../../locales"; + +export const StorageSettingDropdown: React.FC = () => { + const [anchorEl, setAnchorEl] = useState(null); + const [selectedOption, setSelectedOption] = useState("dataStore"); + + const handleClickListItem = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuItemClick = (key: string) => { + setSelectedOption(key); + setAnchorEl(null); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const options = useMemo( + () => [ + { id: "dataStore", label: i18n.t("Data Store") }, + { id: "constant", label: i18n.t("Metadata constant") }, + ], + [] + ); + + return ( + + + + storage + + option.id === selectedOption)?.label} + /> + + + + {options.map(option => ( + handleMenuItemClick(option.id)} + > + {option.label} + + ))} + + + ); +}; From 714ba9a642b9206dcf3f2649195671ef0169f0d8 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 12:59:55 +0100 Subject: [PATCH 087/163] Fix local instance sync --- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index b17d82636..f219db1ef 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -176,7 +176,11 @@ export abstract class GenericSyncUseCase { if (!data) return undefined; - const instance = Instance.build(data).decryptPassword(this.encryptionKey); + const instance = Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); try { const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); From 3630be18451bb4bb467464f4c51a39ebb8ac78b1 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 9 Dec 2020 13:05:43 +0100 Subject: [PATCH 088/163] Patch all usages of local instance --- src/domain/instance/usecases/GetInstanceByIdUseCase.ts | 8 +++++++- .../notifications/usecases/CancelPullRequestUseCase.ts | 6 +++++- .../notifications/usecases/ImportPullRequestUseCase.ts | 6 +++++- .../notifications/usecases/ListNotificationsUseCase.ts | 6 +++++- src/domain/synchronization/usecases/GenericSyncUseCase.ts | 6 +++++- src/domain/synchronization/usecases/PrepareSyncUseCase.ts | 6 +++++- 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index 13f83eb1a..ba4a75081 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -18,6 +18,12 @@ export class GetInstanceByIdUseCase implements UseCase { if (!data) return Either.error("NOT_FOUND"); - return Either.success(Instance.build(data).decryptPassword(this.encryptionKey)); + const instance = Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); + + return Either.success(instance); } } diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index 271e31c98..69da70934 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -87,6 +87,10 @@ export class CancelPullRequestUseCase implements UseCase { const data = objects.find(data => data.id === id); if (!data) return undefined; - return Instance.build(data).decryptPassword(this.encryptionKey); + return Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); } } diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index b28d96596..93d1ecb9e 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -93,7 +93,11 @@ export class ImportPullRequestUseCase implements UseCase { const data = objects.find(data => data.id === id); if (!data) return undefined; - return Instance.build(data).decryptPassword(this.encryptionKey); + return Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); } private async getNotification( diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 59d69f007..2bc0fbf69 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -56,7 +56,11 @@ export class ListNotificationsUseCase implements UseCase { const data = objects.find(data => data.id === id); if (!data) return undefined; - return Instance.build(data).decryptPassword(this.encryptionKey); + return Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); } private async updateSentPullRequest( diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index 2c8ada76c..8c38bd351 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -173,7 +173,11 @@ export abstract class GenericSyncUseCase { if (!data) return undefined; - const instance = Instance.build(data).decryptPassword(this.encryptionKey); + const instance = Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); try { const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 3767b3249..902e986c9 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -87,7 +87,11 @@ export class PrepareSyncUseCase implements UseCase { const data = objects.find(data => data.id === id); if (!data) return Either.error("INSTANCE_NOT_FOUND"); - const instance = Instance.build(data).decryptPassword(this.encryptionKey); + const instance = Instance.build({ + ...data, + url: data.type === "local" ? this.localInstance.url : data.url, + version: data.type === "local" ? this.localInstance.version : data.version, + }).decryptPassword(this.encryptionKey); const version = await this.repositoryFactory.instanceRepository(instance).getVersion(); return Either.success(instance.update({ version })); From 5b100733d2d7bca5cc04d0ead0c205fea3cadc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 9 Dec 2020 17:07:07 +0100 Subject: [PATCH 089/163] Create infrastructure to validate and show dialog --- i18n/en.pot | 10 +- i18n/es.po | 8 +- i18n/fr.po | 8 +- i18n/pt.po | 8 +- .../instance/usecases/ListInstancesUseCase.ts | 11 +- .../msf-aggregate-data/pages/MSFHomePage.tsx | 36 +++++- .../pages/MSFHomePagePresenter.ts | 120 ++++++++++++------ 7 files changed, 150 insertions(+), 51 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index b1dd482b9..d5fd6b6b0 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-12-09T08:47:04.153Z\n" -"PO-Revision-Date: 2020-12-09T08:47:04.153Z\n" +"POT-Creation-Date: 2020-12-09T16:06:06.293Z\n" +"PO-Revision-Date: 2020-12-09T16:06:06.293Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1757,6 +1757,12 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "MSF Validation" +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + msgid "Starting Aggregate Data..." msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 6590b5458..c55db084b 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T15:51:00.236Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1764,6 +1764,12 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "MSF Validation" +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + msgid "Starting Aggregate Data..." msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 2359a787c..758068275 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T15:51:00.236Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1760,6 +1760,12 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "MSF Validation" +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + msgid "Starting Aggregate Data..." msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 2359a787c..758068275 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-12-09T08:23:20.457Z\n" +"POT-Creation-Date: 2020-12-09T15:51:00.236Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1760,6 +1760,12 @@ msgstr "" msgid "Go to History" msgstr "" +msgid "MSF Validation" +msgstr "" + +msgid "Do you want to proceed?" +msgstr "" + msgid "Starting Aggregate Data..." msgstr "" diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index 41c552123..71779170e 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -6,6 +6,7 @@ import { Instance, InstanceData } from "../entities/Instance"; export interface ListInstancesUseCaseProps { search?: string; + ids?: string[]; } export class ListInstancesUseCase implements UseCase { @@ -15,12 +16,12 @@ export class ListInstancesUseCase implements UseCase { private encryptionKey: string ) {} - public async execute({ search }: ListInstancesUseCaseProps = {}): Promise { + public async execute({ search, ids }: ListInstancesUseCaseProps = {}): Promise { const objects = await this.repositoryFactory .storageRepository(this.localInstance) .listObjectsInCollection(Namespace.INSTANCES); - const filteredData = search + const filteredDataBySearch = search ? _.filter(objects, o => _(o) .values() @@ -32,7 +33,11 @@ export class ListInstancesUseCase implements UseCase { ) : objects; - return filteredData.map(data => + const filteredDataByIds = filteredDataBySearch.filter( + instanceData => !ids || ids.includes(instanceData.id) + ); + + return filteredDataByIds.map(data => Instance.build({ ...data, url: data.type === "local" ? this.localInstance.url : data.url, diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 563590af2..9c9efe92f 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,4 +1,5 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; +import { ConfirmationDialog } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { Period } from "../../../../domain/common/entities/Period"; @@ -22,6 +23,7 @@ export const MSFHomePage: React.FC = () => { const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); const [period, setPeriod] = useState(); + const [msfValidationErrors, setMsfValidationErrors] = useState(); const [msfSettings, setMsfSettings] = useState({ runAnalytics: "by-sync-rule-settings", @@ -40,11 +42,13 @@ export const MSFHomePage: React.FC = () => { setMsfSettings(msfSettings); }, []); - const handleAggregateData = () => { + const handleAggregateData = (validateRequired: boolean) => { executeAggregateData( compositionRoot, msfSettings, + validateRequired, progress => setSyncProgress(progress), + errors => setMsfValidationErrors(errors), period ); }; @@ -89,7 +93,7 @@ export const MSFHomePage: React.FC = () => {
    )} + + {syncRule.type === "events" && ( +
    + +
    + )}
    ); }; diff --git a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx index 2163d6511..31e624638 100644 --- a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx @@ -435,6 +435,18 @@ const SaveStep = ({ syncRule, onCancel }: SyncWizardStepProps) => { } /> + {syncRule.type === "events" && ( +
      + +
    + )} )} From d514754fdd9a8f77ee52bb108f97d34c1fd83ae9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 10 Dec 2020 11:57:42 +0100 Subject: [PATCH 092/163] Move runAnalytics to DataSynchronizationParams --- i18n/en.pot | 4 ++-- src/domain/aggregated/types.ts | 1 + src/types/d2.ts | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index be65f58eb..18b43525c 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-12-09T08:22:01.724Z\n" -"PO-Revision-Date: 2020-12-09T08:22:01.724Z\n" +"POT-Creation-Date: 2020-12-10T10:54:47.677Z\n" +"PO-Revision-Date: 2020-12-10T10:54:47.677Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/domain/aggregated/types.ts b/src/domain/aggregated/types.ts index ece16c558..9610faf3e 100644 --- a/src/domain/aggregated/types.ts +++ b/src/domain/aggregated/types.ts @@ -12,6 +12,7 @@ export interface DataSynchronizationParams extends DataImportParams { generateNewUid?: boolean; enableAggregation?: boolean; aggregationType?: DataSyncAggregation; + runAnalytics?: boolean; } export type DataSyncPeriod = diff --git a/src/types/d2.ts b/src/types/d2.ts index d67a56cf3..59118498b 100644 --- a/src/types/d2.ts +++ b/src/types/d2.ts @@ -64,5 +64,4 @@ export interface DataImportParams { skipExistingCheck?: boolean; strategy?: "NEW_AND_UPDATES" | "NEW" | "UPDATES" | "DELETES"; format?: "json" | "xml" | "csv" | "pdf" | "adx"; - runAnalytics?: boolean; } From 55c40287a13b21e83e147110dacc35840b22c68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Thu, 10 Dec 2020 13:13:35 +0100 Subject: [PATCH 093/163] Add select children action in events sync rule --- i18n/en.pot | 4 +- .../common/MetadataSelectionStep.tsx | 123 +++++++++++++----- 2 files changed, 89 insertions(+), 38 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 79de43878..5fd0ff54b 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-12-09T10:20:20.683Z\n" -"PO-Revision-Date: 2020-12-09T10:20:20.683Z\n" +"POT-Creation-Date: 2020-12-10T12:12:12.320Z\n" +"PO-Revision-Date: 2020-12-10T12:12:12.320Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx index 6b00d754c..0da8e0865 100644 --- a/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx @@ -1,8 +1,10 @@ -import { useSnackbar } from "d2-ui-components"; +import { Icon } from "@material-ui/core"; +import { TableAction, useSnackbar } from "d2-ui-components"; import _ from "lodash"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Instance } from "../../../../../../domain/instance/entities/Instance"; import i18n from "../../../../../../locales"; +import { D2Model } from "../../../../../../models/dhis/default"; import { metadataModels } from "../../../../../../models/dhis/factory"; import { AggregatedDataElementModel, @@ -15,8 +17,10 @@ import { DataSetModel, IndicatorModel, } from "../../../../../../models/dhis/metadata"; +import { MetadataType } from "../../../../../../utils/d2"; import { getMetadata } from "../../../../../../utils/synchronization"; import { useAppContext } from "../../../contexts/AppContext"; +import { getChildrenRows } from "../../mapping-table/utils"; import MetadataTable from "../../metadata-table/MetadataTable"; import { SyncWizardStepProps } from "../Steps"; @@ -53,45 +57,50 @@ export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizard const [remoteInstance, setRemoteInstance] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); - const { models, childrenKeys } = config[syncRule.type]; - const changeSelection = (newMetadataIds: string[], newExclusionIds: string[]) => { - const additions = _.difference(newMetadataIds, metadataIds); - if (additions.length > 0) { - snackbar.info( - i18n.t("Selected {{difference}} elements", { difference: additions.length }), - { - autoHideDuration: 1000, - } - ); - } + const [model, setModel] = useState(() => models[0] ?? {}); + const [rows, setRows] = useState([]); - const removals = _.difference(metadataIds, newMetadataIds); - if (removals.length > 0) { - snackbar.info( - i18n.t("Removed {{difference}} elements", { - difference: Math.abs(removals.length), - }), - { autoHideDuration: 1000 } - ); - } + const changeSelection = useCallback( + (newMetadataIds: string[], newExclusionIds: string[]) => { + const additions = _.difference(newMetadataIds, metadataIds); + if (additions.length > 0) { + snackbar.info( + i18n.t("Selected {{difference}} elements", { difference: additions.length }), + { + autoHideDuration: 1000, + } + ); + } - getMetadata(api, newMetadataIds, "id").then(metadata => { - const types = _.keys(metadata); - onChange( - syncRule - .updateMetadataIds(newMetadataIds) - .updateExcludedIds(newExclusionIds) - .updateMetadataTypes(types) - .updateDataSyncEnableAggregation( - types.includes("indicators") || types.includes("programIndicators") - ) - ); - }); + const removals = _.difference(metadataIds, newMetadataIds); + if (removals.length > 0) { + snackbar.info( + i18n.t("Removed {{difference}} elements", { + difference: Math.abs(removals.length), + }), + { autoHideDuration: 1000 } + ); + } - updateMetadataIds(newMetadataIds); - }; + getMetadata(api, newMetadataIds, "id").then(metadata => { + const types = _.keys(metadata); + onChange( + syncRule + .updateMetadataIds(newMetadataIds) + .updateExcludedIds(newExclusionIds) + .updateMetadataTypes(types) + .updateDataSyncEnableAggregation( + types.includes("indicators") || types.includes("programIndicators") + ) + ); + }); + + updateMetadataIds(newMetadataIds); + }, + [api, metadataIds, onChange, snackbar, syncRule] + ); useEffect(() => { compositionRoot.instances.getById(syncRule.originInstance).then(result => { @@ -109,6 +118,45 @@ export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizard }); }, [compositionRoot, snackbar, syncRule]); + const notifyNewModel = useCallback(model => { + setModel(() => model); + }, []); + + const updateRows = useCallback( + (rows: MetadataType[]) => { + setRows([...rows, ...getChildrenRows(rows, model)]); + }, + [model] + ); + + const actions: TableAction[] = useMemo( + () => + syncRule.type === "events" + ? [ + { + name: "select-children-rows", + text: i18n.t("Select children"), + multiple: true, + onClick: (selection: string[]) => { + const selectedRows = _.compact( + selection.map(id => _.find(rows, ["id", id])) + ); + const children = getChildrenRows(selectedRows, model).map( + ({ id }) => id + ); + changeSelection(children, []); + }, + icon: done_all, + isActive: (selection: MetadataType[]) => { + const children = getChildrenRows(selection, model); + return children.length > 0; + }, + }, + ] + : [], + [model, rows, changeSelection, syncRule.type] + ); + if (loading || error) return null; return ( @@ -116,10 +164,13 @@ export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizard models={models} selectedIds={syncRule.metadataIds} excludedIds={syncRule.excludedIds} + additionalActions={actions} notifyNewSelection={changeSelection} childrenKeys={childrenKeys} showIndeterminateSelection={true} remoteInstance={remoteInstance} + notifyNewModel={notifyNewModel} + notifyRowsChange={updateRows} /> ); } From 625b1702bf3a3e5ea185d7ceb4709cc5c53edd12 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 11 Dec 2020 12:09:30 +0100 Subject: [PATCH 094/163] Add major refactor to choose from constant or dataStore storage --- src/data/config/ConfigAppRepository.ts | 27 +++----- .../integration/local-instance-mapped.spec.ts | 2 - .../integration/sync-aggregated.spec.ts | 2 - .../__tests__/integration/sync-events.spec.ts | 2 - .../integration/sync-metadata.spec.ts | 2 - src/data/reports/ReportsD2ApiRepository.ts | 31 +++++---- src/data/rules/RulesD2ApiRepository.ts | 28 ++++---- src/data/stores/StoreD2ApiRepository.ts | 32 +++++---- .../__tests__/integration/helpers.ts | 4 +- .../transformations-api-32.spec.ts | 1 - .../common/factories/RepositoryFactory.ts | 14 ++-- src/domain/config/ConfigRepository.ts | 8 ++- .../usecases/DeleteInstanceUseCase.ts | 8 ++- .../usecases/GetInstanceByIdUseCase.ts | 11 ++- .../instance/usecases/ListInstancesUseCase.ts | 10 ++- .../instance/usecases/SaveInstanceUseCase.ts | 14 ++-- .../usecases/GetMappingByOwnerUseCase.ts | 21 ++++-- .../mapping/usecases/SaveMappingUseCase.ts | 20 +++--- .../usecases/GetResponsiblesUseCase.ts | 10 ++- .../usecases/ListResponsiblesUseCase.ts | 10 ++- .../usecases/SetResponsiblesUseCase.ts | 26 +++---- .../modules/usecases/DeleteModuleUseCase.ts | 20 +++--- .../modules/usecases/GetModuleUseCase.ts | 8 ++- .../modules/usecases/ListModulesUseCase.ts | 8 ++- .../modules/usecases/SaveModuleUseCase.ts | 8 ++- .../usecases/CancelPullRequestUseCase.ts | 40 +++++++---- .../usecases/ImportPullRequestUseCase.ts | 67 ++++++++++++------- .../usecases/ListNotificationsUseCase.ts | 44 +++++++----- .../usecases/MarkReadNotificationsUseCase.ts | 20 +++--- .../UpdatePullRequestStatusUseCase.ts | 24 ++++--- .../usecases/ListImportedPackagesUseCase.ts | 10 ++- .../usecases/SaveImportedPackagesUseCase.ts | 11 ++- .../packages/usecases/CreatePackageUseCase.ts | 12 ++-- .../packages/usecases/DeletePackageUseCase.ts | 23 ++++--- .../packages/usecases/DiffPackageUseCase.ts | 19 ++++-- .../usecases/DownloadPackageUseCase.ts | 8 ++- .../packages/usecases/GetPackageUseCase.ts | 8 ++- .../packages/usecases/ListPackagesUseCase.ts | 8 ++- .../usecases/PublishStorePackageUseCase.ts | 11 ++- .../reports/repositories/ReportsRepository.ts | 4 +- .../rules/repositories/RulesRepository.ts | 4 +- .../storage/repositories/StorageClient.ts | 2 +- .../storage/usecases/DownloadFileUseCase.ts | 6 +- .../stores/usecases/DeleteStoreUseCase.ts | 7 +- src/domain/stores/usecases/GetStoreUseCase.ts | 7 +- .../stores/usecases/ListStoresUseCase.ts | 7 +- .../stores/usecases/SaveStoreUseCase.ts | 18 ++--- .../usecases/SetStoreAsDefaultUseCase.ts | 7 +- .../stores/usecases/ValidateStoreUseCase.ts | 6 +- .../usecases/CreatePullRequestUseCase.ts | 29 +++++--- .../usecases/GenericSyncUseCase.ts | 11 ++- .../usecases/PrepareSyncUseCase.ts | 20 ++++-- src/presentation/CompositionRoot.ts | 36 ++++------ 53 files changed, 472 insertions(+), 324 deletions(-) diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts index d5a6fcd9c..fd82f6077 100644 --- a/src/data/config/ConfigAppRepository.ts +++ b/src/data/config/ConfigAppRepository.ts @@ -7,14 +7,12 @@ import { Namespace } from "../storage/Namespaces"; import { StorageConstantClient } from "../storage/StorageConstantClient"; import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; -export class ConfigAppClient implements ConfigRepository { - constructor() {} +export class ConfigAppRepository implements ConfigRepository { + constructor(private instance: Instance) {} - public async detectStorageClients( - instance: Instance - ): Promise> { - const dataStoreClient = new StorageDataStoreClient(instance); - const constantClient = new StorageConstantClient(instance); + public async detectStorageClients(): Promise> { + const dataStoreClient = new StorageDataStoreClient(this.instance); + const constantClient = new StorageConstantClient(this.instance); const dataStoreConfig = await dataStoreClient.getObject(Namespace.CONFIG); const constantConfig = await constantClient.getObject(Namespace.CONFIG); @@ -26,20 +24,17 @@ export class ConfigAppClient implements ConfigRepository { } @cache() - public async getStorageClient(instance: Instance): Promise { - const dataStoreClient = new StorageDataStoreClient(instance); - const constantClient = new StorageConstantClient(instance); + public async getStorageClient(): Promise { + const dataStoreClient = new StorageDataStoreClient(this.instance); + const constantClient = new StorageConstantClient(this.instance); const dataStoreConfig = await dataStoreClient.getObject(Namespace.CONFIG); return dataStoreConfig ? dataStoreClient : constantClient; } - public async changeStorageClient( - instance: Instance, - client: "dataStore" | "constant" - ): Promise { - const dataStoreClient = new StorageDataStoreClient(instance); - const constantClient = new StorageConstantClient(instance); + public async changeStorageClient(client: "dataStore" | "constant"): Promise { + const dataStoreClient = new StorageDataStoreClient(this.instance); + const constantClient = new StorageConstantClient(this.instance); const oldClient = client === "dataStore" ? constantClient : dataStoreClient; const newClient = client === "dataStore" ? dataStoreClient : constantClient; diff --git a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts index f2130f050..148a0bb3a 100644 --- a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts +++ b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts @@ -11,7 +11,6 @@ import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -171,7 +170,6 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts index 376e4c849..cab4c6e3d 100644 --- a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts @@ -11,7 +11,6 @@ import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -256,7 +255,6 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-events.spec.ts b/src/data/metadata/__tests__/integration/sync-events.spec.ts index 37fe21e5f..21c7b098a 100644 --- a/src/data/metadata/__tests__/integration/sync-events.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-events.spec.ts @@ -12,7 +12,6 @@ import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; import { EventsD2ApiRepository } from "../../../events/EventsD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -288,7 +287,6 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.EventsRepository, EventsD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts index 60b3989a1..9af488690 100644 --- a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts @@ -10,7 +10,6 @@ import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/Metada import { SynchronizationBuilder } from "../../../../types/synchronization"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -155,7 +154,6 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/reports/ReportsD2ApiRepository.ts b/src/data/reports/ReportsD2ApiRepository.ts index 557c8cd2f..ce2906233 100644 --- a/src/data/reports/ReportsD2ApiRepository.ts +++ b/src/data/reports/ReportsD2ApiRepository.ts @@ -1,4 +1,4 @@ -import { Instance } from "../../domain/instance/entities/Instance"; +import { ConfigRepository } from "../../domain/config/ConfigRepository"; import { SynchronizationReport, SynchronizationReportData, @@ -7,17 +7,13 @@ import { SynchronizationResult } from "../../domain/reports/entities/Synchroniza import { ReportsRepository } from "../../domain/reports/repositories/ReportsRepository"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Namespace } from "../storage/Namespaces"; -import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class ReportsD2ApiRepository implements ReportsRepository { - private storageClient: StorageClient; - - constructor(instance: Instance) { - this.storageClient = new StorageDataStoreClient(instance); - } + constructor(private configRepository: ConfigRepository) {} public async getById(id: string): Promise { - const data = await this.storageClient.getObjectInCollection( + const storageClient = await this.getStorageClient(); + const data = await storageClient.getObjectInCollection( Namespace.HISTORY, id ); @@ -26,7 +22,8 @@ export class ReportsD2ApiRepository implements ReportsRepository { } public async getSyncResults(id: string): Promise { - const data = await this.storageClient.getObject( + const storageClient = await this.getStorageClient(); + const data = await storageClient.getObject( `${Namespace.HISTORY}-${id}` ); @@ -34,7 +31,8 @@ export class ReportsD2ApiRepository implements ReportsRepository { } public async list(): Promise { - const stores = await this.storageClient.listObjectsInCollection( + const storageClient = await this.getStorageClient(); + const stores = await storageClient.listObjectsInCollection( Namespace.HISTORY ); @@ -42,18 +40,25 @@ export class ReportsD2ApiRepository implements ReportsRepository { } public async save(report: SynchronizationReport): Promise { - await this.storageClient.saveObjectInCollection( + const storageClient = await this.getStorageClient(); + + await storageClient.saveObjectInCollection( Namespace.HISTORY, report.toObject() ); - await this.storageClient.saveObject( + await storageClient.saveObject( `${Namespace.HISTORY}-${report.id}`, report.getResults() ); } public async delete(id: string): Promise { - await this.storageClient.removeObjectInCollection(Namespace.HISTORY, id); + const storageClient = await this.getStorageClient(); + await storageClient.removeObjectInCollection(Namespace.HISTORY, id); + } + + private getStorageClient(): Promise { + return this.configRepository.getStorageClient(); } } diff --git a/src/data/rules/RulesD2ApiRepository.ts b/src/data/rules/RulesD2ApiRepository.ts index b1effb34b..ebe982c7e 100644 --- a/src/data/rules/RulesD2ApiRepository.ts +++ b/src/data/rules/RulesD2ApiRepository.ts @@ -1,4 +1,4 @@ -import { Instance } from "../../domain/instance/entities/Instance"; +import { ConfigRepository } from "../../domain/config/ConfigRepository"; import { SynchronizationRule, SynchronizationRuleData, @@ -6,17 +6,13 @@ import { import { RulesRepository } from "../../domain/rules/repositories/RulesRepository"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Namespace } from "../storage/Namespaces"; -import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class RulesD2ApiRepository implements RulesRepository { - private storageClient: StorageClient; - - constructor(instance: Instance) { - this.storageClient = new StorageDataStoreClient(instance); - } + constructor(private configRepository: ConfigRepository) {} public async getById(id: string): Promise { - const data = await this.storageClient.getObjectInCollection( + const storageClient = await this.getStorageClient(); + const data = await storageClient.getObjectInCollection( Namespace.RULES, id ); @@ -25,7 +21,8 @@ export class RulesD2ApiRepository implements RulesRepository { } public async getSyncResults(id: string): Promise { - const data = await this.storageClient.getObject( + const storageClient = await this.getStorageClient(); + const data = await storageClient.getObject( `${Namespace.RULES}-${id}` ); @@ -33,7 +30,8 @@ export class RulesD2ApiRepository implements RulesRepository { } public async list(): Promise { - const stores = await this.storageClient.listObjectsInCollection( + const storageClient = await this.getStorageClient(); + const stores = await storageClient.listObjectsInCollection( Namespace.RULES ); @@ -41,13 +39,19 @@ export class RulesD2ApiRepository implements RulesRepository { } public async save(report: SynchronizationRule): Promise { - await this.storageClient.saveObjectInCollection( + const storageClient = await this.getStorageClient(); + await storageClient.saveObjectInCollection( Namespace.RULES, report.toObject() ); } public async delete(id: string): Promise { - await this.storageClient.removeObjectInCollection(Namespace.RULES, id); + const storageClient = await this.getStorageClient(); + await storageClient.removeObjectInCollection(Namespace.RULES, id); + } + + private getStorageClient(): Promise { + return this.configRepository.getStorageClient(); } } diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index 87e2ada41..d4330278e 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,32 +1,31 @@ -import { Instance } from "../../domain/instance/entities/Instance"; +import { ConfigRepository } from "../../domain/config/ConfigRepository"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; import { Namespace } from "../storage/Namespaces"; -import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class StoreD2ApiRepository implements StoreRepository { - private storageClient: StorageClient; - - constructor(instance: Instance) { - this.storageClient = new StorageDataStoreClient(instance); - } + constructor(private configRepository: ConfigRepository) {} public async list(): Promise { - const stores = await this.storageClient.listObjectsInCollection(Namespace.STORES); + const storageClient = await this.getStorageClient(); + const stores = await storageClient.listObjectsInCollection(Namespace.STORES); return stores.filter(store => !store.deleted); } public async getById(id: string): Promise { - const stores = await this.storageClient.listObjectsInCollection(Namespace.STORES); + const storageClient = await this.getStorageClient(); + const stores = await storageClient.listObjectsInCollection(Namespace.STORES); return stores.find(store => store.id === id); } public async delete(id: string): Promise { - const store = await this.storageClient.getObjectInCollection(Namespace.STORES, id); + const storageClient = await this.getStorageClient(); + + const store = await storageClient.getObjectInCollection(Namespace.STORES, id); if (!store) return false; - await this.storageClient.saveObjectInCollection(Namespace.STORES, { + await storageClient.saveObjectInCollection(Namespace.STORES, { ...store, deleted: true, }); @@ -35,7 +34,8 @@ export class StoreD2ApiRepository implements StoreRepository { } public async save(store: Store): Promise { - await this.storageClient.saveObjectInCollection(Namespace.STORES, store); + const storageClient = await this.getStorageClient(); + await storageClient.saveObjectInCollection(Namespace.STORES, store); } public async getDefault(): Promise { @@ -46,6 +46,12 @@ export class StoreD2ApiRepository implements StoreRepository { public async setDefault(id: string): Promise { const stores = await this.list(); const newStores = stores.map(store => ({ ...store, default: store.id === id })); - await this.storageClient.saveObject(Namespace.STORES, newStores); + + const storageClient = await this.getStorageClient(); + await storageClient.saveObject(Namespace.STORES, newStores); + } + + private getStorageClient(): Promise { + return this.configRepository.getStorageClient(); } } diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index a30f55be1..d3c5248fa 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -8,17 +8,15 @@ import { } from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; -import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; export function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts index d157c2582..2dfd9c065 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts @@ -287,7 +287,6 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index d4509f7d5..d9960de4b 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -3,6 +3,7 @@ import { AggregatedRepository, AggregatedRepositoryConstructor, } from "../../aggregated/repositories/AggregatedRepository"; +import { ConfigRepositoryConstructor } from "../../config/ConfigRepository"; import { EventsRepository, EventsRepositoryConstructor, @@ -18,7 +19,6 @@ import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubR import { ReportsRepositoryConstructor } from "../../reports/repositories/ReportsRepository"; import { RulesRepositoryConstructor } from "../../rules/repositories/RulesRepository"; import { DownloadRepositoryConstructor } from "../../storage/repositories/DownloadRepository"; -import { StorageRepositoryConstructor } from "../../storage/repositories/StorageClient"; import { StoreRepositoryConstructor } from "../../stores/repositories/StoreRepository"; import { TransformationRepository, @@ -54,8 +54,8 @@ export class RepositoryFactory { } @cache() - public storageRepository(instance: Instance) { - return this.get(Repositories.StorageRepository, [instance]); + public configRepository(instance: Instance) { + return this.get(Repositories.ConfigRepository, [instance]); } @cache() @@ -109,12 +109,14 @@ export class RepositoryFactory { @cache() public reportsRepository(instance: Instance) { - return this.get(Repositories.ReportsRepository, [instance]); + const config = this.configRepository(instance); + return this.get(Repositories.ReportsRepository, [config]); } @cache() public rulesRepository(instance: Instance) { - return this.get(Repositories.RulesRepository, [instance]); + const config = this.configRepository(instance); + return this.get(Repositories.RulesRepository, [config]); } } @@ -123,7 +125,7 @@ type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; export const Repositories = { InstanceRepository: "instanceRepository", StoreRepository: "storeRepository", - StorageRepository: "storageRepository", + ConfigRepository: "configRepository", DownloadRepository: "downloadRepository", GitHubRepository: "githubRepository", AggregatedRepository: "aggregatedRepository", diff --git a/src/domain/config/ConfigRepository.ts b/src/domain/config/ConfigRepository.ts index 56aa79121..d3d54327a 100644 --- a/src/domain/config/ConfigRepository.ts +++ b/src/domain/config/ConfigRepository.ts @@ -1,7 +1,11 @@ import { Instance } from "../instance/entities/Instance"; import { StorageClient } from "../storage/repositories/StorageClient"; +export interface ConfigRepositoryConstructor { + new (instance: Instance): ConfigRepository; +} + export interface ConfigRepository { - getStorageClient(instance: Instance): Promise; - changeStorageClient(instance: Instance, client: "dataStore" | "constant"): Promise; + getStorageClient(): Promise; + changeStorageClient(client: "dataStore" | "constant"): Promise; } diff --git a/src/domain/instance/usecases/DeleteInstanceUseCase.ts b/src/domain/instance/usecases/DeleteInstanceUseCase.ts index 3d0f8cda3..295de34ed 100644 --- a/src/domain/instance/usecases/DeleteInstanceUseCase.ts +++ b/src/domain/instance/usecases/DeleteInstanceUseCase.ts @@ -7,10 +7,12 @@ export class DeleteInstanceUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string): Promise { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + try { - await this.repositoryFactory - .storageRepository(this.localInstance) - .removeObjectInCollection(Namespace.INSTANCES, id); + await storageClient.removeObjectInCollection(Namespace.INSTANCES, id); } catch (error) { console.error(error); return false; diff --git a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts index ba4a75081..a744e5d82 100644 --- a/src/domain/instance/usecases/GetInstanceByIdUseCase.ts +++ b/src/domain/instance/usecases/GetInstanceByIdUseCase.ts @@ -12,9 +12,14 @@ export class GetInstanceByIdUseCase implements UseCase { ) {} public async execute(id: string): Promise> { - const data = await this.repositoryFactory - .storageRepository(this.localInstance) - .getObjectInCollection(Namespace.INSTANCES, id); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const data = await storageClient.getObjectInCollection( + Namespace.INSTANCES, + id + ); if (!data) return Either.error("NOT_FOUND"); diff --git a/src/domain/instance/usecases/ListInstancesUseCase.ts b/src/domain/instance/usecases/ListInstancesUseCase.ts index 41c552123..1a81a9171 100644 --- a/src/domain/instance/usecases/ListInstancesUseCase.ts +++ b/src/domain/instance/usecases/ListInstancesUseCase.ts @@ -16,9 +16,13 @@ export class ListInstancesUseCase implements UseCase { ) {} public async execute({ search }: ListInstancesUseCaseProps = {}): Promise { - const objects = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.INSTANCES); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); const filteredData = search ? _.filter(objects, o => diff --git a/src/domain/instance/usecases/SaveInstanceUseCase.ts b/src/domain/instance/usecases/SaveInstanceUseCase.ts index 5c2a84743..007d6532b 100644 --- a/src/domain/instance/usecases/SaveInstanceUseCase.ts +++ b/src/domain/instance/usecases/SaveInstanceUseCase.ts @@ -13,10 +13,14 @@ export class SaveInstanceUseCase implements UseCase { ) {} public async execute(instance: Instance): Promise { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + // Find for other existing instance with same name - const existingInstances = await this.repositoryFactory - .storageRepository(this.localInstance) - .getObject(Namespace.INSTANCES); + const existingInstances = await storageClient.getObject( + Namespace.INSTANCES + ); const sameNameInstance = existingInstances?.find( ({ name, id }) => id !== instance.id && name === instance.name @@ -42,9 +46,7 @@ export class SaveInstanceUseCase implements UseCase { version: await this.getVersion(instance), }; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.INSTANCES, instanceData); + await storageClient.saveObjectInCollection(Namespace.INSTANCES, instanceData); return []; } diff --git a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts index 7be4cd710..54044d628 100644 --- a/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts +++ b/src/domain/mapping/usecases/GetMappingByOwnerUseCase.ts @@ -1,18 +1,21 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance } from "../../instance/entities/Instance"; -import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore, MappingOwner } from "../entities/MappingOwner"; export class GetMappingByOwnerUseCase implements UseCase { - constructor(private storageRepository: StorageClient) {} + constructor(private repositoryFactory: RepositoryFactory, protected localInstance: Instance) {} public async execute(owner: MappingOwner): Promise { + const storageClient = await this.getStorageClient(); + if (isMappingOwnerStore(owner)) { - const mappings = await this.storageRepository.listObjectsInCollection< - DataSourceMapping - >(Namespace.MAPPINGS); + const mappings = await storageClient.listObjectsInCollection( + Namespace.MAPPINGS + ); const rawMapping = mappings.find( mapping => @@ -22,7 +25,7 @@ export class GetMappingByOwnerUseCase implements UseCase { ); if (rawMapping) { - const mappingRawWithMetadataMapping = await this.storageRepository.getObjectInCollection< + const mappingRawWithMetadataMapping = await storageClient.getObjectInCollection< DataSourceMapping >(Namespace.MAPPINGS, rawMapping?.id); @@ -33,7 +36,7 @@ export class GetMappingByOwnerUseCase implements UseCase { return undefined; } } else { - const instance = await this.storageRepository.getObjectInCollection( + const instance = await storageClient.getObjectInCollection( Namespace.INSTANCES, owner.id ); @@ -46,4 +49,8 @@ export class GetMappingByOwnerUseCase implements UseCase { : undefined; } } + + private getStorageClient(): Promise { + return this.repositoryFactory.configRepository(this.localInstance).getStorageClient(); + } } diff --git a/src/domain/mapping/usecases/SaveMappingUseCase.ts b/src/domain/mapping/usecases/SaveMappingUseCase.ts index 4983caf4a..a2885ca4a 100644 --- a/src/domain/mapping/usecases/SaveMappingUseCase.ts +++ b/src/domain/mapping/usecases/SaveMappingUseCase.ts @@ -1,7 +1,8 @@ +import { Namespace } from "../../../data/storage/Namespaces"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; -import { Namespace } from "../../../data/storage/Namespaces"; import { StorageClient } from "../../storage/repositories/StorageClient"; import { DataSourceMapping } from "../entities/DataSourceMapping"; import { isMappingOwnerStore } from "../entities/MappingOwner"; @@ -9,18 +10,17 @@ import { isMappingOwnerStore } from "../entities/MappingOwner"; export type SaveMappingError = "UNEXPECTED_ERROR" | "INSTANCE_NOT_FOUND"; export class SaveMappingUseCase implements UseCase { - constructor(private storageRepository: StorageClient) {} + constructor(private repositoryFactory: RepositoryFactory, protected localInstance: Instance) {} public async execute(mapping: DataSourceMapping): Promise> { + const storageClient = await this.getStorageClient(); + if (isMappingOwnerStore(mapping.owner)) { - await this.storageRepository.saveObjectInCollection( - Namespace.MAPPINGS, - mapping.toObject() - ); + await storageClient.saveObjectInCollection(Namespace.MAPPINGS, mapping.toObject()); return Either.success(undefined); } else { - const rawInstance = await this.storageRepository.getObjectInCollection( + const rawInstance = await storageClient.getObjectInCollection( Namespace.INSTANCES, mapping.owner.id ); @@ -34,7 +34,7 @@ export class SaveMappingUseCase implements UseCase { metadataMapping: mapping.mappingDictionary, }); - await this.storageRepository.saveObjectInCollection( + await storageClient.saveObjectInCollection( Namespace.INSTANCES, updatedInstance.toObject() ); @@ -43,4 +43,8 @@ export class SaveMappingUseCase implements UseCase { } } } + + private getStorageClient(): Promise { + return this.repositoryFactory.configRepository(this.localInstance).getStorageClient(); + } } diff --git a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts index cc6ea9382..8d205383e 100644 --- a/src/domain/metadata/usecases/GetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/GetResponsiblesUseCase.ts @@ -11,9 +11,13 @@ export class GetResponsiblesUseCase implements UseCase { ids: string[], instance = this.localInstance ): Promise { - const items = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const items = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); return items.filter(({ id }) => ids.includes(id)); } diff --git a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts index ff74bcfcb..361dd0529 100644 --- a/src/domain/metadata/usecases/ListResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/ListResponsiblesUseCase.ts @@ -9,9 +9,13 @@ export class ListResponsiblesUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(instance = this.localInstance): Promise { - const items = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const items = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); const names = await this.getDisplayNames( instance, diff --git a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts index b2ceba954..c1baf7ba5 100644 --- a/src/domain/metadata/usecases/SetResponsiblesUseCase.ts +++ b/src/domain/metadata/usecases/SetResponsiblesUseCase.ts @@ -13,14 +13,14 @@ export class SetResponsiblesUseCase implements UseCase { public async execute(responsible: MetadataResponsible): Promise { const { id, users, userGroups } = responsible; + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + if (users.length === 0 && userGroups.length === 0) { - await this.repositoryFactory - .storageRepository(this.localInstance) - .removeObjectInCollection(Namespace.RESPONSIBLES, id); + await storageClient.removeObjectInCollection(Namespace.RESPONSIBLES, id); } else { - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.RESPONSIBLES, responsible); + await storageClient.saveObjectInCollection(Namespace.RESPONSIBLES, responsible); } await this.updatePendingPullRequests(responsible); @@ -31,9 +31,13 @@ export class SetResponsiblesUseCase implements UseCase { users, userGroups, }: MetadataResponsible): Promise { - const notifications = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.NOTIFICATIONS); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const notifications = await storageClient.listObjectsInCollection< + ReceivedPullRequestNotification + >(Namespace.NOTIFICATIONS); const relatedPullRequests = notifications.filter( ({ type, selectedIds }) => type === "received-pull-request" && selectedIds.includes(id) @@ -47,9 +51,7 @@ export class SetResponsiblesUseCase implements UseCase { userGroups: _.uniqBy([...notification.userGroups, ...userGroups], "id"), }; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); + await storageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); }); } } diff --git a/src/domain/modules/usecases/DeleteModuleUseCase.ts b/src/domain/modules/usecases/DeleteModuleUseCase.ts index d9a7d73c9..483249f93 100644 --- a/src/domain/modules/usecases/DeleteModuleUseCase.ts +++ b/src/domain/modules/usecases/DeleteModuleUseCase.ts @@ -9,10 +9,12 @@ export class DeleteModuleUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + try { - await this.repositoryFactory - .storageRepository(instance) - .removeObjectInCollection(Namespace.MODULES, id); + await storageClient.removeObjectInCollection(Namespace.MODULES, id); await this.deletePackagesFromModule(id, instance); } catch (error) { return false; @@ -22,9 +24,11 @@ export class DeleteModuleUseCase implements UseCase { } private async deletePackagesFromModule(id: string, instance: Instance): Promise { - const packages = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.PACKAGES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const packages = await storageClient.listObjectsInCollection(Namespace.PACKAGES); const newPackages = packages .filter(({ module }) => module.id === id) @@ -34,9 +38,7 @@ export class DeleteModuleUseCase implements UseCase { })); await promiseMap(newPackages, async (item: BasePackage) => { - await this.repositoryFactory - .storageRepository(instance) - .saveObjectInCollection(Namespace.PACKAGES, item); + await storageClient.saveObjectInCollection(Namespace.PACKAGES, item); }); } } diff --git a/src/domain/modules/usecases/GetModuleUseCase.ts b/src/domain/modules/usecases/GetModuleUseCase.ts index 9a6dd7886..66a54aed0 100644 --- a/src/domain/modules/usecases/GetModuleUseCase.ts +++ b/src/domain/modules/usecases/GetModuleUseCase.ts @@ -9,9 +9,11 @@ export class GetModuleUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { - const module = await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.MODULES, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const module = await storageClient.getObjectInCollection(Namespace.MODULES, id); switch (module?.type) { case "metadata": diff --git a/src/domain/modules/usecases/ListModulesUseCase.ts b/src/domain/modules/usecases/ListModulesUseCase.ts index f0cbf1f8e..6a68784eb 100644 --- a/src/domain/modules/usecases/ListModulesUseCase.ts +++ b/src/domain/modules/usecases/ListModulesUseCase.ts @@ -13,6 +13,10 @@ export class ListModulesUseCase implements UseCase { instance = this.localInstance, includeAutogenerated = false ): Promise { + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + const userGroups = await this.repositoryFactory .instanceRepository(this.localInstance) .getUserGroups(); @@ -21,9 +25,7 @@ export class ListModulesUseCase implements UseCase { .getUser(); const data = ( - await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.MODULES) + await storageClient.listObjectsInCollection(Namespace.MODULES) ).filter(module => includeAutogenerated || !module.autogenerated); return data diff --git a/src/domain/modules/usecases/SaveModuleUseCase.ts b/src/domain/modules/usecases/SaveModuleUseCase.ts index 155a2b319..e6093d8a6 100644 --- a/src/domain/modules/usecases/SaveModuleUseCase.ts +++ b/src/domain/modules/usecases/SaveModuleUseCase.ts @@ -10,6 +10,10 @@ export class SaveModuleUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(module: Module): Promise { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + const validations = module.validate(); if (validations.length === 0) { @@ -36,9 +40,7 @@ export class SaveModuleUseCase implements UseCase { ), }); - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.MODULES, newModule); + await storageClient.saveObjectInCollection(Namespace.MODULES, newModule); } return validations; diff --git a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts index 69da70934..52132b18b 100644 --- a/src/domain/notifications/usecases/CancelPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/CancelPullRequestUseCase.ts @@ -24,6 +24,10 @@ export class CancelPullRequestUseCase implements UseCase { ) {} public async execute(id: string): Promise> { + const localStorageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + const notification = await this.getNotification(this.localInstance, id); if (!notification) { @@ -38,13 +42,15 @@ export class CancelPullRequestUseCase implements UseCase { status: "CANCELLED", }; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); + await localStorageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); const remoteInstance = await this.getInstanceById(notification.instance.id); if (!remoteInstance) return Either.error("INSTANCE_NOT_FOUND"); + const remoteStorageClient = await this.repositoryFactory + .configRepository(remoteInstance) + .getStorageClient(); + const remoteNotification = await this.getNotification( remoteInstance, notification.remoteNotification @@ -63,9 +69,10 @@ export class CancelPullRequestUseCase implements UseCase { payload: {}, }; - await this.repositoryFactory - .storageRepository(remoteInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, newRemoteNotification); + await remoteStorageClient.saveObjectInCollection( + Namespace.NOTIFICATIONS, + newRemoteNotification + ); return Either.success(undefined); } @@ -74,15 +81,24 @@ export class CancelPullRequestUseCase implements UseCase { instance: Instance, id: string ): Promise { - return await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.NOTIFICATIONS, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + return await storageClient.getObjectInCollection( + Namespace.NOTIFICATIONS, + id + ); } private async getInstanceById(id: string): Promise { - const objects = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.INSTANCES); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); const data = objects.find(data => data.id === id); if (!data) return undefined; diff --git a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts index ac187a2d9..24907f52b 100644 --- a/src/domain/notifications/usecases/ImportPullRequestUseCase.ts +++ b/src/domain/notifications/usecases/ImportPullRequestUseCase.ts @@ -31,6 +31,10 @@ export class ImportPullRequestUseCase implements UseCase { public async execute( notificationId: string ): Promise> { + const localStorageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + const notification = await this.getNotification(this.localInstance, notificationId); if (!notification) return Either.error("NOTIFICATION_NOT_FOUND"); if (notification.type !== "sent-pull-request") return Either.error("INVALID_NOTIFICATION"); @@ -38,6 +42,10 @@ export class ImportPullRequestUseCase implements UseCase { const remoteInstance = await this.getInstanceById(notification.instance.id); if (!remoteInstance) return Either.error("INSTANCE_NOT_FOUND"); + const remoteStorageClient = await this.repositoryFactory + .configRepository(remoteInstance) + .getStorageClient(); + const remoteNotification = await this.getNotification( remoteInstance, notification.remoteNotification @@ -59,22 +67,18 @@ export class ImportPullRequestUseCase implements UseCase { const payload = status === "IMPORTED" ? {} : remoteNotification.payload; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, { - ...notification, - read: true, - status, - }); - - await this.repositoryFactory - .storageRepository(remoteInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, { - ...remoteNotification, - read: false, - status, - payload, - }); + await localStorageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, { + ...notification, + read: true, + status, + }); + + await remoteStorageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, { + ...remoteNotification, + read: false, + status, + payload, + }); await this.sendMessage( remoteInstance, @@ -86,9 +90,13 @@ export class ImportPullRequestUseCase implements UseCase { } private async getInstanceById(id: string): Promise { - const objects = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.INSTANCES); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); const data = objects.find(data => data.id === id); if (!data) return undefined; @@ -104,9 +112,14 @@ export class ImportPullRequestUseCase implements UseCase { instance: Instance, id: string ): Promise { - return await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.NOTIFICATIONS, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + return await storageClient.getObjectInCollection( + Namespace.NOTIFICATIONS, + id + ); } private async sendMessage( @@ -144,9 +157,13 @@ export class ImportPullRequestUseCase implements UseCase { } private async getResponsibleNames(instance: Instance, ids: string[]) { - const responsibles = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const responsibles = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/notifications/usecases/ListNotificationsUseCase.ts b/src/domain/notifications/usecases/ListNotificationsUseCase.ts index 2bc0fbf69..e30e4c807 100644 --- a/src/domain/notifications/usecases/ListNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/ListNotificationsUseCase.ts @@ -43,15 +43,21 @@ export class ListNotificationsUseCase implements UseCase { } private async getInstanceNotifications(): Promise { - return this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.NOTIFICATIONS); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + return storageClient.listObjectsInCollection(Namespace.NOTIFICATIONS); } private async getInstanceById(id: string): Promise { - const objects = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.INSTANCES); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); const data = objects.find(data => data.id === id); if (!data) return undefined; @@ -66,18 +72,24 @@ export class ListNotificationsUseCase implements UseCase { private async updateSentPullRequest( notification: AppNotification ): Promise { + const localStorageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + if (notification.type !== "sent-pull-request" || notification.status !== "PENDING") return undefined; - const instance = await this.getInstanceById(notification.instance.id); - if (!instance) return undefined; + const remoteInstance = await this.getInstanceById(notification.instance.id); + if (!remoteInstance) return undefined; - const remoteNotification = await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection( - Namespace.NOTIFICATIONS, - notification.remoteNotification - ); + const remoteStorageClient = await this.repositoryFactory + .configRepository(remoteInstance) + .getStorageClient(); + + const remoteNotification = await remoteStorageClient.getObjectInCollection( + Namespace.NOTIFICATIONS, + notification.remoteNotification + ); if ( !remoteNotification || @@ -93,9 +105,7 @@ export class ListNotificationsUseCase implements UseCase { status: remoteNotification.status, }; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); + await localStorageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); return newNotification; } diff --git a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts index 25b5128d5..35690e034 100644 --- a/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts +++ b/src/domain/notifications/usecases/MarkReadNotificationsUseCase.ts @@ -9,9 +9,13 @@ export class MarkReadNotificationsUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(ids: string[], read: boolean): Promise { - const notifications = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.NOTIFICATIONS); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const notifications = await storageClient.listObjectsInCollection( + Namespace.NOTIFICATIONS + ); if (!notifications) return; const targetNotifications = notifications.filter(({ id }) => ids.includes(id)); @@ -20,12 +24,10 @@ export class MarkReadNotificationsUseCase implements UseCase { const hasPermissions = await this.hasPermissions(notification); if (!hasPermissions) return; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, { - ...notification, - read, - }); + await storageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, { + ...notification, + read, + }); }); } diff --git a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts index 6fc10edcd..e6170aa2c 100644 --- a/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts +++ b/src/domain/notifications/usecases/UpdatePullRequestStatusUseCase.ts @@ -19,9 +19,13 @@ export class UpdatePullRequestStatusUseCase implements UseCase { id: string, status: PullRequestStatus ): Promise> { - const notification = await this.repositoryFactory - .storageRepository(this.localInstance) - .getObjectInCollection(Namespace.NOTIFICATIONS, id); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const notification = await storageClient.getObjectInCollection< + ReceivedPullRequestNotification + >(Namespace.NOTIFICATIONS, id); if (!notification) { return Either.error("NOT_FOUND"); @@ -38,9 +42,7 @@ export class UpdatePullRequestStatusUseCase implements UseCase { status, }; - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); + await storageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, newNotification); return Either.success(undefined); } @@ -62,9 +64,13 @@ export class UpdatePullRequestStatusUseCase implements UseCase { } private async getResponsibles(instance: Instance, ids: string[]) { - const responsibles = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const responsibles = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts index 79218429a..347320d4e 100644 --- a/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/ListImportedPackagesUseCase.ts @@ -11,10 +11,14 @@ export class ListImportedPackagesUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(): Promise> { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + try { - const items = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.IMPORTEDPACKAGES); + const items = await storageClient.listObjectsInCollection( + Namespace.IMPORTEDPACKAGES + ); return Either.success(items); } catch (error) { diff --git a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts index 157c6768e..76bf6faae 100644 --- a/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts +++ b/src/domain/package-import/usecases/SaveImportedPackagesUseCase.ts @@ -13,10 +13,15 @@ export class SaveImportedPackagesUseCase implements UseCase { public async execute( importedPackages: ImportedPackage[] ): Promise> { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + try { - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectsInCollection(Namespace.IMPORTEDPACKAGES, importedPackages); + await storageClient.saveObjectsInCollection( + Namespace.IMPORTEDPACKAGES, + importedPackages + ); return Either.success(undefined); } catch (error) { diff --git a/src/domain/packages/usecases/CreatePackageUseCase.ts b/src/domain/packages/usecases/CreatePackageUseCase.ts index d1e0fb64e..26d0faafc 100644 --- a/src/domain/packages/usecases/CreatePackageUseCase.ts +++ b/src/domain/packages/usecases/CreatePackageUseCase.ts @@ -24,6 +24,10 @@ export class CreatePackageUseCase implements UseCase { dhisVersion: string, contents?: MetadataPackage ): Promise { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + const apiVersion = getMajorVersion(dhisVersion); const basePayload = contents @@ -52,14 +56,10 @@ export class CreatePackageUseCase implements UseCase { user: payload.user.id ? payload.user : user, }); - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.PACKAGES, newPackage); + await storageClient.saveObjectInCollection(Namespace.PACKAGES, newPackage); const newModule = module.update({ lastPackageVersion: newPackage.version }); - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.MODULES, newModule); + await storageClient.saveObjectInCollection(Namespace.MODULES, newModule); } return validations; diff --git a/src/domain/packages/usecases/DeletePackageUseCase.ts b/src/domain/packages/usecases/DeletePackageUseCase.ts index 0f83bf55f..51dea12a4 100644 --- a/src/domain/packages/usecases/DeletePackageUseCase.ts +++ b/src/domain/packages/usecases/DeletePackageUseCase.ts @@ -8,20 +8,23 @@ export class DeletePackageUseCase implements UseCase { constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string, instance = this.localInstance): Promise { + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + try { - const item = await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.PACKAGES, id); + const item = await storageClient.getObjectInCollection( + Namespace.PACKAGES, + id + ); if (!item) return false; - await this.repositoryFactory - .storageRepository(instance) - .saveObjectInCollection(Namespace.PACKAGES, { - ...item, - deleted: true, - contents: {}, - }); + await storageClient.saveObjectInCollection(Namespace.PACKAGES, { + ...item, + deleted: true, + contents: {}, + }); } catch (error) { return false; } diff --git a/src/domain/packages/usecases/DiffPackageUseCase.ts b/src/domain/packages/usecases/DiffPackageUseCase.ts index 33ab4e993..be272703f 100644 --- a/src/domain/packages/usecases/DiffPackageUseCase.ts +++ b/src/domain/packages/usecases/DiffPackageUseCase.ts @@ -25,6 +25,10 @@ export class DiffPackageUseCase implements UseCase { storeId: string | undefined, instance = this.localInstance ): Promise> { + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + const packageMerge = await this.getPackage(packageIdMerge, storeId, instance); if (!packageMerge) return Either.error("PACKAGE_NOT_FOUND"); @@ -37,9 +41,10 @@ export class DiffPackageUseCase implements UseCase { contentsBase = packageBase.contents; } else { // No package B specified, use local contents - const moduleDataMerge = await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.MODULES, packageMerge.module.id); + const moduleDataMerge = await storageClient.getObjectInCollection( + Namespace.MODULES, + packageMerge.module.id + ); if (!moduleDataMerge) return Either.error("MODULE_NOT_FOUND"); const moduleMerge = MetadataModule.build(moduleDataMerge); @@ -65,9 +70,11 @@ export class DiffPackageUseCase implements UseCase { } private async getDataStorePackage(id: string, instance: Instance) { - return this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.PACKAGES, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + return storageClient.getObjectInCollection(Namespace.PACKAGES, id); } private async getStorePackage(storeId: string, url: string) { diff --git a/src/domain/packages/usecases/DownloadPackageUseCase.ts b/src/domain/packages/usecases/DownloadPackageUseCase.ts index 6cf21cf53..5a3158b09 100644 --- a/src/domain/packages/usecases/DownloadPackageUseCase.ts +++ b/src/domain/packages/usecases/DownloadPackageUseCase.ts @@ -25,9 +25,11 @@ export class DownloadPackageUseCase implements UseCase { } private async getDataStorePackage(id: string, instance: Instance) { - return this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.PACKAGES, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + return storageClient.getObjectInCollection(Namespace.PACKAGES, id); } private async getStorePackage(storeId: string, url: string) { diff --git a/src/domain/packages/usecases/GetPackageUseCase.ts b/src/domain/packages/usecases/GetPackageUseCase.ts index 1245e46c1..f8ff20b1a 100644 --- a/src/domain/packages/usecases/GetPackageUseCase.ts +++ b/src/domain/packages/usecases/GetPackageUseCase.ts @@ -12,9 +12,11 @@ export class GetPackageUseCase implements UseCase { id: string, instance = this.localInstance ): Promise> { - const data = await this.repositoryFactory - .storageRepository(instance) - .getObjectInCollection(Namespace.PACKAGES, id); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const data = await storageClient.getObjectInCollection(Namespace.PACKAGES, id); if (data) return Either.success(Package.build(data)); else return Either.error("NOT_FOUND"); diff --git a/src/domain/packages/usecases/ListPackagesUseCase.ts b/src/domain/packages/usecases/ListPackagesUseCase.ts index 2c4b68248..fd009be16 100644 --- a/src/domain/packages/usecases/ListPackagesUseCase.ts +++ b/src/domain/packages/usecases/ListPackagesUseCase.ts @@ -12,6 +12,10 @@ export class ListPackagesUseCase implements UseCase { bypassSharingSettings = false, instance = this.localInstance ): Promise { + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + const userGroups = await this.repositoryFactory .instanceRepository(this.localInstance) .getUserGroups(); @@ -19,9 +23,7 @@ export class ListPackagesUseCase implements UseCase { .instanceRepository(this.localInstance) .getUser(); - const items = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.PACKAGES); + const items = await storageClient.listObjectsInCollection(Namespace.PACKAGES); return items .filter(({ deleted }) => !deleted) diff --git a/src/domain/packages/usecases/PublishStorePackageUseCase.ts b/src/domain/packages/usecases/PublishStorePackageUseCase.ts index dcc5ff71e..141c94871 100644 --- a/src/domain/packages/usecases/PublishStorePackageUseCase.ts +++ b/src/domain/packages/usecases/PublishStorePackageUseCase.ts @@ -23,15 +23,20 @@ export class PublishStorePackageUseCase implements UseCase { packageId: string, force = false ): Promise> { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + const defaultStore = await this.repositoryFactory .storeRepository(this.localInstance) .getDefault(); if (!defaultStore) return Either.error("DEFAULT_STORE_NOT_FOUND"); - const storedPackage = await this.repositoryFactory - .storageRepository(this.localInstance) - .getObjectInCollection(Namespace.PACKAGES, packageId); + const storedPackage = await storageClient.getObjectInCollection( + Namespace.PACKAGES, + packageId + ); if (!storedPackage) return Either.error("PACKAGE_NOT_FOUND"); const { contents, ...item } = storedPackage; diff --git a/src/domain/reports/repositories/ReportsRepository.ts b/src/domain/reports/repositories/ReportsRepository.ts index 349d7071f..d7ecda292 100644 --- a/src/domain/reports/repositories/ReportsRepository.ts +++ b/src/domain/reports/repositories/ReportsRepository.ts @@ -1,9 +1,9 @@ -import { Instance } from "../../instance/entities/Instance"; +import { ConfigRepository } from "../../config/ConfigRepository"; import { SynchronizationReport } from "../entities/SynchronizationReport"; import { SynchronizationResult } from "../entities/SynchronizationResult"; export interface ReportsRepositoryConstructor { - new (instance: Instance): ReportsRepository; + new (configRepository: ConfigRepository): ReportsRepository; } export interface ReportsRepository { diff --git a/src/domain/rules/repositories/RulesRepository.ts b/src/domain/rules/repositories/RulesRepository.ts index c638f93e7..b510183e8 100644 --- a/src/domain/rules/repositories/RulesRepository.ts +++ b/src/domain/rules/repositories/RulesRepository.ts @@ -1,8 +1,8 @@ -import { Instance } from "../../instance/entities/Instance"; +import { ConfigRepository } from "../../config/ConfigRepository"; import { SynchronizationRule } from "../entities/SynchronizationRule"; export interface RulesRepositoryConstructor { - new (instance: Instance): RulesRepository; + new (configRepository: ConfigRepository): RulesRepository; } export interface RulesRepository { diff --git a/src/domain/storage/repositories/StorageClient.ts b/src/domain/storage/repositories/StorageClient.ts index 0d5cc3573..1beb7ec40 100644 --- a/src/domain/storage/repositories/StorageClient.ts +++ b/src/domain/storage/repositories/StorageClient.ts @@ -4,7 +4,7 @@ import { Dictionary } from "../../../types/utils"; import { Ref } from "../../common/entities/Ref"; import { Instance } from "../../instance/entities/Instance"; -export interface StorageRepositoryConstructor { +export interface StorageClientConstructor { new (instance: Instance): StorageClient; } diff --git a/src/domain/storage/usecases/DownloadFileUseCase.ts b/src/domain/storage/usecases/DownloadFileUseCase.ts index 67d048c3e..8e63416a2 100644 --- a/src/domain/storage/usecases/DownloadFileUseCase.ts +++ b/src/domain/storage/usecases/DownloadFileUseCase.ts @@ -1,10 +1,10 @@ import { UseCase } from "../../common/entities/UseCase"; -import { DownloadRepository } from "../repositories/DownloadRepository"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; export class DownloadFileUseCase implements UseCase { - constructor(private downloadRepository: DownloadRepository) {} + constructor(private repositoryFactory: RepositoryFactory) {} public async execute(name: string, file: unknown) { - return this.downloadRepository.downloadFile(name, file); + return this.repositoryFactory.downloadRepository().downloadFile(name, file); } } diff --git a/src/domain/stores/usecases/DeleteStoreUseCase.ts b/src/domain/stores/usecases/DeleteStoreUseCase.ts index 746af5b45..db0800f61 100644 --- a/src/domain/stores/usecases/DeleteStoreUseCase.ts +++ b/src/domain/stores/usecases/DeleteStoreUseCase.ts @@ -1,10 +1,11 @@ import { UseCase } from "../../common/entities/UseCase"; -import { StoreRepository } from "../repositories/StoreRepository"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; export class DeleteStoreUseCase implements UseCase { - constructor(private storeRepository: StoreRepository) {} + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string): Promise { - return this.storeRepository.delete(id); + return this.repositoryFactory.storeRepository(this.localInstance).delete(id); } } diff --git a/src/domain/stores/usecases/GetStoreUseCase.ts b/src/domain/stores/usecases/GetStoreUseCase.ts index 0c73fbc12..5ea46d08d 100644 --- a/src/domain/stores/usecases/GetStoreUseCase.ts +++ b/src/domain/stores/usecases/GetStoreUseCase.ts @@ -1,11 +1,12 @@ import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; import { Store } from "../entities/Store"; -import { StoreRepository } from "../repositories/StoreRepository"; export class GetStoreUseCase implements UseCase { - constructor(private storeRepository: StoreRepository) {} + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string): Promise { - return this.storeRepository.getById(id); + return this.repositoryFactory.storeRepository(this.localInstance).getById(id); } } diff --git a/src/domain/stores/usecases/ListStoresUseCase.ts b/src/domain/stores/usecases/ListStoresUseCase.ts index dcf3df279..1c225de28 100644 --- a/src/domain/stores/usecases/ListStoresUseCase.ts +++ b/src/domain/stores/usecases/ListStoresUseCase.ts @@ -1,11 +1,12 @@ import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; import { Store } from "../entities/Store"; -import { StoreRepository } from "../repositories/StoreRepository"; export class ListStoresUseCase implements UseCase { - constructor(private storeRepository: StoreRepository) {} + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(): Promise { - return this.storeRepository.list(); + return this.repositoryFactory.storeRepository(this.localInstance).list(); } } diff --git a/src/domain/stores/usecases/SaveStoreUseCase.ts b/src/domain/stores/usecases/SaveStoreUseCase.ts index 815aa64b7..7f245b14b 100644 --- a/src/domain/stores/usecases/SaveStoreUseCase.ts +++ b/src/domain/stores/usecases/SaveStoreUseCase.ts @@ -1,27 +1,27 @@ import { generateUid } from "d2/uid"; import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; import { GitHubError } from "../../packages/entities/Errors"; -import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; import { Store } from "../entities/Store"; -import { StoreRepository } from "../repositories/StoreRepository"; export class SaveStoreUseCase implements UseCase { - constructor( - private githubRepository: GitHubRepository, - private storeRepository: StoreRepository - ) {} + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(store: Store, validate = true): Promise> { if (validate) { - const validation = await this.githubRepository.validateStore(store); + const validation = await this.repositoryFactory.gitRepository().validateStore(store); if (validation.isError()) return Either.error(validation.value.error ?? "UNKNOWN"); } - const currentStores = await this.storeRepository.list(); + const currentStores = await this.repositoryFactory + .storeRepository(this.localInstance) + .list(); + const isFirstStore = !store.id && currentStores.length === 0; - await this.storeRepository.save({ + await this.repositoryFactory.storeRepository(this.localInstance).save({ ...store, id: store.id || generateUid(), default: isFirstStore || store.default, diff --git a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts index 82feb4df6..338b5d7bb 100644 --- a/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts +++ b/src/domain/stores/usecases/SetStoreAsDefaultUseCase.ts @@ -1,17 +1,18 @@ import { Either } from "../../common/entities/Either"; import { UseCase } from "../../common/entities/UseCase"; -import { StoreRepository } from "../repositories/StoreRepository"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; type SetStoreAsDefaultError = { kind: "SetStoreAsDefaultError"; }; export class SetStoreAsDefaultUseCase implements UseCase { - constructor(private storeRepository: StoreRepository) {} + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(id: string): Promise> { try { - await this.storeRepository.setDefault(id); + await this.repositoryFactory.storeRepository(this.localInstance).setDefault(id); return Either.success(undefined); } catch { return Either.error({ diff --git a/src/domain/stores/usecases/ValidateStoreUseCase.ts b/src/domain/stores/usecases/ValidateStoreUseCase.ts index 3fde1d469..1e86fde38 100644 --- a/src/domain/stores/usecases/ValidateStoreUseCase.ts +++ b/src/domain/stores/usecases/ValidateStoreUseCase.ts @@ -1,11 +1,11 @@ import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; import { Store } from "../entities/Store"; -import { GitHubRepository } from "../../packages/repositories/GitHubRepository"; export class ValidateStoreUseCase implements UseCase { - constructor(private githubRepository: GitHubRepository) {} + constructor(private repositoryFactory: RepositoryFactory) {} public async execute(store: Store) { - return this.githubRepository.validateStore(store); + return this.repositoryFactory.gitRepository().validateStore(store); } } diff --git a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts index b3f5fc8bb..b5210178c 100644 --- a/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts +++ b/src/domain/synchronization/usecases/CreatePullRequestUseCase.ts @@ -35,6 +35,14 @@ export class CreatePullRequestUseCase implements UseCase { description = "", notificationUsers: { users, userGroups }, }: CreatePullRequestParams): Promise { + const localStorageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const remoteStorageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + const owner = await this.getOwner(); const receivedPullRequest = ReceivedPullRequestNotification.create({ @@ -61,13 +69,12 @@ export class CreatePullRequestUseCase implements UseCase { remoteNotification: receivedPullRequest.id, }); - await this.repositoryFactory - .storageRepository(instance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, receivedPullRequest); + await remoteStorageClient.saveObjectInCollection( + Namespace.NOTIFICATIONS, + receivedPullRequest + ); - await this.repositoryFactory - .storageRepository(this.localInstance) - .saveObjectInCollection(Namespace.NOTIFICATIONS, sentPullRequest); + await localStorageClient.saveObjectInCollection(Namespace.NOTIFICATIONS, sentPullRequest); await this.sendMessage(instance, receivedPullRequest); } @@ -113,9 +120,13 @@ export class CreatePullRequestUseCase implements UseCase { } private async getResponsibleNames(instance: Instance, ids: string[]) { - const responsibles = await this.repositoryFactory - .storageRepository(instance) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance) + .getStorageClient(); + + const responsibles = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); const metadataResponsibles = responsibles.filter(({ id }) => ids.includes(id)); diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index f219db1ef..e87089a72 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -170,9 +170,14 @@ export abstract class GenericSyncUseCase { } private async getInstanceById(id: string): Promise { - const data = await this.repositoryFactory - .storageRepository(this.localInstance) - .getObjectInCollection(Namespace.INSTANCES, id); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const data = await storageClient.getObjectInCollection( + Namespace.INSTANCES, + id + ); if (!data) return undefined; diff --git a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts index 65c02d2dc..872d60b73 100644 --- a/src/domain/synchronization/usecases/PrepareSyncUseCase.ts +++ b/src/domain/synchronization/usecases/PrepareSyncUseCase.ts @@ -72,17 +72,25 @@ export class PrepareSyncUseCase implements UseCase { const instance = await this.getInstanceById(instanceId); if (instance.isError() || !instance.value.data) return Either.error("INSTANCE_NOT_FOUND"); - const responsibles = await this.repositoryFactory - .storageRepository(instance.value.data) - .listObjectsInCollection(Namespace.RESPONSIBLES); + const storageClient = await this.repositoryFactory + .configRepository(instance.value.data) + .getStorageClient(); + + const responsibles = await storageClient.listObjectsInCollection( + Namespace.RESPONSIBLES + ); return Either.success(responsibles); } private async getInstanceById(id: string): Promise> { - const objects = await this.repositoryFactory - .storageRepository(this.localInstance) - .listObjectsInCollection(Namespace.INSTANCES); + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); const data = objects.find(data => data.id === id); if (!data) return Either.error("INSTANCE_NOT_FOUND"); diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 6507402f4..127d30859 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -1,4 +1,5 @@ import { AggregatedD2ApiRepository } from "../data/aggregated/AggregatedD2ApiRepository"; +import { ConfigAppRepository } from "../data/config/ConfigAppRepository"; import { EventsD2ApiRepository } from "../data/events/EventsD2ApiRepository"; import { FileD2Repository } from "../data/file/FileD2Repository"; import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepository"; @@ -8,8 +9,7 @@ import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepositor import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; -import { StorageDataStoreClient } from "../data/storage/StorageDataStoreClient"; -import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; +import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; @@ -79,12 +79,11 @@ import { ListStoresUseCase } from "../domain/stores/usecases/ListStoresUseCase"; import { SaveStoreUseCase } from "../domain/stores/usecases/SaveStoreUseCase"; import { SetStoreAsDefaultUseCase } from "../domain/stores/usecases/SetStoreAsDefaultUseCase"; import { ValidateStoreUseCase } from "../domain/stores/usecases/ValidateStoreUseCase"; +import { SynchronizationBuilder } from "../domain/synchronization/entities/SynchronizationBuilder"; import { CreatePullRequestUseCase } from "../domain/synchronization/usecases/CreatePullRequestUseCase"; import { PrepareSyncUseCase } from "../domain/synchronization/usecases/PrepareSyncUseCase"; -import { SynchronizationBuilder } from "../domain/synchronization/entities/SynchronizationBuilder"; -import { cache } from "../utils/cache"; import { GetSystemInfoUseCase } from "../domain/system-info/usecases/GetSystemInfoUseCase"; -import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; +import { cache } from "../utils/cache"; export class CompositionRoot { private repositoryFactory: RepositoryFactory; @@ -92,7 +91,7 @@ export class CompositionRoot { constructor(public readonly localInstance: Instance, private encryptionKey: string) { this.repositoryFactory = new RepositoryFactory(encryptionKey); this.repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); - this.repositoryFactory.bind(Repositories.StorageRepository, StorageDataStoreClient); + this.repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); this.repositoryFactory.bind(Repositories.DownloadRepository, DownloadWebRepository); this.repositoryFactory.bind(Repositories.GitHubRepository, GitHubOctokitRepository); this.repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); @@ -179,16 +178,13 @@ export class CompositionRoot { @cache() public get store() { - const github = new GitHubOctokitRepository(); - const storeRepository = new StoreD2ApiRepository(this.localInstance); - return getExecute({ - get: new GetStoreUseCase(storeRepository), - update: new SaveStoreUseCase(github, storeRepository), - validate: new ValidateStoreUseCase(github), - list: new ListStoresUseCase(storeRepository), - delete: new DeleteStoreUseCase(storeRepository), - setAsDefault: new SetStoreAsDefaultUseCase(storeRepository), + get: new GetStoreUseCase(this.repositoryFactory, this.localInstance), + update: new SaveStoreUseCase(this.repositoryFactory, this.localInstance), + validate: new ValidateStoreUseCase(this.repositoryFactory), + list: new ListStoresUseCase(this.repositoryFactory, this.localInstance), + delete: new DeleteStoreUseCase(this.repositoryFactory, this.localInstance), + setAsDefault: new SetStoreAsDefaultUseCase(this.repositoryFactory, this.localInstance), }); } @@ -229,10 +225,8 @@ export class CompositionRoot { @cache() public get storage() { - const download = new DownloadWebRepository(); - return getExecute({ - downloadFile: new DownloadFileUseCase(download), + downloadFile: new DownloadFileUseCase(this.repositoryFactory), }); } @@ -313,11 +307,9 @@ export class CompositionRoot { @cache() public get mapping() { - const storage = new StorageDataStoreClient(this.localInstance); - return getExecute({ - get: new GetMappingByOwnerUseCase(storage), - save: new SaveMappingUseCase(storage), + get: new GetMappingByOwnerUseCase(this.repositoryFactory, this.localInstance), + save: new SaveMappingUseCase(this.repositoryFactory, this.localInstance), apply: new ApplyMappingUseCase(this.repositoryFactory, this.localInstance), getValidIds: new GetValidMappingIdUseCase(this.repositoryFactory, this.localInstance), autoMap: new AutoMapUseCase(this.repositoryFactory, this.localInstance), From 1c78bdfce1115cf76aec148d05d552a23ae6074b Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 09:09:04 +0100 Subject: [PATCH 095/163] Allow changing to constant storage --- src/data/config/ConfigAppRepository.ts | 10 ++- src/data/reports/ReportsD2ApiRepository.ts | 2 +- src/data/rules/RulesD2ApiRepository.ts | 2 +- src/data/storage/StorageConstantClient.ts | 21 +++++- src/data/storage/StorageDataStoreClient.ts | 24 +++++- src/data/stores/StoreD2ApiRepository.ts | 2 +- .../common/factories/RepositoryFactory.ts | 2 +- .../{ => repositories}/ConfigRepository.ts | 4 +- .../usecases/GetStorageConfigUseCase.ts | 15 ++++ .../usecases/SetStorageConfigUseCase.ts | 13 ++++ .../reports/repositories/ReportsRepository.ts | 2 +- .../rules/repositories/RulesRepository.ts | 2 +- .../storage/repositories/StorageClient.ts | 2 + src/presentation/CompositionRoot.ts | 10 +++ .../core/components/dropdown/Dropdown.tsx | 18 ++--- .../core/components/migrations/Migrations.tsx | 1 - src/presentation/webapp/WebApp.tsx | 3 + .../storage/StorageSettingDropdown.tsx | 74 +++++++++---------- 18 files changed, 142 insertions(+), 65 deletions(-) rename src/domain/config/{ => repositories}/ConfigRepository.ts (65%) create mode 100644 src/domain/config/usecases/GetStorageConfigUseCase.ts create mode 100644 src/domain/config/usecases/SetStorageConfigUseCase.ts diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts index fd82f6077..ccaa3325e 100644 --- a/src/data/config/ConfigAppRepository.ts +++ b/src/data/config/ConfigAppRepository.ts @@ -1,5 +1,5 @@ import _ from "lodash"; -import { ConfigRepository } from "../../domain/config/ConfigRepository"; +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; import { Instance } from "../../domain/instance/entities/Instance"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { cache, clear } from "../../utils/cache"; @@ -39,11 +39,17 @@ export class ConfigAppRepository implements ConfigRepository { const oldClient = client === "dataStore" ? constantClient : dataStoreClient; const newClient = client === "dataStore" ? dataStoreClient : constantClient; - console.log({ oldClient, newClient }); + // TODO: Back-up everything // Clear new client + await newClient.clearStorage(); + // Copy old client data into new client + const dump = await oldClient.clone(); + await newClient.import(dump); + // Clear old client + oldClient.clearStorage(); // Reset memoize clear(this.getStorageClient, this); diff --git a/src/data/reports/ReportsD2ApiRepository.ts b/src/data/reports/ReportsD2ApiRepository.ts index ce2906233..8f04676c0 100644 --- a/src/data/reports/ReportsD2ApiRepository.ts +++ b/src/data/reports/ReportsD2ApiRepository.ts @@ -1,4 +1,4 @@ -import { ConfigRepository } from "../../domain/config/ConfigRepository"; +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; import { SynchronizationReport, SynchronizationReportData, diff --git a/src/data/rules/RulesD2ApiRepository.ts b/src/data/rules/RulesD2ApiRepository.ts index ebe982c7e..0c548c66f 100644 --- a/src/data/rules/RulesD2ApiRepository.ts +++ b/src/data/rules/RulesD2ApiRepository.ts @@ -1,4 +1,4 @@ -import { ConfigRepository } from "../../domain/config/ConfigRepository"; +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; import { SynchronizationRule, SynchronizationRuleData, diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index 0e5c061ba..c2a081c91 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -9,6 +9,8 @@ const defaultName = "MDSync Storage"; const defaultKey = "MDSYNC_STORAGE"; export class StorageConstantClient extends StorageClient { + public type = "constant" as const; + private api: D2Api; constructor(instance: Instance) { @@ -40,8 +42,21 @@ export class StorageConstantClient extends StorageClient { } public async clearStorage(): Promise { - const { id } = await this.getConstant(); - await this.api.models.constants.delete({ id }).getData(); + try { + const { objects: constants } = await this.api.models.constants + .get({ + paging: false, + fields: { id: true, code: true, name: true, description: true }, + filter: { code: { eq: defaultKey } }, + }) + .getData(); + + const { id } = constants[0] ?? {}; + + if (id) await this.api.models.constants.delete({ id }).getData(); + } catch (error) { + console.log(error); + } } public async clone(): Promise> { @@ -77,7 +92,7 @@ export class StorageConstantClient extends StorageClient { const { id = generateUid(), description } = constants[0] ?? {}; try { - const value = JSON.parse(description); + const value = description ? JSON.parse(description) : undefined; return { id, value }; } catch (error) { console.error(error); diff --git a/src/data/storage/StorageDataStoreClient.ts b/src/data/storage/StorageDataStoreClient.ts index f0a5d540c..d18da9bbe 100644 --- a/src/data/storage/StorageDataStoreClient.ts +++ b/src/data/storage/StorageDataStoreClient.ts @@ -1,12 +1,16 @@ +import _ from "lodash"; import { Instance } from "../../domain/instance/entities/Instance"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { D2Api, DataStore } from "../../types/d2-api"; import { Dictionary } from "../../types/utils"; +import { promiseMap } from "../../utils/common"; import { getD2APiFromInstance } from "../../utils/d2-utils"; const dataStoreNamespace = "metadata-synchronization"; export class StorageDataStoreClient extends StorageClient { + public type = "dataStore" as const; + private api: D2Api; private dataStore: DataStore; @@ -42,14 +46,26 @@ export class StorageDataStoreClient extends StorageClient { } public async clearStorage(): Promise { - throw new Error("Method not implemented."); + const keys = await this.dataStore.getKeys().getData(); + await promiseMap(keys, key => this.removeObject(key)); } public async clone(): Promise> { - throw new Error("Method not implemented."); + const keys = await this.dataStore.getKeys().getData(); + + const pairs = await promiseMap(keys, async key => { + const value = await this.getObject(key); + return [key, value]; + }); + + return _.fromPairs(pairs); } - public async import(_dump: Dictionary): Promise { - throw new Error("Method not implemented."); + public async import(dump: Dictionary): Promise { + const pairs = _.toPairs(dump); + + await promiseMap(pairs, async ([key, value]) => { + await this.saveObject(key, value as object); + }); } } diff --git a/src/data/stores/StoreD2ApiRepository.ts b/src/data/stores/StoreD2ApiRepository.ts index d4330278e..ac1176baa 100644 --- a/src/data/stores/StoreD2ApiRepository.ts +++ b/src/data/stores/StoreD2ApiRepository.ts @@ -1,4 +1,4 @@ -import { ConfigRepository } from "../../domain/config/ConfigRepository"; +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; import { StorageClient } from "../../domain/storage/repositories/StorageClient"; import { Store } from "../../domain/stores/entities/Store"; import { StoreRepository } from "../../domain/stores/repositories/StoreRepository"; diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index d9960de4b..66c4c314b 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -3,7 +3,7 @@ import { AggregatedRepository, AggregatedRepositoryConstructor, } from "../../aggregated/repositories/AggregatedRepository"; -import { ConfigRepositoryConstructor } from "../../config/ConfigRepository"; +import { ConfigRepositoryConstructor } from "../../config/repositories/ConfigRepository"; import { EventsRepository, EventsRepositoryConstructor, diff --git a/src/domain/config/ConfigRepository.ts b/src/domain/config/repositories/ConfigRepository.ts similarity index 65% rename from src/domain/config/ConfigRepository.ts rename to src/domain/config/repositories/ConfigRepository.ts index d3d54327a..ef5de65f2 100644 --- a/src/domain/config/ConfigRepository.ts +++ b/src/domain/config/repositories/ConfigRepository.ts @@ -1,5 +1,5 @@ -import { Instance } from "../instance/entities/Instance"; -import { StorageClient } from "../storage/repositories/StorageClient"; +import { Instance } from "../../instance/entities/Instance"; +import { StorageClient } from "../../storage/repositories/StorageClient"; export interface ConfigRepositoryConstructor { new (instance: Instance): ConfigRepository; diff --git a/src/domain/config/usecases/GetStorageConfigUseCase.ts b/src/domain/config/usecases/GetStorageConfigUseCase.ts new file mode 100644 index 000000000..8d77a8420 --- /dev/null +++ b/src/domain/config/usecases/GetStorageConfigUseCase.ts @@ -0,0 +1,15 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; + +export class GetStorageConfigUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(): Promise<"dataStore" | "constant"> { + const client = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + return client.type; + } +} diff --git a/src/domain/config/usecases/SetStorageConfigUseCase.ts b/src/domain/config/usecases/SetStorageConfigUseCase.ts new file mode 100644 index 000000000..7da3cb505 --- /dev/null +++ b/src/domain/config/usecases/SetStorageConfigUseCase.ts @@ -0,0 +1,13 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; + +export class SetStorageConfigUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(client: "dataStore" | "constant"): Promise { + await this.repositoryFactory + .configRepository(this.localInstance) + .changeStorageClient(client); + } +} diff --git a/src/domain/reports/repositories/ReportsRepository.ts b/src/domain/reports/repositories/ReportsRepository.ts index d7ecda292..816f978c6 100644 --- a/src/domain/reports/repositories/ReportsRepository.ts +++ b/src/domain/reports/repositories/ReportsRepository.ts @@ -1,4 +1,4 @@ -import { ConfigRepository } from "../../config/ConfigRepository"; +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; import { SynchronizationReport } from "../entities/SynchronizationReport"; import { SynchronizationResult } from "../entities/SynchronizationResult"; diff --git a/src/domain/rules/repositories/RulesRepository.ts b/src/domain/rules/repositories/RulesRepository.ts index b510183e8..205dd6bce 100644 --- a/src/domain/rules/repositories/RulesRepository.ts +++ b/src/domain/rules/repositories/RulesRepository.ts @@ -1,4 +1,4 @@ -import { ConfigRepository } from "../../config/ConfigRepository"; +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; import { SynchronizationRule } from "../entities/SynchronizationRule"; export interface RulesRepositoryConstructor { diff --git a/src/domain/storage/repositories/StorageClient.ts b/src/domain/storage/repositories/StorageClient.ts index 1beb7ec40..762a6027b 100644 --- a/src/domain/storage/repositories/StorageClient.ts +++ b/src/domain/storage/repositories/StorageClient.ts @@ -9,6 +9,8 @@ export interface StorageClientConstructor { } export abstract class StorageClient { + public abstract type: "constant" | "dataStore"; + // Object operations public abstract getObject(key: string): Promise; public abstract getOrCreateObject(key: string, defaultValue: T): Promise; diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 127d30859..0a913edcb 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -14,6 +14,8 @@ import { TransformationD2ApiRepository } from "../data/transformations/Transform import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; +import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageConfigUseCase"; +import { SetStorageConfigUseCase } from "../domain/config/usecases/SetStorageConfigUseCase"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; import { Instance } from "../domain/instance/entities/Instance"; @@ -337,6 +339,14 @@ export class CompositionRoot { get: new GetSyncRuleUseCase(this.repositoryFactory, this.localInstance), }); } + + @cache() + public get config() { + return getExecute({ + getStorage: new GetStorageConfigUseCase(this.repositoryFactory, this.localInstance), + setStorage: new SetStorageConfigUseCase(this.repositoryFactory, this.localInstance), + }); + } } function getExecute, Key extends keyof UseCases>( diff --git a/src/presentation/react/core/components/dropdown/Dropdown.tsx b/src/presentation/react/core/components/dropdown/Dropdown.tsx index cc5ea84be..576511807 100644 --- a/src/presentation/react/core/components/dropdown/Dropdown.tsx +++ b/src/presentation/react/core/components/dropdown/Dropdown.tsx @@ -4,19 +4,19 @@ import _ from "lodash"; import React from "react"; import i18n from "../../../../../locales"; -export interface DropdownOption { - id: string; +export interface DropdownOption { + id: T; name: string; } export type DropdownViewOption = "filter" | "inline" | "full-width"; -interface DropdownProps { - items: DropdownOption[]; +interface DropdownProps { + items: DropdownOption[]; value: string; label?: string; onChange?: Function; - onValueChange?(value: string): void; + onValueChange?(value: T): void; hideEmpty?: boolean; emptyLabel?: string; view?: DropdownViewOption; @@ -68,7 +68,7 @@ const getTheme = (view: DropdownViewOption) => { } }; -const Dropdown: React.FC = ({ +export function Dropdown({ items, value, onChange = _.noop, @@ -78,7 +78,7 @@ const Dropdown: React.FC = ({ emptyLabel, view = "filter", disabled = false, -}) => { +}: DropdownProps) { const inlineStyles = { minWidth: 120, paddingLeft: 25, paddingRight: 25 }; const styles = view === "inline" ? inlineStyles : {}; @@ -92,7 +92,7 @@ const Dropdown: React.FC = ({ value={value} onChange={e => { onChange(e); - onValueChange(e.target.value as string); + onValueChange(e.target.value as T); }} MenuProps={{ getContentAnchorEl: null, @@ -116,6 +116,6 @@ const Dropdown: React.FC = ({ ); -}; +} export default Dropdown; diff --git a/src/presentation/react/core/components/migrations/Migrations.tsx b/src/presentation/react/core/components/migrations/Migrations.tsx index f21438785..a00e60650 100644 --- a/src/presentation/react/core/components/migrations/Migrations.tsx +++ b/src/presentation/react/core/components/migrations/Migrations.tsx @@ -40,7 +40,6 @@ const Migrations: React.FC = props => { title={i18n.t("There are pending migrations")} onSave={() => (state.type === "success" ? onFinish() : startMigration())} saveText={actionText} - onCancel={undefined} disableSave={state.type === "migrating" || !actionText} maxWidth="md" fullWidth={true} diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index ab14e06b7..dd661591a 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -154,6 +154,9 @@ const App = () => { }; async function runMigrations(api: D2Api): Promise { + // TODO: Fix migrations + return { type: "checked" }; + const runner = await MigrationsRunner.init({ api, debug: debug }); if (runner.hasPendingMigrations()) { diff --git a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx index 1ccb1c663..54a44b1a4 100644 --- a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx +++ b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx @@ -1,61 +1,59 @@ -import { Icon, ListItem, ListItemIcon, ListItemText, Menu, MenuItem } from "@material-ui/core"; -import React, { useMemo, useState } from "react"; +import { Icon, ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; +import { useLoading } from "d2-ui-components"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import i18n from "../../../../../../locales"; +import Dropdown from "../../../../../react/core/components/dropdown/Dropdown"; +import { useAppContext } from "../../../../../react/core/contexts/AppContext"; export const StorageSettingDropdown: React.FC = () => { - const [anchorEl, setAnchorEl] = useState(null); - const [selectedOption, setSelectedOption] = useState("dataStore"); - - const handleClickListItem = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuItemClick = (key: string) => { - setSelectedOption(key); - setAnchorEl(null); - }; + const { compositionRoot } = useAppContext(); + const loading = useLoading(); - const handleClose = () => { - setAnchorEl(null); - }; + const [selectedOption, setSelectedOption] = useState("dataStore"); const options = useMemo( () => [ - { id: "dataStore", label: i18n.t("Data Store") }, - { id: "constant", label: i18n.t("Metadata constant") }, + { id: "dataStore" as const, name: i18n.t("Data Store") }, + { id: "constant" as const, name: i18n.t("Metadata constant") }, ], [] ); + const changeStorage = useCallback( + async (storage: "constant" | "dataStore") => { + loading.show(true, i18n.t("Updating storage location, please wait...")); + await compositionRoot.config.setStorage(storage); + + const newStorage = await compositionRoot.config.getStorage(); + setSelectedOption(newStorage); + loading.reset(); + }, + [compositionRoot, loading] + ); + + useEffect(() => { + compositionRoot.config.getStorage().then(storage => setSelectedOption(storage)); + }, [compositionRoot]); + return ( - + storage option.id === selectedOption)?.label} + secondary={ + + items={options} + value={selectedOption} + onValueChange={changeStorage} + hideEmpty={true} + view={"full-width"} + /> + } /> - - - {options.map(option => ( - handleMenuItemClick(option.id)} - > - {option.label} - - ))} - ); }; From 511062767ac679a468b355138868dbef226f127a Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 10:53:51 +0100 Subject: [PATCH 096/163] Fix most tests --- .../__tests__/integration/local-instance-mapped.spec.ts | 4 +++- .../metadata/__tests__/integration/sync-aggregated.spec.ts | 4 +++- src/data/metadata/__tests__/integration/sync-events.spec.ts | 4 +++- .../metadata/__tests__/integration/sync-metadata.spec.ts | 4 +++- src/data/transformations/__tests__/integration/helpers.ts | 2 ++ .../__tests__/integration/transformations-api-32.spec.ts | 5 +++-- src/utils/dhisServer.ts | 3 +++ 7 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts index 148a0bb3a..4517991e9 100644 --- a/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts +++ b/src/data/metadata/__tests__/integration/local-instance-mapped.spec.ts @@ -7,9 +7,10 @@ import { RepositoryFactory, } from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -170,6 +171,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts index cab4c6e3d..42c3d8671 100644 --- a/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-aggregated.spec.ts @@ -7,9 +7,10 @@ import { RepositoryFactory, } from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -255,6 +256,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-events.spec.ts b/src/data/metadata/__tests__/integration/sync-events.spec.ts index 21c7b098a..2ca69b6bf 100644 --- a/src/data/metadata/__tests__/integration/sync-events.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-events.spec.ts @@ -7,9 +7,10 @@ import { } from "../../../../domain/common/factories/RepositoryFactory"; import { EventsSyncUseCase } from "../../../../domain/events/usecases/EventsSyncUseCase"; import { Instance } from "../../../../domain/instance/entities/Instance"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; import { AggregatedD2ApiRepository } from "../../../aggregated/AggregatedD2ApiRepository"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { EventsD2ApiRepository } from "../../../events/EventsD2ApiRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; @@ -287,6 +288,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); repositoryFactory.bind(Repositories.EventsRepository, EventsD2ApiRepository); diff --git a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts index 9af488690..c0f6ebd26 100644 --- a/src/data/metadata/__tests__/integration/sync-metadata.spec.ts +++ b/src/data/metadata/__tests__/integration/sync-metadata.spec.ts @@ -7,8 +7,9 @@ import { } from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; import { MetadataD2ApiRepository } from "../../MetadataD2ApiRepository"; @@ -154,6 +155,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/transformations/__tests__/integration/helpers.ts b/src/data/transformations/__tests__/integration/helpers.ts index d3c5248fa..3c0f0900c 100644 --- a/src/data/transformations/__tests__/integration/helpers.ts +++ b/src/data/transformations/__tests__/integration/helpers.ts @@ -10,6 +10,7 @@ import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; @@ -17,6 +18,7 @@ import { TransformationD2ApiRepository } from "../../../transformations/Transfor export function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts index 2dfd9c065..95be4d338 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-32.spec.ts @@ -7,11 +7,11 @@ import { } from "../../../../domain/common/factories/RepositoryFactory"; import { Instance } from "../../../../domain/instance/entities/Instance"; import { MetadataSyncUseCase } from "../../../../domain/metadata/usecases/MetadataSyncUseCase"; -import { SynchronizationBuilder } from "../../../../types/synchronization"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import { startDhis } from "../../../../utils/dhisServer"; +import { ConfigAppRepository } from "../../../config/ConfigAppRepository"; import { InstanceD2ApiRepository } from "../../../instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../../../metadata/MetadataD2ApiRepository"; -import { StorageDataStoreClient } from "../../../storage/StorageDataStoreClient"; import { TransformationD2ApiRepository } from "../../../transformations/TransformationD2ApiRepository"; const repositoryFactory = buildRepositoryFactory(); @@ -287,6 +287,7 @@ describe("Sync metadata", () => { function buildRepositoryFactory() { const repositoryFactory: RepositoryFactory = new RepositoryFactory(""); repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); + repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); repositoryFactory.bind(Repositories.MetadataRepository, MetadataD2ApiRepository); repositoryFactory.bind(Repositories.TransformationRepository, TransformationD2ApiRepository); return repositoryFactory; diff --git a/src/utils/dhisServer.ts b/src/utils/dhisServer.ts index fc20a7f4f..6a6de3491 100644 --- a/src/utils/dhisServer.ts +++ b/src/utils/dhisServer.ts @@ -58,6 +58,9 @@ export function startDhis( })); this.get("/system/info", async () => ({ version })); this.get("/apps", async () => []); + this.get("/dataStore/metadata-synchronization/config", async () => ({ + version: 0, + })); }, }); From ed999892c510e91301da9906c0786c88e515ad70 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 10:56:13 +0100 Subject: [PATCH 097/163] Re-add removed property --- src/presentation/react/core/components/migrations/Migrations.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation/react/core/components/migrations/Migrations.tsx b/src/presentation/react/core/components/migrations/Migrations.tsx index a00e60650..f21438785 100644 --- a/src/presentation/react/core/components/migrations/Migrations.tsx +++ b/src/presentation/react/core/components/migrations/Migrations.tsx @@ -40,6 +40,7 @@ const Migrations: React.FC = props => { title={i18n.t("There are pending migrations")} onSave={() => (state.type === "success" ? onFinish() : startMigration())} saveText={actionText} + onCancel={undefined} disableSave={state.type === "migrating" || !actionText} maxWidth="md" fullWidth={true} From fb0022b4440baf4393d2ff4ef98efd90a69171a0 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 10:59:08 +0100 Subject: [PATCH 098/163] Add missing await --- src/data/config/ConfigAppRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts index ccaa3325e..46db52a54 100644 --- a/src/data/config/ConfigAppRepository.ts +++ b/src/data/config/ConfigAppRepository.ts @@ -49,7 +49,7 @@ export class ConfigAppRepository implements ConfigRepository { await newClient.import(dump); // Clear old client - oldClient.clearStorage(); + await oldClient.clearStorage(); // Reset memoize clear(this.getStorageClient, this); From cccc31f88e59499e60c0a1e1235c2b5cd88897af Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 11:05:34 +0100 Subject: [PATCH 099/163] Add confirmation dialog --- i18n/en.pot | 16 +++++++++-- i18n/es.po | 14 +++++++++- i18n/fr.po | 14 +++++++++- i18n/pt.po | 14 +++++++++- .../storage/StorageSettingDropdown.tsx | 28 +++++++++++++++++-- 5 files changed, 79 insertions(+), 7 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 79de43878..82722c7fa 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-12-09T10:20:20.683Z\n" -"PO-Revision-Date: 2020-12-09T10:20:20.683Z\n" +"POT-Creation-Date: 2020-12-14T10:05:29.488Z\n" +"PO-Revision-Date: 2020-12-14T10:05:29.488Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1649,6 +1649,18 @@ msgstr "" msgid "Metadata constant" msgstr "" +msgid "Updating storage location, please wait..." +msgstr "" + +msgid "Change storage" +msgstr "" + +msgid "" +"When changing the storage of the application, all stored information will " +"be moved to the new storage. This might take a while, please wait. Do you " +"want to proceed?" +msgstr "" + msgid "Application storage" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 689b3d36c..79a256cb5 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-12-09T10:19:19.536Z\n" +"POT-Creation-Date: 2020-12-14T10:05:29.488Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1656,6 +1656,18 @@ msgstr "" msgid "Metadata constant" msgstr "" +msgid "Updating storage location, please wait..." +msgstr "" + +msgid "Change storage" +msgstr "" + +msgid "" +"When changing the storage of the application, all stored information will be " +"moved to the new storage. This might take a while, please wait. Do you want " +"to proceed?" +msgstr "" + msgid "Application storage" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index d65d79db9..1b99267d6 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-12-09T10:19:19.536Z\n" +"POT-Creation-Date: 2020-12-14T10:05:29.488Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1652,6 +1652,18 @@ msgstr "" msgid "Metadata constant" msgstr "" +msgid "Updating storage location, please wait..." +msgstr "" + +msgid "Change storage" +msgstr "" + +msgid "" +"When changing the storage of the application, all stored information will be " +"moved to the new storage. This might take a while, please wait. Do you want " +"to proceed?" +msgstr "" + msgid "Application storage" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index d65d79db9..1b99267d6 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-12-09T10:19:19.536Z\n" +"POT-Creation-Date: 2020-12-14T10:05:29.488Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1652,6 +1652,18 @@ msgstr "" msgid "Metadata constant" msgstr "" +msgid "Updating storage location, please wait..." +msgstr "" + +msgid "Change storage" +msgstr "" + +msgid "" +"When changing the storage of the application, all stored information will be " +"moved to the new storage. This might take a while, please wait. Do you want " +"to proceed?" +msgstr "" + msgid "Application storage" msgstr "" diff --git a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx index 54a44b1a4..a1b15bf79 100644 --- a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx +++ b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx @@ -1,5 +1,5 @@ import { Icon, ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; -import { useLoading } from "d2-ui-components"; +import { ConfirmationDialog, ConfirmationDialogProps, useLoading } from "d2-ui-components"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import i18n from "../../../../../../locales"; import Dropdown from "../../../../../react/core/components/dropdown/Dropdown"; @@ -10,6 +10,7 @@ export const StorageSettingDropdown: React.FC = () => { const loading = useLoading(); const [selectedOption, setSelectedOption] = useState("dataStore"); + const [dialogProps, updateDialog] = useState(null); const options = useMemo( () => [ @@ -31,12 +32,35 @@ export const StorageSettingDropdown: React.FC = () => { [compositionRoot, loading] ); + const showConfirmationDialog = useCallback( + (storage: "constant" | "dataStore") => { + updateDialog({ + title: i18n.t("Change storage"), + description: i18n.t( + "When changing the storage of the application, all stored information will be moved to the new storage. This might take a while, please wait. Do you want to proceed?" + ), + onCancel: () => { + updateDialog(null); + }, + onSave: async () => { + updateDialog(null); + await changeStorage(storage); + }, + cancelText: i18n.t("Cancel"), + saveText: i18n.t("Proceed"), + }); + }, + [changeStorage] + ); + useEffect(() => { compositionRoot.config.getStorage().then(storage => setSelectedOption(storage)); }, [compositionRoot]); return ( + {dialogProps && } + storage @@ -47,7 +71,7 @@ export const StorageSettingDropdown: React.FC = () => { items={options} value={selectedOption} - onValueChange={changeStorage} + onValueChange={showConfirmationDialog} hideEmpty={true} view={"full-width"} /> From a5a3829dd13a90ad1c1c3a131b1d774d3cf2e158 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 14 Dec 2020 12:14:44 +0100 Subject: [PATCH 100/163] Enable select org unit by program --- i18n/en.pot | 4 ++-- .../sync-wizard/data/OrganisationUnitsSelectionStep.tsx | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 5fd0ff54b..4b7e425b2 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-12-10T12:12:12.320Z\n" -"PO-Revision-Date: 2020-12-10T12:12:12.320Z\n" +"POT-Creation-Date: 2020-12-14T10:11:59.240Z\n" +"PO-Revision-Date: 2020-12-14T10:11:59.240Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx index 30eaab3ee..dbaeba3d7 100644 --- a/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/data/OrganisationUnitsSelectionStep.tsx @@ -47,6 +47,12 @@ const OrganisationUnitsSelectionStep: React.FC = ({ syncRul rootIds={orgUnitRootIds} withElevation={false} initiallyExpanded={syncRule.dataSyncOrgUnitPaths} + controls={{ + filterByLevel: true, + filterByGroup: true, + filterByProgram: true, + selectAll: true, + }} /> ); } From 0365b364f7d91e0585fd2d2e2cd61fb9be6d1b95 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 12:46:25 +0100 Subject: [PATCH 101/163] Fix 409 updating constant --- package.json | 2 +- src/data/storage/StorageConstantClient.ts | 14 +++++++++----- yarn.lock | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 6aee30dc6..f9f66f477 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "cronstrue": "1.95.0", "cryptr": "4.0.2", "d2": "31.8.1", - "d2-api": "1.4.1", + "d2-api": "1.4.2-beta.1", "d2-manifest": "1.0.0", "d2-ui-components": "2.2.0", "file-saver": "2.0.2", diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index c2a081c91..5499b0d66 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -70,12 +70,16 @@ export class StorageConstantClient extends StorageClient { } private async updateConstant(id: string, value: T): Promise { - await this.api.models.constants + await this.api.metadata .post({ - id, - code: defaultKey, - name: defaultName, - description: JSON.stringify(value, null, 2), + constants: [ + { + id, + code: defaultKey, + name: defaultName, + description: JSON.stringify(value, null, 2), + }, + ], }) .getData(); } diff --git a/yarn.lock b/yarn.lock index 0c33982f7..d8e44bb85 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5211,10 +5211,10 @@ cypress@4.10.0: url "0.11.0" yauzl "2.10.0" -d2-api@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/d2-api/-/d2-api-1.4.1.tgz#7d12f676a5549a3c5ab2efa40cd9550924e3ec81" - integrity sha512-dxk0HzNn7kPHs2wsvtmbXVGxy5Sv9DKQjhk5VCFEHm/Kd/dddqj8lXHvhBCAxYvlFqq27G293FQqwTAl1TwB2Q== +d2-api@1.4.2-beta.1: + version "1.4.2-beta.1" + resolved "https://registry.yarnpkg.com/d2-api/-/d2-api-1.4.2-beta.1.tgz#cff7dcf3bc2784cfa5fc2113712ee1b24d3f7a84" + integrity sha512-SW456Nu8wDoqPg2f3st/AibUC5Z+3Ps/WITBDNCCtWETUmnstQNNzphuV9djzCrbi1JssPsOJNF1tUHg0/icIQ== dependencies: "@babel/runtime" "^7.5.4" "@dhis2/d2-i18n" "^1.0.5" From 19e2c90189c6a004bd80a387cd5582e01cad07bf Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 13:03:30 +0100 Subject: [PATCH 102/163] Update UI in settings page --- .../core/pages/settings/SettingsPage.tsx | 10 ++++---- .../storage/StorageSettingDropdown.tsx | 25 ++++++------------- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/src/presentation/webapp/core/pages/settings/SettingsPage.tsx b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx index f63c20c45..35965dddb 100644 --- a/src/presentation/webapp/core/pages/settings/SettingsPage.tsx +++ b/src/presentation/webapp/core/pages/settings/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { FormGroup, makeStyles } from "@material-ui/core"; +import { FormGroup, makeStyles, Paper } from "@material-ui/core"; import React from "react"; import { useHistory } from "react-router-dom"; import i18n from "../../../../../locales"; @@ -15,13 +15,13 @@ export const SettingsPage: React.FC = () => { -
    -

    {i18n.t("Storage")}

    + +

    {i18n.t("Application storage")}

    -
    +
    ); }; @@ -29,5 +29,5 @@ export const SettingsPage: React.FC = () => { const useStyles = makeStyles({ content: { margin: "1rem", marginBottom: 35, marginLeft: 0 }, title: { marginTop: 0 }, - container: { margin: "1rem" }, + container: { margin: "1rem", padding: "1rem" }, }); diff --git a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx index a1b15bf79..b3e7a6eff 100644 --- a/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx +++ b/src/presentation/webapp/core/pages/settings/storage/StorageSettingDropdown.tsx @@ -1,4 +1,3 @@ -import { Icon, ListItem, ListItemIcon, ListItemText } from "@material-ui/core"; import { ConfirmationDialog, ConfirmationDialogProps, useLoading } from "d2-ui-components"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import i18n from "../../../../../../locales"; @@ -61,23 +60,13 @@ export const StorageSettingDropdown: React.FC = () => { {dialogProps && } - - - storage - - - items={options} - value={selectedOption} - onValueChange={showConfirmationDialog} - hideEmpty={true} - view={"full-width"} - /> - } - /> - + + items={options} + value={selectedOption} + onValueChange={showConfirmationDialog} + hideEmpty={true} + view={"full-width"} + /> ); }; From d0a653c014dba883bc161378e8f94a1f7c9268c1 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 13:07:57 +0100 Subject: [PATCH 103/163] Add default value to constant --- src/data/storage/StorageConstantClient.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index 5499b0d66..96bbbeaf6 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -78,6 +78,7 @@ export class StorageConstantClient extends StorageClient { code: defaultKey, name: defaultName, description: JSON.stringify(value, null, 2), + value: 1, }, ], }) From 74ba0e82e6457e329f012789c407040911af1651 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 14 Dec 2020 13:09:53 +0100 Subject: [PATCH 104/163] Update translation files --- i18n/en.pot | 9 +++------ i18n/es.po | 7 ++----- i18n/fr.po | 7 ++----- i18n/pt.po | 7 ++----- 4 files changed, 9 insertions(+), 21 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 2379ae655..7e9dca03c 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-12-10T10:54:47.677Z\n" -"PO-Revision-Date: 2020-12-10T10:54:47.677Z\n" +"POT-Creation-Date: 2020-12-14T12:09:33.611Z\n" +"PO-Revision-Date: 2020-12-14T12:09:33.611Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1640,7 +1640,7 @@ msgstr "" msgid "Refresh" msgstr "" -msgid "Storage" +msgid "Application storage" msgstr "" msgid "Data Store" @@ -1661,9 +1661,6 @@ msgid "" "want to proceed?" msgstr "" -msgid "Application storage" -msgstr "" - msgid "Edit store" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 79a256cb5..e778c24ad 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-12-14T10:05:29.488Z\n" +"POT-Creation-Date: 2020-12-14T12:09:33.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" @@ -1647,7 +1647,7 @@ msgstr "" msgid "Refresh" msgstr "" -msgid "Storage" +msgid "Application storage" msgstr "" msgid "Data Store" @@ -1668,9 +1668,6 @@ msgid "" "to proceed?" msgstr "" -msgid "Application storage" -msgstr "" - msgid "Edit store" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 1b99267d6..f3525c4ab 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-12-14T10:05:29.488Z\n" +"POT-Creation-Date: 2020-12-14T12:09:33.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" @@ -1643,7 +1643,7 @@ msgstr "" msgid "Refresh" msgstr "" -msgid "Storage" +msgid "Application storage" msgstr "" msgid "Data Store" @@ -1664,9 +1664,6 @@ msgid "" "to proceed?" msgstr "" -msgid "Application storage" -msgstr "" - msgid "Edit store" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 1b99267d6..f3525c4ab 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-12-14T10:05:29.488Z\n" +"POT-Creation-Date: 2020-12-14T12:09:33.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" @@ -1643,7 +1643,7 @@ msgstr "" msgid "Refresh" msgstr "" -msgid "Storage" +msgid "Application storage" msgstr "" msgid "Data Store" @@ -1664,9 +1664,6 @@ msgid "" "to proceed?" msgstr "" -msgid "Application storage" -msgstr "" - msgid "Edit store" msgstr "" From a532b11b89e2a05fda3a195a558ca780c3577e87 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Mon, 14 Dec 2020 20:49:33 +0000 Subject: [PATCH 105/163] add msf variant to README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9a0443daf..38c108abd 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 core-app|data-metadata-app|module-package-app|modules-list|package-exporter +$ yarn start -p 8082 core-app|data-metadata-app|module-package-app|modules-list|package-exporter|msf-aggregate-data-app ``` 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|data-metadata-app|module-package-app|modules-list|package-exporter] +$ yarn build [all|core-app|data-metadata-app|module-package-app|modules-list|package-exporter|msf-aggregate-data-app] ``` To build the scheduler: From 8cc713374df5e1bdbf1ec70d3e4a5d21923192e8 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Tue, 15 Dec 2020 09:20:56 +0000 Subject: [PATCH 106/163] upgrade d2-ui-comp --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index f9f66f477..8f13a6068 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "d2": "31.8.1", "d2-api": "1.4.2-beta.1", "d2-manifest": "1.0.0", - "d2-ui-components": "2.2.0", + "d2-ui-components": "2.4.0-beta.1", "file-saver": "2.0.2", "font-awesome": "4.7.0", "husky": "4.2.5", diff --git a/yarn.lock b/yarn.lock index d8e44bb85..b7698806e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5252,10 +5252,10 @@ d2-manifest@1.0.0: minimist "^1.1.0" readline-sync "^1.4.1" -d2-ui-components@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.2.0.tgz#ab09f17caeda144803bbbee9dc0814395ee1b587" - integrity sha512-SCkp7RzLJHbys1E8THD8fxF0VdPexGSb3tYqqPHp4jw1dN+GOS3VqBlYWaz3riSUd22NLkK+ntmmgmfWGn4hdw== +d2-ui-components@2.4.0-beta.1: + version "2.4.0-beta.1" + resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.1.tgz#c81ccb562f5cca5c80d7a0f0ca536da7d1019bf9" + integrity sha512-xq8Zi1VeLjLRXnVtTCRb8Vfs5q2L9CoMevDvQDK9NaLx70apal6G1fQxL3k/RFEYDtS7fOrsd66PpKPNnKgS2A== dependencies: "@date-io/core" "^1.3.6" "@date-io/moment" "^1.0.2" From 961164e19e1fd0f030796ec0085852410387d977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 15 Dec 2020 10:24:41 +0100 Subject: [PATCH 107/163] Fix bug unselecting previous items to select children --- i18n/en.pot | 4 ++-- .../components/sync-wizard/common/MetadataSelectionStep.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 7e9dca03c..cb1fc1b40 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-12-14T12:09:33.611Z\n" -"PO-Revision-Date: 2020-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-15T08:29:18.609Z\n" +"PO-Revision-Date: 2020-12-15T08:29:18.609Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx index 0da8e0865..a9ed06389 100644 --- a/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/MetadataSelectionStep.tsx @@ -144,7 +144,9 @@ export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizard const children = getChildrenRows(selectedRows, model).map( ({ id }) => id ); - changeSelection(children, []); + + const newSelected = _.uniq([...syncRule.metadataIds, ...children]); + changeSelection(newSelected, []); }, icon: done_all, isActive: (selection: MetadataType[]) => { @@ -154,7 +156,7 @@ export default function MetadataSelectionStep({ syncRule, onChange }: SyncWizard }, ] : [], - [model, rows, changeSelection, syncRule.type] + [model, rows, changeSelection, syncRule.type, syncRule.metadataIds] ); if (loading || error) return null; From ae802f68235517126817f7e7840cb66695706386 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Tue, 15 Dec 2020 10:26:51 +0100 Subject: [PATCH 108/163] Fix tests --- .../__tests__/integration/transformations-api-31.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts b/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts index 7c92e4b35..0b411fd09 100644 --- a/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts +++ b/src/data/transformations/__tests__/integration/transformations-api-31.spec.ts @@ -31,6 +31,8 @@ describe("Transformations for 2.30 -> 2.31", () => { }; beforeAll(async () => { + jest.setTimeout(30000); + payload = await sync({ from: "2.30", to: "2.31", From b155bd3f02834a200a437e3c50a0f2aaf3001744 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 16 Dec 2020 18:49:29 +0100 Subject: [PATCH 109/163] Update migrations from project-monitoring --- i18n/en.pot | 16 +- i18n/es.po | 14 +- i18n/fr.po | 14 +- i18n/pt.po | 14 +- src/index.js | 5 +- src/migrations/cli.ts | 10 +- src/migrations/hooks.ts | 42 ++++++ src/migrations/index.ts | 137 ++++++++++++------ src/migrations/tasks/01.instances-by-id.ts | 8 +- src/migrations/tasks/02.rules-by-id.ts | 8 +- src/migrations/tasks/03.sync-reports.ts | 8 +- .../tasks/04.history-notifications.ts | 7 +- src/migrations/tasks/05.multiple-stores.ts | 7 +- src/migrations/tasks/06.this-instance.ts | 7 +- src/migrations/tasks/index.ts | 26 ++-- src/migrations/types.ts | 17 ++- src/migrations/utils.ts | 9 ++ src/presentation/PresentationLoader.tsx | 5 +- .../core/components/migrations/Migrations.tsx | 39 +++-- src/presentation/webapp/WebApp.tsx | 57 ++------ .../widget/{WidgetApp.jsx => WidgetApp.tsx} | 32 ++-- src/scheduler/cli.ts | 11 +- 22 files changed, 310 insertions(+), 183 deletions(-) create mode 100644 src/migrations/hooks.ts rename src/presentation/widget/{WidgetApp.jsx => WidgetApp.tsx} (78%) diff --git a/i18n/en.pot b/i18n/en.pot index cb1fc1b40..d58a8fea8 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-12-15T08:29:18.609Z\n" -"PO-Revision-Date: 2020-12-15T08:29:18.609Z\n" +"POT-Creation-Date: 2020-12-16T17:46:49.200Z\n" +"PO-Revision-Date: 2020-12-16T17:46:49.200Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -414,20 +414,20 @@ msgid "Continue to the App" msgstr "" msgid "" -"The app needs to run all pending migrations (v{{instanceVersion}} -> " -"v{{appVersion}}) in order to continue. This may take a long time, make sure " -"the process is not interrupted." +"The app needs to run pending migrations (from version {{instanceVersion}} " +"to version {{appVersion}}) in order to continue. This may take a long time, " +"make sure the process is not interrupted." msgstr "" msgid "Error" msgstr "" -msgid "Continue to app anyway" +msgid "Continue to the app anyway" msgstr "" msgid "" -"The database version (v{{instanceVersion}}) is greater than the app version " -"(v{{appVersion}}), we cannot continue. Please contact the administrator to " +"The database version ({{instanceVersion}}) is greater than the app version " +"({{appVersion}}), cannot continue. Please contact the administrator to " "update the app." msgstr "" diff --git a/i18n/es.po b/i18n/es.po index e778c24ad..8020604b5 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-16T16:59:31.687Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -415,20 +415,20 @@ msgid "Continue to the App" msgstr "" msgid "" -"The app needs to run all pending migrations (v{{instanceVersion}} -> " -"v{{appVersion}}) in order to continue. This may take a long time, make sure " -"the process is not interrupted." +"The app needs to run pending migrations (from version {{instanceVersion}} to " +"version {{appVersion}}) in order to continue. This may take a long time, " +"make sure the process is not interrupted." msgstr "" msgid "Error" msgstr "" -msgid "Continue to app anyway" +msgid "Continue to the app anyway" msgstr "" msgid "" -"The database version (v{{instanceVersion}}) is greater than the app version " -"(v{{appVersion}}), we cannot continue. Please contact the administrator to " +"The database version ({{instanceVersion}}) is greater than the app version " +"({{appVersion}}), cannot continue. Please contact the administrator to " "update the app." msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index f3525c4ab..b7505cc01 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-16T16:59:31.687Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -415,20 +415,20 @@ msgid "Continue to the App" msgstr "" msgid "" -"The app needs to run all pending migrations (v{{instanceVersion}} -> " -"v{{appVersion}}) in order to continue. This may take a long time, make sure " -"the process is not interrupted." +"The app needs to run pending migrations (from version {{instanceVersion}} to " +"version {{appVersion}}) in order to continue. This may take a long time, " +"make sure the process is not interrupted." msgstr "" msgid "Error" msgstr "" -msgid "Continue to app anyway" +msgid "Continue to the app anyway" msgstr "" msgid "" -"The database version (v{{instanceVersion}}) is greater than the app version " -"(v{{appVersion}}), we cannot continue. Please contact the administrator to " +"The database version ({{instanceVersion}}) is greater than the app version " +"({{appVersion}}), cannot continue. Please contact the administrator to " "update the app." msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index f3525c4ab..b7505cc01 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-16T16:59:31.687Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -415,20 +415,20 @@ msgid "Continue to the App" msgstr "" msgid "" -"The app needs to run all pending migrations (v{{instanceVersion}} -> " -"v{{appVersion}}) in order to continue. This may take a long time, make sure " -"the process is not interrupted." +"The app needs to run pending migrations (from version {{instanceVersion}} to " +"version {{appVersion}}) in order to continue. This may take a long time, " +"make sure the process is not interrupted." msgstr "" msgid "Error" msgstr "" -msgid "Continue to app anyway" +msgid "Continue to the app anyway" msgstr "" msgid "" -"The database version (v{{instanceVersion}}) is greater than the app version " -"(v{{appVersion}}), we cannot continue. Please contact the administrator to " +"The database version ({{instanceVersion}}) is greater than the app version " +"({{appVersion}}), cannot continue. Please contact the administrator to " "update the app." msgstr "" diff --git a/src/index.js b/src/index.js index df7967079..121365c50 100644 --- a/src/index.js +++ b/src/index.js @@ -32,8 +32,9 @@ const configI18n = ({ keyUiLocale }) => { async function main() { const baseUrl = await getBaseUrl(); + const api = new D2Api({ baseUrl, backend: "fetch" }); + try { - const api = new D2Api({ baseUrl, backend: "fetch" }); const userSettings = await api.get("/userSettings").getData(); if (typeof userSettings === "string") throw new Error("User needs to log in"); configI18n(userSettings); @@ -55,7 +56,7 @@ async function main() { try { ReactDOM.render( - + , document.getElementById("root") ); diff --git a/src/migrations/cli.ts b/src/migrations/cli.ts index 08f28baaa..c5a7cb19f 100644 --- a/src/migrations/cli.ts +++ b/src/migrations/cli.ts @@ -1,13 +1,17 @@ import { D2Api } from "../types/d2-api"; -import { debug } from "../utils/debug"; import { MigrationsRunner } from "./index"; -import { migrationTasks } from "./tasks"; +import { getMigrationTasks } from "./tasks"; async function main() { const [baseUrl] = process.argv.slice(2); if (!baseUrl) throw new Error("Usage: index.ts DHIS2_URL"); const api = new D2Api({ baseUrl: baseUrl, backend: "fetch" }); - const runner = await MigrationsRunner.init({ api, debug, migrations: migrationTasks }); + const runner = await MigrationsRunner.init({ + api, + debug: console.debug, + migrations: await getMigrationTasks(), + dataStoreNamespace: "metadata-synchronization", + }); runner.execute(); } diff --git a/src/migrations/hooks.ts b/src/migrations/hooks.ts new file mode 100644 index 000000000..7f6c34214 --- /dev/null +++ b/src/migrations/hooks.ts @@ -0,0 +1,42 @@ +import { D2Api } from "d2-api/2.30"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { MigrationsRunner } from "./index"; +import { getMigrationTasks } from "./tasks"; + +export type MigrationsState = + | { type: "checking" } + | { type: "pending"; runner: MigrationsRunner } + | { type: "checked" }; + +export interface UseMigrationsResult { + state: MigrationsState; + onFinish: () => void; +} + +export function useMigrations(api: D2Api, dataStoreNamespace: string): UseMigrationsResult { + const [state, setState] = useState({ type: "checking" }); + const onFinish = useCallback(() => setState({ type: "checked" }), [setState]); + + useEffect(() => { + runMigrations(api, dataStoreNamespace).then(setState); + }, [api, dataStoreNamespace]); + + const result = useMemo(() => ({ state, onFinish }), [state, onFinish]); + + return result; +} + +async function runMigrations(api: D2Api, dataStoreNamespace: string): Promise { + const runner = await MigrationsRunner.init({ + api, + debug: console.log, + migrations: await getMigrationTasks(), + dataStoreNamespace, + }); + + if (runner.hasPendingMigrations()) { + return { type: "pending", runner }; + } else { + return { type: "checked" }; + } +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index a988e83cb..53bc3c5de 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -1,26 +1,24 @@ import _ from "lodash"; -import { - dataStoreNamespace, - deleteDataStore, - getDataStore, - saveDataStore, -} from "../models/dataStore"; import { D2Api } from "../types/d2-api"; import { promiseMap } from "../utils/common"; -import { Config, Debug, Migration, RunnerOptions } from "./types"; -import { migrationTasks } from "./tasks"; +import { Config, Debug, MigrationWithVersion, RunnerOptions } from "./types"; +import { zeroPad } from "./utils"; + +const configKey = "config"; export class MigrationsRunner { - public migrations: Migration[]; + public migrations: MigrationWithVersion[]; public debug: Debug; public appVersion: number; private backupPrefix = "backup-"; + private namespace: string; constructor(private api: D2Api, private config: Config, private options: RunnerOptions) { - const { debug = _.identity, migrations = migrationTasks } = options; + const { debug = _.identity, migrations } = options; this.appVersion = _.max(migrations.map(info => info.version)) || 0; this.debug = debug; this.migrations = this.getMigrationToApply(migrations, config); + this.namespace = options.dataStoreNamespace; } setDebug(debug: Debug) { @@ -30,7 +28,9 @@ export class MigrationsRunner { static async init(options: RunnerOptions): Promise { const { api } = options; - const config = await getDataStore(api, "config", { version: 0 }); + const config = await getDataStore(api, options.dataStoreNamespace, configKey, { + version: 0, + }); return new MigrationsRunner(api, config, options); } @@ -48,48 +48,48 @@ export class MigrationsRunner { return; } - debug(`Migrate: v${this.instanceVersion} -> v${this.appVersion}`); - - await this.rollBackExistingBackup(); - await this.backupDataStore(); - - try { - await this.runMigrations(migrations); - } catch (error) { - await this.rollbackDataStore(error); - throw error; - } + debug(`Migrate: version ${this.instanceVersion} to version ${this.appVersion}`); - await this.deleteBackup(); + await this.runMigrations(migrations); } - async runMigrations(migrations: Migration[]): Promise { - const { api, debug, config } = this; + async runMigrations(migrations: MigrationWithVersion[]): Promise { + const { api, debug, config, namespace } = this; const configWithCurrentMigration: Config = { ...config, migration: { version: this.appVersion }, }; - await saveDataStore(api, "config", configWithCurrentMigration); + await saveDataStore(api, namespace, configKey, configWithCurrentMigration); + + await checkCurrentUserIsSuperadmin(api, debug); for (const migration of migrations) { - debug(`Apply migration ${migration.version}: ${migration.name}`); - await migration.fn(api, debug); + debug(`Apply migration ${zeroPad(migration.version, 2)} - ${migration.name}`); + try { + await migration.migrate(api, debug); + } catch (error) { + const errorMsg = `${migration.name}: ${error.message}`; + await this.saveConfig({ errorMsg }); + throw error; + } } const newConfig = { version: this.appVersion }; - await saveDataStore(api, "config", newConfig); + await saveDataStore(api, namespace, configKey, newConfig); return newConfig; } + // dataStore backup methods are currently unused, call only if a migration needs it. + async deleteBackup() { try { - const { debug, api } = this; + const { debug, api, namespace } = this; const backupKeys = await this.getBackupKeys(); debug(`Delete backup entries`); await promiseMap(backupKeys, async backupKey => { - await deleteDataStore(api, backupKey); + await deleteDataStore(api, namespace, backupKey); }); } catch (err) { this.debug(`Error deleting backup (non-fatal)`); @@ -103,24 +103,24 @@ export class MigrationsRunner { } async backupDataStore() { - const { api, debug } = this; + const { api, debug, namespace } = this; debug(`Backup data store`); const allKeys = await this.getDataStoreKeys(); const keysToBackup = _(allKeys) .reject(key => key.startsWith(this.backupPrefix)) - .difference(["config"]) + .difference([configKey]) .compact() .value(); await promiseMap(keysToBackup, async key => { - const value = await getDataStore(api, key, {}); + const value = await getDataStore(api, namespace, key, {}); const backupKey = this.backupPrefix + key; - await saveDataStore(api, backupKey, value); + await saveDataStore(api, namespace, backupKey, value); }); } async getDataStoreKeys(): Promise { - return this.api.dataStore(dataStoreNamespace).getKeys().getData(); + return this.api.dataStore(this.options.dataStoreNamespace).getKeys().getData(); } async getBackupKeys() { @@ -129,7 +129,7 @@ export class MigrationsRunner { } async rollbackDataStore(error: Error): Promise { - const { api, debug, config } = this; + const { api, debug, config, namespace } = this; const errorMsg = error.message || error.toString(); const keysToRestore = await this.getBackupKeys(); @@ -139,21 +139,26 @@ export class MigrationsRunner { debug("Start rollback"); await promiseMap(keysToRestore, async backupKey => { - const value = await getDataStore(api, backupKey, {}); + const value = await getDataStore(api, namespace, backupKey, {}); const key = backupKey.replace(/^backup-/, ""); - await saveDataStore(api, key, value); - await deleteDataStore(api, backupKey); + await saveDataStore(api, namespace, key, value); + await deleteDataStore(api, namespace, backupKey); }); - const configWithCurrentMigration: Config = { + return this.saveConfig({ errorMsg }); + } + + private async saveConfig(options: { errorMsg?: string } = {}) { + const { errorMsg } = options; + const newConfig: Config = { ...this.config, migration: { version: this.appVersion, error: errorMsg }, }; - await saveDataStore(api, "config", configWithCurrentMigration); - return configWithCurrentMigration; + await saveDataStore(this.api, this.namespace, configKey, newConfig); + return newConfig; } - getMigrationToApply(allMigrations: Migration[], config: Config) { + getMigrationToApply(allMigrations: MigrationWithVersion[], config: Config) { return _(allMigrations) .filter(info => info.version > config.version) .sortBy(info => info.version) @@ -168,3 +173,47 @@ export class MigrationsRunner { return this.config.version; } } + +async function checkCurrentUserIsSuperadmin(api: D2Api, debug: Debug) { + debug("Check that current user is superadmin"); + const currentUser = await api.currentUser.get({ fields: { authorities: true } }).getData(); + + if (!currentUser.authorities.includes("ALL")) + throw new Error("Only a user with authority ALL can run this migration"); +} + +export async function getDataStore( + api: D2Api, + dataStoreNamespace: string, + dataStoreKey: string, + defaultValue: T +): Promise { + const dataStore = api.dataStore(dataStoreNamespace); + const value = await dataStore.get(dataStoreKey).getData(); + if (!value) await dataStore.save(dataStoreKey, defaultValue).getData(); + return value ?? defaultValue; +} + +export async function saveDataStore( + api: D2Api, + dataStoreNamespace: string, + dataStoreKey: string, + value: any +): Promise { + const dataStore = api.dataStore(dataStoreNamespace); + await dataStore.save(dataStoreKey, value).getData(); +} + +export async function deleteDataStore( + api: D2Api, + dataStoreNamespace: string, + dataStoreKey: string +): Promise { + try { + await api.delete(`/dataStore/${dataStoreNamespace}/${dataStoreKey}`).getData(); + } catch (error) { + if (!error.response || error.response.status !== 404) { + throw error; + } + } +} diff --git a/src/migrations/tasks/01.instances-by-id.ts b/src/migrations/tasks/01.instances-by-id.ts index c7b695171..aad956a4e 100644 --- a/src/migrations/tasks/01.instances-by-id.ts +++ b/src/migrations/tasks/01.instances-by-id.ts @@ -3,7 +3,7 @@ import { getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; import { Maybe } from "../../types/utils"; import { promiseMap } from "../../utils/common"; -import { Debug } from "../types"; +import { Debug, Migration } from "../types"; import { getDuplicatedIds } from "../utils"; interface InstanceOld { @@ -26,7 +26,7 @@ interface InstanceDetailsNew { metadataMapping: MetadataMappingDictionary; } -export default async function migrate(api: D2Api, debug: Debug): Promise { +async function migrate(api: D2Api, debug: Debug): Promise { const oldInstances = await getDataStore(api, "instances", []); const newInstances: InstanceNew[] = oldInstances.map(ins => _.omit(ins, ["metadataMapping"])); const duplicatedIds = getDuplicatedIds(oldInstances); @@ -45,3 +45,7 @@ export default async function migrate(api: D2Api, debug: Debug): Promise { debug(`Save main instances object`); await saveDataStore(api, "instances", newInstances); } + +const migration: Migration = { name: "Update instance ids", migrate }; + +export default migration; diff --git a/src/migrations/tasks/02.rules-by-id.ts b/src/migrations/tasks/02.rules-by-id.ts index 23d7855d2..61595560b 100644 --- a/src/migrations/tasks/02.rules-by-id.ts +++ b/src/migrations/tasks/02.rules-by-id.ts @@ -3,7 +3,7 @@ import { getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api, Id } from "../../types/d2-api"; import { Maybe } from "../../types/utils"; import { promiseMap } from "../../utils/common"; -import { Debug } from "../types"; +import { Debug, Migration } from "../types"; import { getDuplicatedIds } from "../utils"; type SynchronizationBuilder = { targetInstances: Id[] }; @@ -20,7 +20,7 @@ interface SynchronizationRuleDetailsNew { builder: SynchronizationBuilder; } -export default async function migrate(api: D2Api, debug: Debug): Promise { +async function migrate(api: D2Api, debug: Debug): Promise { const oldRules = await getDataStore(api, "rules", []); const newRules: SynchronizationRuleNew[] = oldRules.map(oldRule => ({ ..._.omit(oldRule, ["builder"]), @@ -42,3 +42,7 @@ export default async function migrate(api: D2Api, debug: Debug): Promise { debug(`Save main sync rules object`); await saveDataStore(api, "rules", newRules); } + +const migration: Migration = { name: "Update sync rules ids", migrate }; + +export default migration; diff --git a/src/migrations/tasks/03.sync-reports.ts b/src/migrations/tasks/03.sync-reports.ts index 2b56575d6..2a4735a8d 100644 --- a/src/migrations/tasks/03.sync-reports.ts +++ b/src/migrations/tasks/03.sync-reports.ts @@ -1,7 +1,7 @@ import { SynchronizationReportData } from "../../domain/reports/entities/SynchronizationReport"; import { getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; -import { Debug } from "../types"; +import { Debug, Migration } from "../types"; export interface SynchronizationResultOld { status: "PENDING" | "SUCCESS" | "WARNING" | "ERROR" | "NETWORK ERROR"; @@ -81,7 +81,7 @@ interface SynchronizationResultNew { }[]; } -export default async function migrate(api: D2Api, debug: Debug): Promise { +async function migrate(api: D2Api, debug: Debug): Promise { const dataStoreKeys = await api.dataStore("metadata-synchronization").getKeys().getData(); const notificationKeys = dataStoreKeys @@ -131,3 +131,7 @@ export default async function migrate(api: D2Api, debug: Debug): Promise { await saveDataStore(api, `notifications-${notification}`, newNotification); } } + +const migration: Migration = { name: "Update sync reports", migrate }; + +export default migration; diff --git a/src/migrations/tasks/04.history-notifications.ts b/src/migrations/tasks/04.history-notifications.ts index 2d7535944..dc308e68e 100644 --- a/src/migrations/tasks/04.history-notifications.ts +++ b/src/migrations/tasks/04.history-notifications.ts @@ -2,8 +2,9 @@ import { SynchronizationReportData } from "../../domain/reports/entities/Synchro import { deleteDataStore, getDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; import { promiseMap } from "../../utils/common"; +import { Migration } from "../types"; -export default async function migrate(api: D2Api): Promise { +async function migrate(api: D2Api): Promise { const dataStoreKeys = await api.dataStore("metadata-synchronization").getKeys().getData(); const notificationKeys = dataStoreKeys.filter(key => key.startsWith("notifications")); @@ -15,3 +16,7 @@ export default async function migrate(api: D2Api): Promise { await deleteDataStore(api, key); }); } + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/migrations/tasks/05.multiple-stores.ts b/src/migrations/tasks/05.multiple-stores.ts index 61fdc1bfa..6cbfe27d6 100644 --- a/src/migrations/tasks/05.multiple-stores.ts +++ b/src/migrations/tasks/05.multiple-stores.ts @@ -2,8 +2,9 @@ import { generateUid } from "d2/uid"; import { Store } from "../../domain/stores/entities/Store"; import { deleteDataStore, saveDataStore } from "../../models/dataStore"; import { D2Api } from "../../types/d2-api"; +import { Migration } from "../types"; -export default async function migrate(api: D2Api): Promise { +export async function migrate(api: D2Api): Promise { const oldKey = "store"; const newKey = "stores"; @@ -17,3 +18,7 @@ export default async function migrate(api: D2Api): Promise { await deleteDataStore(api, oldKey); } } + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/migrations/tasks/06.this-instance.ts b/src/migrations/tasks/06.this-instance.ts index 24642c91f..9cce71d26 100644 --- a/src/migrations/tasks/06.this-instance.ts +++ b/src/migrations/tasks/06.this-instance.ts @@ -1,8 +1,9 @@ import _ from "lodash"; import { Instance, InstanceData } from "../../domain/instance/entities/Instance"; import { D2Api } from "../../types/d2-api"; +import { Migration } from "../types"; -export default async function migrate(api: D2Api): Promise { +export async function migrate(api: D2Api): Promise { const dataStore = api.dataStore("metadata-synchronization"); const oldContents = await dataStore.get("instances").getData(); if (!oldContents) return; @@ -28,3 +29,7 @@ export default async function migrate(api: D2Api): Promise { await dataStore.save("instances", instances).getData(); } + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/migrations/tasks/index.ts b/src/migrations/tasks/index.ts index 3f160ad91..0173fe2da 100644 --- a/src/migrations/tasks/index.ts +++ b/src/migrations/tasks/index.ts @@ -1,16 +1,12 @@ -import { Migration } from "../types"; -import Migration01 from "./01.instances-by-id"; -import Migration02 from "./02.rules-by-id"; -import Migration03 from "./03.sync-reports"; -import Migration04 from "./04.history-notifications"; -import Migration05 from "./05.multiple-stores"; -import Migration06 from "./06.this-instance"; +import { MigrationTasks, migration } from "../types"; -export const migrationTasks: Migration[] = [ - { version: 1, name: "01.instances-by-id", fn: Migration01 }, - { version: 2, name: "02.rules-by-id", fn: Migration02 }, - { version: 3, name: "03.sync-reports", fn: Migration03 }, - { version: 4, name: "04.history-notifications", fn: Migration04 }, - { version: 5, name: "05.multiple-stores", fn: Migration05 }, - { version: 6, name: "06.this-instance", fn: Migration06 }, -]; +export async function getMigrationTasks(): Promise { + return [ + migration(1, (await import("./01.instances-by-id")).default), + migration(2, (await import("./02.rules-by-id")).default), + migration(3, (await import("./03.sync-reports")).default), + migration(4, (await import("./04.history-notifications")).default), + migration(5, (await import("./05.multiple-stores")).default), + migration(6, (await import("./06.this-instance")).default), + ]; +} diff --git a/src/migrations/types.ts b/src/migrations/types.ts index c47ebcf79..c2fa4ea1a 100644 --- a/src/migrations/types.ts +++ b/src/migrations/types.ts @@ -7,12 +7,25 @@ export interface Config { export type Debug = (message: string) => void; -export type Migration = { version: number; fn: MigrationFn; name: string }; +export interface MigrationWithVersion { + version: number; + migrate: MigrationFn; + name: string; +} + +export type Migration = Omit; export type MigrationFn = (api: D2Api, debug: Debug) => Promise; export interface RunnerOptions { api: D2Api; debug?: Debug; - migrations?: Migration[]; + dataStoreNamespace: string; + migrations: MigrationWithVersion[]; +} + +export type MigrationTasks = MigrationWithVersion[]; + +export function migration(version: number, migration: Migration): MigrationWithVersion { + return { version, ...migration }; } diff --git a/src/migrations/utils.ts b/src/migrations/utils.ts index d33a78091..f43e89b49 100644 --- a/src/migrations/utils.ts +++ b/src/migrations/utils.ts @@ -9,3 +9,12 @@ export function getDuplicatedIds(objects: Obj[]): string[] { .keys() .value(); } + +export function zeroPad(num: number, places: number) { + const zero = places - num.toString().length + 1; + return Array(+(zero > 0 && zero)).join("0") + num; +} + +export function enumerate(xs: T[]): Array<[number, T]> { + return xs.map((x, idx) => [idx, x]); +} diff --git a/src/presentation/PresentationLoader.tsx b/src/presentation/PresentationLoader.tsx index 8b926df0c..fb5cd4686 100644 --- a/src/presentation/PresentationLoader.tsx +++ b/src/presentation/PresentationLoader.tsx @@ -1,4 +1,5 @@ import React, { Suspense } from "react"; +import { D2Api } from "../types/d2-api"; const App = React.lazy(() => { switch (process.env.REACT_APP_PRESENTATION_TYPE) { @@ -11,10 +12,10 @@ const App = React.lazy(() => { } }); -export const PresentationLoader: React.FC = () => { +export const PresentationLoader: React.FC<{ api: D2Api }> = ({ api }) => { return ( - + ); }; diff --git a/src/presentation/react/core/components/migrations/Migrations.tsx b/src/presentation/react/core/components/migrations/Migrations.tsx index f21438785..c37efa852 100644 --- a/src/presentation/react/core/components/migrations/Migrations.tsx +++ b/src/presentation/react/core/components/migrations/Migrations.tsx @@ -2,22 +2,38 @@ import { ConfirmationDialog } from "d2-ui-components"; import React, { useCallback, useEffect, useState } from "react"; import i18n from "../../../../../locales"; import { MigrationsRunner } from "../../../../../migrations"; +import { UseMigrationsResult } from "../../../../../migrations/hooks"; export interface MigrationsProps { + migrations: UseMigrationsResult; +} + +export interface MigrationsRunnerProps { runner: MigrationsRunner; onFinish: () => void; } -type State = +const Migrations: React.FC = props => { + const { state, onFinish } = props.migrations; + + if (state.type === "checking" || state.type === "checked") { + return null; + } else { + return ; + } +}; + +type DialogState = | { type: "show-info" } | { type: "app-out-of-date" } | { type: "migrating" } | { type: "success" }; -const Migrations: React.FC = props => { +const MigrationsDialog: React.FC = props => { const { runner, onFinish } = props; const [messages, setMessages] = useState([]); - const [state, setState] = useState(getInitialState(runner)); + + const [state, setState] = useState(getInitialState(runner)); useEffect(followContents, [messages]); const debug = useCallback((message: string) => { @@ -69,16 +85,17 @@ const Migrations: React.FC = props => { function runMigrations( runner: MigrationsRunner, debug: (message: string) => void, - setState: React.Dispatch> -): Promise { + setState: React.Dispatch> +): Promise { setState({ type: "migrating" }); return runner .setDebug(debug) .execute() .then(() => ({ type: "success" as const })) - .catch(() => { + .catch(err => { debug("---"); + debug(`Error: ${err.message}`); debug( i18n.t( "There has been an error. You can either retry or contact your administrator if you think there has been an un recoverable error" @@ -94,7 +111,7 @@ function followContents() { if (divEl) divEl.scrollTop = divEl.scrollHeight; } -function getActionText(state: State): string | undefined { +function getActionText(state: DialogState): string | undefined { switch (state.type) { case "show-info": return i18n.t("Migrate instance"); @@ -107,7 +124,7 @@ function getActionText(state: State): string | undefined { } } -function getInitialState(runner: MigrationsRunner): State { +function getInitialState(runner: MigrationsRunner): DialogState { if (runner.instanceVersion === runner.appVersion) { return { type: "success" }; } else if (runner.instanceVersion > runner.appVersion) { @@ -119,7 +136,7 @@ function getInitialState(runner: MigrationsRunner): State { function getPendingMigrationsText(runner: MigrationsRunner): string { return i18n.t( - "The app needs to run all pending migrations (v{{instanceVersion}} -> v{{appVersion}}) in order to continue. This may take a long time, make sure the process is not interrupted.", + "The app needs to run pending migrations (from version {{instanceVersion}} to version {{appVersion}}) in order to continue. This may take a long time, make sure the process is not interrupted.", runner ); } @@ -134,12 +151,12 @@ const MigrationsError: React.FC<{ runner: MigrationsRunner; onFinish: () => void isOpen={true} title={i18n.t("Error")} onSave={isDebug ? onFinish : undefined} - saveText={i18n.t("Continue to app anyway")} + saveText={i18n.t("Continue to the app anyway")} maxWidth="md" fullWidth={true} > {i18n.t( - "The database version (v{{instanceVersion}}) is greater than the app version (v{{appVersion}}), we cannot continue. Please contact the administrator to update the app.", + "The database version ({{instanceVersion}}) is greater than the app version ({{appVersion}}), cannot continue. Please contact the administrator to update the app.", runner )}
    diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index dd661591a..e2f249511 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -10,16 +10,15 @@ import _ from "lodash"; import OldMuiThemeProvider from "material-ui/styles/MuiThemeProvider"; import React, { useEffect, useState } from "react"; import { Instance } from "../../domain/instance/entities/Instance"; -import { MigrationsRunner } from "../../migrations"; +import { useMigrations } from "../../migrations/hooks"; import { D2Api } from "../../types/d2-api"; -import { debug } from "../../utils/debug"; import { initializeAppRoles } from "../../utils/permissions"; -import { AppContext } from "../react/core/contexts/AppContext"; -import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; -import { muiTheme } from "../react/core/themes/dhis2.theme"; import { CompositionRoot } from "../CompositionRoot"; import Migrations from "../react/core/components/migrations/Migrations"; import Share from "../react/core/components/share/Share"; +import { AppContext } from "../react/core/contexts/AppContext"; +import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; +import { muiTheme } from "../react/core/themes/dhis2.theme"; import Root from "./Root"; import "./WebApp.css"; @@ -70,19 +69,11 @@ function initFeedbackTool(d2: unknown, appConfig: AppConfig): void { } } -type MigrationState = - | { - type: "checking" | "checked"; - } - | { - type: "pending"; - runner: MigrationsRunner; - }; - -const App = () => { +const App: React.FC<{ api: D2Api }> = ({ api }) => { const { baseUrl } = useConfig(); + const migrations = useMigrations(api, "metadata-synchronization"); + const [appContext, setAppContext] = useState(null); - const [migrationsState, setMigrationsState] = useState({ type: "checking" }); const [showShareButton, setShowShareButton] = useState(false); const appTitle = process.env.REACT_APP_PRESENTATION_TITLE; @@ -97,7 +88,6 @@ const App = () => { if (!encryptionKey) throw new Error("You need to provide a valid encryption key"); const d2 = await init({ baseUrl: `${baseUrl}/api` }); - const api = new D2Api({ baseUrl, backend: "fetch" }); const version = await api.getVersion(); const instance = Instance.build({ type: "local", @@ -115,20 +105,16 @@ const App = () => { initFeedbackTool(d2, appConfig); await initializeAppRoles(baseUrl); - runMigrations(api).then(setMigrationsState); }; run(); - }, [baseUrl]); + }, [baseUrl, api]); - if (migrationsState.type === "pending") { - return ( - setMigrationsState({ type: "checked" })} - /> - ); - } else if (migrationsState.type === "checked") { + if (migrations.state.type === "pending") { + return ; + } + + if (migrations.state.type === "checked") { return ( @@ -150,20 +136,9 @@ const App = () => { ); - } else return null; -}; - -async function runMigrations(api: D2Api): Promise { - // TODO: Fix migrations - return { type: "checked" }; - - const runner = await MigrationsRunner.init({ api, debug: debug }); - - if (runner.hasPendingMigrations()) { - return { type: "pending", runner }; - } else { - return { type: "checked" }; } -} + + return null; +}; export default App; diff --git a/src/presentation/widget/WidgetApp.jsx b/src/presentation/widget/WidgetApp.tsx similarity index 78% rename from src/presentation/widget/WidgetApp.jsx rename to src/presentation/widget/WidgetApp.tsx index e0c5a88ee..16552545b 100644 --- a/src/presentation/widget/WidgetApp.jsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -3,17 +3,17 @@ 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"; +//@ts-ignore 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 { useMigrations } from "../../migrations/hooks"; import { D2Api } from "../../types/d2-api"; -import { debug } from "../../utils/debug"; +import { CompositionRoot } from "../CompositionRoot"; import { AppContext } from "../react/core/contexts/AppContext"; import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; import { muiTheme } from "../react/core/themes/dhis2.theme"; -import { CompositionRoot } from "../CompositionRoot"; import Root from "./pages/Root"; import "./WidgetApp.css"; @@ -21,10 +21,11 @@ const generateClassName = createGenerateClassName({ productionPrefix: "c", }); -const App = () => { +const App: React.FC<{ api: D2Api }> = ({ api }) => { const { baseUrl } = useConfig(); - const [appContext, setAppContext] = useState(null); - const [migrationsState, setMigrationsState] = useState({ type: "checking" }); + const migrations = useMigrations(api, "metadata-synchronization"); + + const [appContext, setAppContext] = useState(null); useEffect(() => { const run = async () => { @@ -36,7 +37,6 @@ const App = () => { if (!encryptionKey) throw new Error("You need to provide a valid encryption key"); const d2 = await init({ baseUrl: `${baseUrl}/api` }); - const api = new D2Api({ baseUrl, backend: "fetch" }); const version = await api.getVersion(); const instance = Instance.build({ type: "local", @@ -46,15 +46,13 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); - setAppContext({ d2, api, compositionRoot }); - - runMigrations(api).then(setMigrationsState); + setAppContext({ d2: d2 as object, api, compositionRoot }); }; run(); - }, [baseUrl]); + }, [baseUrl, api]); - if (migrationsState.type === "pending") { + if (migrations.state.type === "pending") { return (

    {i18n.t("Widget cannot be used until an administrator opens the application")}

    ); @@ -81,14 +79,4 @@ const App = () => { ); }; -async function runMigrations(api) { - const runner = await MigrationsRunner.init({ api, debug: debug }); - - if (runner.hasPendingMigrations()) { - return { type: "pending", runner }; - } else { - return { type: "checked" }; - } -} - export default App; diff --git a/src/scheduler/cli.ts b/src/scheduler/cli.ts index 7a7dfd17f..5ce3e129a 100644 --- a/src/scheduler/cli.ts +++ b/src/scheduler/cli.ts @@ -5,7 +5,7 @@ import path from "path"; import * as yargs from "yargs"; import { Instance } from "../domain/instance/entities/Instance"; import { MigrationsRunner } from "../migrations"; -import { migrationTasks } from "../migrations/tasks"; +import { getMigrationTasks } from "../migrations/tasks"; import { CompositionRoot } from "../presentation/CompositionRoot"; import { D2Api } from "../types/d2-api"; import Scheduler from "./scheduler"; @@ -36,8 +36,13 @@ const { config } = yargs }).argv; const checkMigrations = async (api: D2Api) => { - const debug = getLogger("migrations").debug; - const runner = await MigrationsRunner.init({ api, debug, migrations: migrationTasks }); + const runner = await MigrationsRunner.init({ + api, + debug: getLogger("migrations").debug, + migrations: await getMigrationTasks(), + dataStoreNamespace: "data-management-app", + }); + if (runner.hasPendingMigrations()) { getLogger("migrations").fatal("Scheduler is unable to continue due to database migrations"); throw new Error("There are pending migrations to be applied to the data store"); From 616a500f3c92a8df498824727ccfee6aa0e548ed Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 17 Dec 2020 08:43:37 +0100 Subject: [PATCH 110/163] Integrate migrations into compositionRoot --- i18n/en.pot | 13 +- i18n/es.po | 11 +- i18n/fr.po | 11 +- i18n/pt.po | 11 +- src/data/config/ConfigAppRepository.ts | 4 + src/data/file/FileD2Repository.ts | 2 +- .../migrations/MigrationsAppRepository.ts | 52 +++++ .../migrations/client/MigrationsRunner.ts | 184 +++++++++++++++ src/data/migrations/client/types.ts | 35 +++ .../migrations/client}/utils.ts | 2 +- .../migrations/tasks/01.instances-by-id.ts | 27 ++- .../migrations/tasks/02.rules-by-id.ts | 28 ++- .../migrations/tasks/03.sync-reports.ts | 30 +-- .../tasks/04.history-notifications.ts | 25 ++ .../migrations/tasks/05.multiple-stores.ts | 27 +++ src/data/migrations/tasks/06.this-instance.ts | 39 ++++ src/{ => data}/migrations/tasks/index.ts | 15 +- src/data/storage/StorageConstantClient.ts | 5 + src/data/storage/StorageDataStoreClient.ts | 4 + .../common/factories/RepositoryFactory.ts | 10 + .../config/repositories/ConfigRepository.ts | 1 + src/domain/migrations/entities/Debug.ts | 1 + .../migrations/entities/MigrationVersions.ts | 4 + .../repositories/MigrationsRepository.ts | 13 ++ .../usecases/GetMigrationVersionsUseCase.ts | 12 + .../usecases/HasPendingMigrationsUseCase.ts | 13 ++ .../usecases/RunMigrationsUseCase.ts | 21 ++ .../storage/repositories/StorageClient.ts | 1 + .../usecases/GenericSyncUseCase.ts | 10 +- src/migrations/cli.ts | 19 +- src/migrations/hooks.ts | 42 ---- src/migrations/index.ts | 219 ------------------ .../tasks/04.history-notifications.ts | 22 -- src/migrations/tasks/05.multiple-stores.ts | 24 -- src/migrations/tasks/06.this-instance.ts | 35 --- src/migrations/types.ts | 31 --- src/models/dataStore.ts | 117 ---------- src/presentation/CompositionRoot.ts | 17 ++ .../core/components/migrations/Migrations.tsx | 111 ++++----- .../react/core/components/migrations/hooks.ts | 30 +++ src/presentation/webapp/WebApp.tsx | 4 +- src/presentation/widget/WidgetApp.tsx | 4 +- src/scheduler/cli.ts | 39 ++-- 43 files changed, 683 insertions(+), 642 deletions(-) create mode 100644 src/data/migrations/MigrationsAppRepository.ts create mode 100644 src/data/migrations/client/MigrationsRunner.ts create mode 100644 src/data/migrations/client/types.ts rename src/{migrations => data/migrations/client}/utils.ts (89%) rename src/{ => data}/migrations/tasks/01.instances-by-id.ts (59%) rename src/{ => data}/migrations/tasks/02.rules-by-id.ts (57%) rename src/{ => data}/migrations/tasks/03.sync-reports.ts (79%) create mode 100644 src/data/migrations/tasks/04.history-notifications.ts create mode 100644 src/data/migrations/tasks/05.multiple-stores.ts create mode 100644 src/data/migrations/tasks/06.this-instance.ts rename src/{ => data}/migrations/tasks/index.ts (60%) create mode 100644 src/domain/migrations/entities/Debug.ts create mode 100644 src/domain/migrations/entities/MigrationVersions.ts create mode 100644 src/domain/migrations/repositories/MigrationsRepository.ts create mode 100644 src/domain/migrations/usecases/GetMigrationVersionsUseCase.ts create mode 100644 src/domain/migrations/usecases/HasPendingMigrationsUseCase.ts create mode 100644 src/domain/migrations/usecases/RunMigrationsUseCase.ts delete mode 100644 src/migrations/hooks.ts delete mode 100644 src/migrations/index.ts delete mode 100644 src/migrations/tasks/04.history-notifications.ts delete mode 100644 src/migrations/tasks/05.multiple-stores.ts delete mode 100644 src/migrations/tasks/06.this-instance.ts delete mode 100644 src/migrations/types.ts delete mode 100644 src/models/dataStore.ts create mode 100644 src/presentation/react/core/components/migrations/hooks.ts diff --git a/i18n/en.pot b/i18n/en.pot index d58a8fea8..d9a32cabc 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-12-16T17:46:49.200Z\n" -"PO-Revision-Date: 2020-12-16T17:46:49.200Z\n" +"POT-Creation-Date: 2020-12-17T07:44:01.078Z\n" +"PO-Revision-Date: 2020-12-17T07:44:01.078Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -393,15 +393,18 @@ msgstr "" msgid "Search by " msgstr "" +msgid "" +"There has been an error. You can either retry or contact your administrator " +"if you think there has been an un recoverable error" +msgstr "" + msgid "There are pending migrations" msgstr "" msgid "Migrations finished successfully, you may now continue to the app" msgstr "" -msgid "" -"There has been an error. You can either retry or contact your administrator " -"if you think there has been an un recoverable error" +msgid "Checking migrations" msgstr "" msgid "Migrate instance" diff --git a/i18n/es.po b/i18n/es.po index 8020604b5..59d75689d 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-12-16T16:59:31.687Z\n" +"POT-Creation-Date: 2020-12-17T07:44:01.078Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -394,15 +394,18 @@ msgstr "" msgid "Search by " msgstr "" +msgid "" +"There has been an error. You can either retry or contact your administrator " +"if you think there has been an un recoverable error" +msgstr "" + msgid "There are pending migrations" msgstr "" msgid "Migrations finished successfully, you may now continue to the app" msgstr "" -msgid "" -"There has been an error. You can either retry or contact your administrator " -"if you think there has been an un recoverable error" +msgid "Checking migrations" msgstr "" msgid "Migrate instance" diff --git a/i18n/fr.po b/i18n/fr.po index b7505cc01..c8e0aabac 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-12-16T16:59:31.687Z\n" +"POT-Creation-Date: 2020-12-17T07:44:01.078Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -394,15 +394,18 @@ msgstr "" msgid "Search by " msgstr "" +msgid "" +"There has been an error. You can either retry or contact your administrator " +"if you think there has been an un recoverable error" +msgstr "" + msgid "There are pending migrations" msgstr "" msgid "Migrations finished successfully, you may now continue to the app" msgstr "" -msgid "" -"There has been an error. You can either retry or contact your administrator " -"if you think there has been an un recoverable error" +msgid "Checking migrations" msgstr "" msgid "Migrate instance" diff --git a/i18n/pt.po b/i18n/pt.po index b7505cc01..c8e0aabac 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-12-16T16:59:31.687Z\n" +"POT-Creation-Date: 2020-12-17T07:44:01.078Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -394,15 +394,18 @@ msgstr "" msgid "Search by " msgstr "" +msgid "" +"There has been an error. You can either retry or contact your administrator " +"if you think there has been an un recoverable error" +msgstr "" + msgid "There are pending migrations" msgstr "" msgid "Migrations finished successfully, you may now continue to the app" msgstr "" -msgid "" -"There has been an error. You can either retry or contact your administrator " -"if you think there has been an un recoverable error" +msgid "Checking migrations" msgstr "" msgid "Migrate instance" diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts index 46db52a54..28366eb91 100644 --- a/src/data/config/ConfigAppRepository.ts +++ b/src/data/config/ConfigAppRepository.ts @@ -10,6 +10,10 @@ import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class ConfigAppRepository implements ConfigRepository { constructor(private instance: Instance) {} + public getBaseUrl(): string { + return this.instance.url; + } + public async detectStorageClients(): Promise> { const dataStoreClient = new StorageDataStoreClient(this.instance); const constantClient = new StorageConstantClient(this.instance); diff --git a/src/data/file/FileD2Repository.ts b/src/data/file/FileD2Repository.ts index a9ada357b..b6f5047f2 100644 --- a/src/data/file/FileD2Repository.ts +++ b/src/data/file/FileD2Repository.ts @@ -1,7 +1,7 @@ -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"; +import { D2Document } from "../../types/d2-api"; interface SaveApiResponse { response: { diff --git a/src/data/migrations/MigrationsAppRepository.ts b/src/data/migrations/MigrationsAppRepository.ts new file mode 100644 index 000000000..aae8aff92 --- /dev/null +++ b/src/data/migrations/MigrationsAppRepository.ts @@ -0,0 +1,52 @@ +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; +import { Debug } from "../../domain/migrations/entities/Debug"; +import { MigrationVersions } from "../../domain/migrations/entities/MigrationVersions"; +import { MigrationsRepository } from "../../domain/migrations/repositories/MigrationsRepository"; +import { cache } from "../../utils/cache"; +import { MigrationsRunner } from "./client/MigrationsRunner"; +import { AppStorage } from "./client/types"; +import { getMigrationTasks, MigrationParams } from "./tasks"; + +export class MigrationsAppRepository implements MigrationsRepository { + constructor(private configRepository: ConfigRepository) {} + + public async runMigrations(debug: Debug): Promise { + const runner = await this.getMigrationsRunner(); + await runner.setDebug(debug).execute(); + } + + public async hasPendingMigrations(): Promise { + const runner = await this.getMigrationsRunner(); + return runner.hasPendingMigrations(); + } + + public async getAppVersion(): Promise { + const runner = await this.getMigrationsRunner(); + return { appVersion: runner.appVersion, instanceVersion: runner.instanceVersion }; + } + + @cache() + private async getMigrationsRunner(): Promise> { + const storage = await this.getStorageClient(); + const baseUrl = this.configRepository.getBaseUrl(); + + return MigrationsRunner.init({ + storage, + debug: console.debug, + migrations: await getMigrationTasks(), + migrationParams: { baseUrl }, + }); + } + + private async getStorageClient(): Promise { + const storageClient = await this.configRepository.getStorageClient(); + + return { + get: storageClient.getObject, + getOrCreate: storageClient.getOrCreateObject, + save: storageClient.saveObject, + remove: storageClient.removeObject, + listKeys: storageClient.listKeys, + }; + } +} diff --git a/src/data/migrations/client/MigrationsRunner.ts b/src/data/migrations/client/MigrationsRunner.ts new file mode 100644 index 000000000..6bff526ce --- /dev/null +++ b/src/data/migrations/client/MigrationsRunner.ts @@ -0,0 +1,184 @@ +import _ from "lodash"; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { promiseMap } from "../../../utils/common"; +import { AppStorage, Config, MigrationWithVersion, RunnerOptions } from "./types"; +import { zeroPad } from "./utils"; + +export class MigrationsRunner { + public migrations: MigrationWithVersion[]; + public debug: Debug; + public appVersion: number; + private backupPrefix: string; + private storage: AppStorage; + private storageKey: string; + private migrationParams: T; + + constructor(private config: Config, private options: RunnerOptions) { + this.appVersion = getMaxMigrationVersion(options.migrations); + this.migrations = this.getMigrationsToApply(options.migrations, config); + this.storage = options.storage; + this.storageKey = options.storageKey ?? "config"; + this.backupPrefix = options.backupPrefix ?? "backup-"; + this.debug = options.debug ?? _.identity; + this.migrationParams = options.migrationParams; + } + + setDebug(debug: Debug): MigrationsRunner { + const newOptions = { ...this.options, debug }; + return new MigrationsRunner(this.config, newOptions); + } + + static async init(options: RunnerOptions): Promise> { + const { migrations, storage, storageKey = "config" } = options; + const config = await storage.getOrCreate(storageKey, { + version: getMaxMigrationVersion(migrations), + }); + + return new MigrationsRunner(config, options); + } + + public hasPendingMigrations(): boolean { + return this.config.version !== this.appVersion; + } + + public get instanceVersion(): number { + return this.config.version; + } + + public async execute(): Promise { + // Re-load the runner to make sure we have the latest data as config. + const runner = await MigrationsRunner.init(this.options); + return runner.migrateFromCurrent(); + } + + private async migrateFromCurrent(): Promise { + const { config, migrations, debug } = this; + + if (_.isEmpty(migrations)) { + debug(`No migrations pending to run (current version: ${config.version})`); + return; + } + + debug(`Migrate: version ${this.instanceVersion} to version ${this.appVersion}`); + + await this.runMigrations(migrations); + } + + private async runMigrations(migrations: MigrationWithVersion[]): Promise { + const { storage, debug, config, migrationParams } = this; + + // Save migration state in storage + await storage.save(this.storageKey, { + ...config, + migration: { version: this.appVersion }, + }); + + await promiseMap(migrations, async migration => { + debug(`Apply migration ${zeroPad(migration.version, 2)} - ${migration.name}`); + try { + await migration.migrate(storage, debug, migrationParams); + } catch (error) { + const errorMsg = `${migration.name}: ${error.message}`; + await this.saveConfig({ errorMsg }); + throw error; + } + }); + + // Save success state in storage + const newConfig = { version: this.appVersion }; + await storage.save(this.storageKey, newConfig); + return newConfig; + } + + // dataStore backup methods are currently unused, call only if a migration needs it. + + public async deleteBackup() { + try { + const { storage, debug } = this; + const backupKeys = await this.getBackupKeys(); + debug(`Delete backup entries`); + + await promiseMap(backupKeys, backupKey => storage.remove(backupKey)); + } catch (err) { + this.debug(`Error deleting backup (non-fatal)`); + } + } + + public async rollBackExistingBackup() { + if (this.config.migration) { + await this.rollbackDataStore(new Error("Rollback existing backup")); + } + } + + public async backupDataStore() { + const { storage, storageKey, debug } = this; + debug(`Backup data store`); + const allKeys = await this.getStorageKeys(); + const keysToBackup = _(allKeys) + .reject(key => key.startsWith(this.backupPrefix)) + .difference([storageKey]) + .compact() + .value(); + + await promiseMap(keysToBackup, async key => { + const value = await storage.get(key); + if (!value) return; + + const backupKey = this.backupPrefix + key; + await storage.save(backupKey, value); + }); + } + + private async getStorageKeys(): Promise { + return this.storage.listKeys(); + } + + private async getBackupKeys() { + const allKeys = await this.getStorageKeys(); + return allKeys.filter(key => key.startsWith(this.backupPrefix)); + } + + private async rollbackDataStore(error: Error): Promise { + const { debug, config, storage } = this; + const errorMsg = error.message || error.toString(); + const keysToRestore = await this.getBackupKeys(); + + if (_.isEmpty(keysToRestore)) return config; + + debug(`Error: ${errorMsg}`); + debug("Start rollback"); + + await promiseMap(keysToRestore, async backupKey => { + const value = await storage.get(backupKey); + if (!value) return; + + const key = backupKey.replace(/^backup-/, ""); + await storage.save(key, value); + await storage.remove(backupKey); + }); + + return this.saveConfig({ errorMsg }); + } + + private async saveConfig(options: { errorMsg?: string } = {}) { + const { errorMsg } = options; + const newConfig: Config = { + ...this.config, + migration: { version: this.appVersion, error: errorMsg }, + }; + + await this.storage.save(this.storageKey, newConfig); + return newConfig; + } + + private getMigrationsToApply(allMigrations: MigrationWithVersion[], config: Config) { + return _(allMigrations) + .filter(info => info.version > config.version) + .sortBy(info => info.version) + .value(); + } +} + +function getMaxMigrationVersion(migrations: MigrationWithVersion[]): number { + return _.max(migrations.map(info => info.version)) || 0; +} diff --git a/src/data/migrations/client/types.ts b/src/data/migrations/client/types.ts new file mode 100644 index 000000000..68f99718a --- /dev/null +++ b/src/data/migrations/client/types.ts @@ -0,0 +1,35 @@ +import { Debug } from "../../../domain/migrations/entities/Debug"; + +export interface MigrationWithVersion { + version: number; + migrate: MigrationFn; + name: string; +} + +export type Migration = Omit, "version">; + +export type MigrationFn = (storage: AppStorage, debug: Debug, params: T) => Promise; + +export interface Config { + version: number; + migration?: { version: number; error?: string }; +} + +export interface AppStorage { + get(key: string): Promise; + getOrCreate(key: string, defaultValue: T): Promise; + save(key: string, value: T): Promise; + remove(key: string): Promise; + listKeys(): Promise; +} + +export interface RunnerOptions { + storage: AppStorage; + storageKey?: string; + migrations: MigrationWithVersion[]; + debug?: Debug; + backupPrefix?: string; + migrationParams: T; +} + +export type MigrationTasks = MigrationWithVersion[]; diff --git a/src/migrations/utils.ts b/src/data/migrations/client/utils.ts similarity index 89% rename from src/migrations/utils.ts rename to src/data/migrations/client/utils.ts index f43e89b49..7f15231ce 100644 --- a/src/migrations/utils.ts +++ b/src/data/migrations/client/utils.ts @@ -1,5 +1,5 @@ import _ from "lodash"; -import { Ref } from "../types/d2-api"; +import { Ref } from "../../../domain/common/entities/Ref"; export function getDuplicatedIds(objects: Obj[]): string[] { return _(objects) diff --git a/src/migrations/tasks/01.instances-by-id.ts b/src/data/migrations/tasks/01.instances-by-id.ts similarity index 59% rename from src/migrations/tasks/01.instances-by-id.ts rename to src/data/migrations/tasks/01.instances-by-id.ts index aad956a4e..21ade9c8a 100644 --- a/src/migrations/tasks/01.instances-by-id.ts +++ b/src/data/migrations/tasks/01.instances-by-id.ts @@ -1,10 +1,11 @@ +import debug from "debug"; import _ from "lodash"; -import { getDataStore, saveDataStore } from "../../models/dataStore"; -import { D2Api } from "../../types/d2-api"; -import { Maybe } from "../../types/utils"; -import { promiseMap } from "../../utils/common"; -import { Debug, Migration } from "../types"; -import { getDuplicatedIds } from "../utils"; +import { MigrationParams } from "."; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { Maybe } from "../../../types/utils"; +import { promiseMap } from "../../../utils/common"; +import { AppStorage, Migration } from "../client/types"; +import { getDuplicatedIds } from "../client/utils"; interface InstanceOld { id: string; @@ -26,8 +27,12 @@ interface InstanceDetailsNew { metadataMapping: MetadataMappingDictionary; } -async function migrate(api: D2Api, debug: Debug): Promise { - const oldInstances = await getDataStore(api, "instances", []); +export async function migrate( + storage: AppStorage, + _debug: Debug, + _params: MigrationParams +): Promise { + const oldInstances = (await storage.get("instances")) ?? []; const newInstances: InstanceNew[] = oldInstances.map(ins => _.omit(ins, ["metadataMapping"])); const duplicatedIds = getDuplicatedIds(oldInstances); const uniqueOldInstances = _.uniqBy(oldInstances, instance => instance.id); @@ -39,13 +44,13 @@ async function migrate(api: D2Api, debug: Debug): Promise { metadataMapping: oldInstance.metadataMapping || {}, }; debug(`Create details entry for instance ${oldInstance.id}`); - await saveDataStore(api, "instances-" + oldInstance.id, newInstanceDatails); + await storage.save("instances-" + oldInstance.id, newInstanceDatails); }); debug(`Save main instances object`); - await saveDataStore(api, "instances", newInstances); + await storage.save("instances", newInstances); } -const migration: Migration = { name: "Update instance ids", migrate }; +const migration: Migration = { name: "Update instance ids", migrate }; export default migration; diff --git a/src/migrations/tasks/02.rules-by-id.ts b/src/data/migrations/tasks/02.rules-by-id.ts similarity index 57% rename from src/migrations/tasks/02.rules-by-id.ts rename to src/data/migrations/tasks/02.rules-by-id.ts index 61595560b..2d5f77a04 100644 --- a/src/migrations/tasks/02.rules-by-id.ts +++ b/src/data/migrations/tasks/02.rules-by-id.ts @@ -1,10 +1,12 @@ +import debug from "debug"; import _ from "lodash"; -import { getDataStore, saveDataStore } from "../../models/dataStore"; -import { D2Api, Id } from "../../types/d2-api"; -import { Maybe } from "../../types/utils"; -import { promiseMap } from "../../utils/common"; -import { Debug, Migration } from "../types"; -import { getDuplicatedIds } from "../utils"; +import { MigrationParams } from "."; +import { Id } from "../../../domain/common/entities/Schemas"; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { Maybe } from "../../../types/utils"; +import { promiseMap } from "../../../utils/common"; +import { AppStorage, Migration } from "../client/types"; +import { getDuplicatedIds } from "../client/utils"; type SynchronizationBuilder = { targetInstances: Id[] }; @@ -20,8 +22,12 @@ interface SynchronizationRuleDetailsNew { builder: SynchronizationBuilder; } -async function migrate(api: D2Api, debug: Debug): Promise { - const oldRules = await getDataStore(api, "rules", []); +export async function migrate( + storage: AppStorage, + _debug: Debug, + _params: MigrationParams +): Promise { + const oldRules = (await storage.get("rules")) ?? []; const newRules: SynchronizationRuleNew[] = oldRules.map(oldRule => ({ ..._.omit(oldRule, ["builder"]), targetInstances: oldRule.builder.targetInstances, @@ -36,13 +42,13 @@ async function migrate(api: D2Api, debug: Debug): Promise { builder: oldRule.builder, }; debug(`Create details entry for sync rule ${oldRule.id}`); - await saveDataStore(api, "rules-" + oldRule.id, newRuleDetails); + await storage.save("rules-" + oldRule.id, newRuleDetails); }); debug(`Save main sync rules object`); - await saveDataStore(api, "rules", newRules); + await storage.save("rules", newRules); } -const migration: Migration = { name: "Update sync rules ids", migrate }; +const migration: Migration = { name: "Update sync rules ids", migrate }; export default migration; diff --git a/src/migrations/tasks/03.sync-reports.ts b/src/data/migrations/tasks/03.sync-reports.ts similarity index 79% rename from src/migrations/tasks/03.sync-reports.ts rename to src/data/migrations/tasks/03.sync-reports.ts index 2a4735a8d..c35ad02a5 100644 --- a/src/migrations/tasks/03.sync-reports.ts +++ b/src/data/migrations/tasks/03.sync-reports.ts @@ -1,7 +1,8 @@ -import { SynchronizationReportData } from "../../domain/reports/entities/SynchronizationReport"; -import { getDataStore, saveDataStore } from "../../models/dataStore"; -import { D2Api } from "../../types/d2-api"; -import { Debug, Migration } from "../types"; +import debug from "debug"; +import { MigrationParams } from "."; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { SynchronizationReportData } from "../../../domain/reports/entities/SynchronizationReport"; +import { AppStorage, Migration } from "../client/types"; export interface SynchronizationResultOld { status: "PENDING" | "SUCCESS" | "WARNING" | "ERROR" | "NETWORK ERROR"; @@ -81,24 +82,25 @@ interface SynchronizationResultNew { }[]; } -async function migrate(api: D2Api, debug: Debug): Promise { - const dataStoreKeys = await api.dataStore("metadata-synchronization").getKeys().getData(); +export async function migrate( + storage: AppStorage, + _debug: Debug, + _params: MigrationParams +): Promise { + const dataStoreKeys = await storage.listKeys(); const notificationKeys = dataStoreKeys .filter(key => key.startsWith("notifications-")) .map(key => key.replace("notifications-", "")); - const notifications = await getDataStore(api, "notifications", []); + const notifications = (await storage.get("notifications")) ?? []; for (const notification of notificationKeys) { const { type = "metadata" } = notifications.find(({ id }) => id === notification) ?? {}; debug(`Updating ${type} notification ${notification}`); - const oldNotification = await getDataStore( - api, - `notifications-${notification}`, - [] - ); + const oldNotification = + (await storage.get(`notifications-${notification}`)) ?? []; const newNotification: SynchronizationResultNew[] = oldNotification.map( ({ @@ -128,10 +130,10 @@ async function migrate(api: D2Api, debug: Debug): Promise { }) ); - await saveDataStore(api, `notifications-${notification}`, newNotification); + await storage.save(`notifications-${notification}`, newNotification); } } -const migration: Migration = { name: "Update sync reports", migrate }; +const migration: Migration = { name: "Update sync reports", migrate }; export default migration; diff --git a/src/data/migrations/tasks/04.history-notifications.ts b/src/data/migrations/tasks/04.history-notifications.ts new file mode 100644 index 000000000..e6c62571e --- /dev/null +++ b/src/data/migrations/tasks/04.history-notifications.ts @@ -0,0 +1,25 @@ +import { MigrationParams } from "."; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { SynchronizationReportData } from "../../../domain/reports/entities/SynchronizationReport"; +import { promiseMap } from "../../../utils/common"; +import { AppStorage, Migration } from "../client/types"; + +export async function migrate( + storage: AppStorage, + _debug: Debug, + _params: MigrationParams +): Promise { + const dataStoreKeys = await storage.listKeys(); + const notificationKeys = dataStoreKeys.filter(key => key.startsWith("notifications")); + + await promiseMap(notificationKeys, async key => { + const contents = (await storage.get(key)) ?? []; + const newKey = key.replace("notifications", "history"); + await storage.save(newKey, contents); + await storage.remove(key); + }); +} + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/data/migrations/tasks/05.multiple-stores.ts b/src/data/migrations/tasks/05.multiple-stores.ts new file mode 100644 index 000000000..5656104bc --- /dev/null +++ b/src/data/migrations/tasks/05.multiple-stores.ts @@ -0,0 +1,27 @@ +import { generateUid } from "d2/uid"; +import { MigrationParams } from "."; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { Store } from "../../../domain/stores/entities/Store"; +import { AppStorage, Migration } from "../client/types"; + +export async function migrate( + storage: AppStorage, + _debug: Debug, + _params: MigrationParams +): Promise { + const oldKey = "store"; + const newKey = "stores"; + + const oldContents = await storage.get(oldKey); + + if (oldContents) { + const newContents = [{ ...oldContents, id: generateUid(), default: true }]; + + await storage.save(newKey, newContents); + await storage.remove(oldKey); + } +} + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/data/migrations/tasks/06.this-instance.ts b/src/data/migrations/tasks/06.this-instance.ts new file mode 100644 index 000000000..bee943c52 --- /dev/null +++ b/src/data/migrations/tasks/06.this-instance.ts @@ -0,0 +1,39 @@ +import _ from "lodash"; +import { Instance, InstanceData } from "../../../domain/instance/entities/Instance"; +import { Debug } from "../../../domain/migrations/entities/Debug"; +import { AppStorage, Migration } from "../client/types"; +import { MigrationParams } from "./index"; + +export async function migrate( + storageClient: AppStorage, + _debug: Debug, + params: MigrationParams +): Promise { + const oldContents = await storageClient.get("instances"); + if (!oldContents) return; + + const oldInstances = oldContents.map(({ name, ...rest }) => + Instance.build({ + ...rest, + name: + name === "This instance" + ? `Local Instance with user ${rest.username ?? "unknown"}` + : name, + }).toObject() + ); + + const localInstance = Instance.build({ + type: "local", + id: "LOCAL", + name: "This instance", + url: params.baseUrl, + }).toObject(); + + const instances = _.uniqBy([localInstance, ...oldInstances], "id"); + + await storageClient.save("instances", instances); +} + +const migration: Migration = { name: "Update history notifications", migrate }; + +export default migration; diff --git a/src/migrations/tasks/index.ts b/src/data/migrations/tasks/index.ts similarity index 60% rename from src/migrations/tasks/index.ts rename to src/data/migrations/tasks/index.ts index 0173fe2da..63b43254b 100644 --- a/src/migrations/tasks/index.ts +++ b/src/data/migrations/tasks/index.ts @@ -1,6 +1,13 @@ -import { MigrationTasks, migration } from "../types"; +import { Migration, MigrationWithVersion, MigrationTasks } from "../client/types"; -export async function getMigrationTasks(): Promise { +function migration( + version: number, + migration: Migration +): MigrationWithVersion { + return { version, ...migration }; +} + +export async function getMigrationTasks(): Promise> { return [ migration(1, (await import("./01.instances-by-id")).default), migration(2, (await import("./02.rules-by-id")).default), @@ -10,3 +17,7 @@ export async function getMigrationTasks(): Promise { migration(6, (await import("./06.this-instance")).default), ]; } + +export interface MigrationParams { + baseUrl: string; +} diff --git a/src/data/storage/StorageConstantClient.ts b/src/data/storage/StorageConstantClient.ts index 96bbbeaf6..e90902b34 100644 --- a/src/data/storage/StorageConstantClient.ts +++ b/src/data/storage/StorageConstantClient.ts @@ -69,6 +69,11 @@ export class StorageConstantClient extends StorageClient { await this.updateConstant(id, value); } + public async listKeys(): Promise { + const { value = {} } = await this.getConstant>(); + return Object.keys(value); + } + private async updateConstant(id: string, value: T): Promise { await this.api.metadata .post({ diff --git a/src/data/storage/StorageDataStoreClient.ts b/src/data/storage/StorageDataStoreClient.ts index d18da9bbe..c2ae35c14 100644 --- a/src/data/storage/StorageDataStoreClient.ts +++ b/src/data/storage/StorageDataStoreClient.ts @@ -68,4 +68,8 @@ export class StorageDataStoreClient extends StorageClient { await this.saveObject(key, value as object); }); } + + public async listKeys(): Promise { + return this.dataStore.getKeys().getData(); + } } diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 66c4c314b..5f59c8a7f 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -15,6 +15,7 @@ import { MetadataRepository, MetadataRepositoryConstructor, } from "../../metadata/repositories/MetadataRepository"; +import { MigrationsRepositoryConstructor } from "../../migrations/repositories/MigrationsRepository"; import { GitHubRepositoryConstructor } from "../../packages/repositories/GitHubRepository"; import { ReportsRepositoryConstructor } from "../../reports/repositories/ReportsRepository"; import { RulesRepositoryConstructor } from "../../rules/repositories/RulesRepository"; @@ -118,6 +119,14 @@ export class RepositoryFactory { const config = this.configRepository(instance); return this.get(Repositories.RulesRepository, [config]); } + + @cache() + public migrationsRepository(instance: Instance) { + const config = this.configRepository(instance); + return this.get(Repositories.MigrationsRepository, [ + config, + ]); + } } type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; @@ -136,4 +145,5 @@ export const Repositories = { ReportsRepository: "reportsRepository", RulesRepository: "rulesRepository", SystemInfoRepository: "systemInfoRepository", + MigrationsRepository: "migrationsRepository", } as const; diff --git a/src/domain/config/repositories/ConfigRepository.ts b/src/domain/config/repositories/ConfigRepository.ts index ef5de65f2..874ec2281 100644 --- a/src/domain/config/repositories/ConfigRepository.ts +++ b/src/domain/config/repositories/ConfigRepository.ts @@ -6,6 +6,7 @@ export interface ConfigRepositoryConstructor { } export interface ConfigRepository { + getBaseUrl(): string; getStorageClient(): Promise; changeStorageClient(client: "dataStore" | "constant"): Promise; } diff --git a/src/domain/migrations/entities/Debug.ts b/src/domain/migrations/entities/Debug.ts new file mode 100644 index 000000000..288e144e9 --- /dev/null +++ b/src/domain/migrations/entities/Debug.ts @@ -0,0 +1 @@ +export type Debug = (message: string) => void; diff --git a/src/domain/migrations/entities/MigrationVersions.ts b/src/domain/migrations/entities/MigrationVersions.ts new file mode 100644 index 000000000..242a5c164 --- /dev/null +++ b/src/domain/migrations/entities/MigrationVersions.ts @@ -0,0 +1,4 @@ +export interface MigrationVersions { + appVersion: number; + instanceVersion: number; +} diff --git a/src/domain/migrations/repositories/MigrationsRepository.ts b/src/domain/migrations/repositories/MigrationsRepository.ts new file mode 100644 index 000000000..8176fbe79 --- /dev/null +++ b/src/domain/migrations/repositories/MigrationsRepository.ts @@ -0,0 +1,13 @@ +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; +import { Debug } from "../entities/Debug"; +import { MigrationVersions } from "../entities/MigrationVersions"; + +export interface MigrationsRepositoryConstructor { + new (configRepository: ConfigRepository): MigrationsRepository; +} + +export interface MigrationsRepository { + runMigrations(debug: Debug): Promise; + hasPendingMigrations(): Promise; + getAppVersion(): Promise; +} diff --git a/src/domain/migrations/usecases/GetMigrationVersionsUseCase.ts b/src/domain/migrations/usecases/GetMigrationVersionsUseCase.ts new file mode 100644 index 000000000..9e74dfdf7 --- /dev/null +++ b/src/domain/migrations/usecases/GetMigrationVersionsUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { MigrationVersions } from "../entities/MigrationVersions"; + +export class GetMigrationVersionsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(): Promise { + return this.repositoryFactory.migrationsRepository(this.localInstance).getAppVersion(); + } +} diff --git a/src/domain/migrations/usecases/HasPendingMigrationsUseCase.ts b/src/domain/migrations/usecases/HasPendingMigrationsUseCase.ts new file mode 100644 index 000000000..24a25987e --- /dev/null +++ b/src/domain/migrations/usecases/HasPendingMigrationsUseCase.ts @@ -0,0 +1,13 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; + +export class HasPendingMigrationsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(): Promise { + return this.repositoryFactory + .migrationsRepository(this.localInstance) + .hasPendingMigrations(); + } +} diff --git a/src/domain/migrations/usecases/RunMigrationsUseCase.ts b/src/domain/migrations/usecases/RunMigrationsUseCase.ts new file mode 100644 index 000000000..96b110416 --- /dev/null +++ b/src/domain/migrations/usecases/RunMigrationsUseCase.ts @@ -0,0 +1,21 @@ +import { getD2APiFromInstance } from "../../../utils/d2-utils"; +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { Debug } from "../entities/Debug"; + +export class RunMigrationsUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(debug: Debug): Promise { + // TODO: Move to a new permissions repository + const api = getD2APiFromInstance(this.localInstance); + const currentUser = await api.currentUser.get({ fields: { authorities: true } }).getData(); + + if (!currentUser.authorities.includes("ALL")) { + throw new Error("Only a user with authority ALL can run this migration"); + } + + await this.repositoryFactory.migrationsRepository(this.localInstance).runMigrations(debug); + } +} diff --git a/src/domain/storage/repositories/StorageClient.ts b/src/domain/storage/repositories/StorageClient.ts index 762a6027b..88bf9096e 100644 --- a/src/domain/storage/repositories/StorageClient.ts +++ b/src/domain/storage/repositories/StorageClient.ts @@ -19,6 +19,7 @@ export abstract class StorageClient { public abstract clearStorage(): Promise; public abstract clone(): Promise>; public abstract import(dump: Dictionary): Promise; + public abstract listKeys(): Promise; public async listObjectsInCollection(key: string): Promise { const collection = await this.getObject(key); diff --git a/src/domain/synchronization/usecases/GenericSyncUseCase.ts b/src/domain/synchronization/usecases/GenericSyncUseCase.ts index e87089a72..7cabd8df6 100644 --- a/src/domain/synchronization/usecases/GenericSyncUseCase.ts +++ b/src/domain/synchronization/usecases/GenericSyncUseCase.ts @@ -1,4 +1,3 @@ -import { D2Api } from "d2-api/2.30"; import _ from "lodash"; import { Namespace } from "../../../data/storage/Namespaces"; import i18n from "../../../locales"; @@ -30,6 +29,7 @@ import { } from "../../reports/entities/SynchronizationResult"; import { SynchronizationType } from "../entities/SynchronizationType"; import { executeAnalytics } from "../../../utils/analytics"; +import { D2Api } from "../../../types/d2-api"; export type SyncronizationClass = | typeof MetadataSyncUseCase @@ -52,8 +52,8 @@ export abstract class GenericSyncUseCase { this.api = getD2APiFromInstance(localInstance); } - public abstract async buildPayload(): Promise; - public abstract async mapPayload( + public abstract buildPayload(): Promise; + public abstract mapPayload( instance: Instance, payload: SyncronizationPayload ): Promise; @@ -61,8 +61,8 @@ export abstract class GenericSyncUseCase { // We start to use domain concepts: // for the moment old model instance and domain entity instance are going to live together for a while on sync classes. // Little by little through refactors the old instance model should disappear - public abstract async postPayload(instance: Instance): Promise; - public abstract async buildDataStats(): Promise< + public abstract postPayload(instance: Instance): Promise; + public abstract buildDataStats(): Promise< AggregatedDataStats[] | EventsDataStats[] | undefined >; diff --git a/src/migrations/cli.ts b/src/migrations/cli.ts index c5a7cb19f..83de4508e 100644 --- a/src/migrations/cli.ts +++ b/src/migrations/cli.ts @@ -1,18 +1,21 @@ +import { Instance } from "../domain/instance/entities/Instance"; +import { CompositionRoot } from "../presentation/CompositionRoot"; import { D2Api } from "../types/d2-api"; -import { MigrationsRunner } from "./index"; -import { getMigrationTasks } from "./tasks"; async function main() { const [baseUrl] = process.argv.slice(2); if (!baseUrl) throw new Error("Usage: index.ts DHIS2_URL"); const api = new D2Api({ baseUrl: baseUrl, backend: "fetch" }); - const runner = await MigrationsRunner.init({ - api, - debug: console.debug, - migrations: await getMigrationTasks(), - dataStoreNamespace: "metadata-synchronization", + const version = await api.getVersion(); + const instance = Instance.build({ + type: "local", + name: "This instance", + url: baseUrl, + version, }); - runner.execute(); + + const compositionRoot = new CompositionRoot(instance, ""); + await compositionRoot.migrations.run(console.debug); } main(); diff --git a/src/migrations/hooks.ts b/src/migrations/hooks.ts deleted file mode 100644 index 7f6c34214..000000000 --- a/src/migrations/hooks.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { D2Api } from "d2-api/2.30"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { MigrationsRunner } from "./index"; -import { getMigrationTasks } from "./tasks"; - -export type MigrationsState = - | { type: "checking" } - | { type: "pending"; runner: MigrationsRunner } - | { type: "checked" }; - -export interface UseMigrationsResult { - state: MigrationsState; - onFinish: () => void; -} - -export function useMigrations(api: D2Api, dataStoreNamespace: string): UseMigrationsResult { - const [state, setState] = useState({ type: "checking" }); - const onFinish = useCallback(() => setState({ type: "checked" }), [setState]); - - useEffect(() => { - runMigrations(api, dataStoreNamespace).then(setState); - }, [api, dataStoreNamespace]); - - const result = useMemo(() => ({ state, onFinish }), [state, onFinish]); - - return result; -} - -async function runMigrations(api: D2Api, dataStoreNamespace: string): Promise { - const runner = await MigrationsRunner.init({ - api, - debug: console.log, - migrations: await getMigrationTasks(), - dataStoreNamespace, - }); - - if (runner.hasPendingMigrations()) { - return { type: "pending", runner }; - } else { - return { type: "checked" }; - } -} diff --git a/src/migrations/index.ts b/src/migrations/index.ts deleted file mode 100644 index 53bc3c5de..000000000 --- a/src/migrations/index.ts +++ /dev/null @@ -1,219 +0,0 @@ -import _ from "lodash"; -import { D2Api } from "../types/d2-api"; -import { promiseMap } from "../utils/common"; -import { Config, Debug, MigrationWithVersion, RunnerOptions } from "./types"; -import { zeroPad } from "./utils"; - -const configKey = "config"; - -export class MigrationsRunner { - public migrations: MigrationWithVersion[]; - public debug: Debug; - public appVersion: number; - private backupPrefix = "backup-"; - private namespace: string; - - constructor(private api: D2Api, private config: Config, private options: RunnerOptions) { - const { debug = _.identity, migrations } = options; - this.appVersion = _.max(migrations.map(info => info.version)) || 0; - this.debug = debug; - this.migrations = this.getMigrationToApply(migrations, config); - this.namespace = options.dataStoreNamespace; - } - - setDebug(debug: Debug) { - const newOptions = { ...this.options, debug }; - return new MigrationsRunner(this.api, this.config, newOptions); - } - - static async init(options: RunnerOptions): Promise { - const { api } = options; - const config = await getDataStore(api, options.dataStoreNamespace, configKey, { - version: 0, - }); - return new MigrationsRunner(api, config, options); - } - - public async execute(): Promise { - // Re-load the runner to make sure we have the latest data as config. - const runner = await MigrationsRunner.init(this.options); - return runner.migrateFromCurrent(); - } - - public async migrateFromCurrent(): Promise { - const { config, migrations, debug } = this; - - if (_.isEmpty(migrations)) { - debug(`No migrations pending to run (current version: ${config.version})`); - return; - } - - debug(`Migrate: version ${this.instanceVersion} to version ${this.appVersion}`); - - await this.runMigrations(migrations); - } - - async runMigrations(migrations: MigrationWithVersion[]): Promise { - const { api, debug, config, namespace } = this; - - const configWithCurrentMigration: Config = { - ...config, - migration: { version: this.appVersion }, - }; - await saveDataStore(api, namespace, configKey, configWithCurrentMigration); - - await checkCurrentUserIsSuperadmin(api, debug); - - for (const migration of migrations) { - debug(`Apply migration ${zeroPad(migration.version, 2)} - ${migration.name}`); - try { - await migration.migrate(api, debug); - } catch (error) { - const errorMsg = `${migration.name}: ${error.message}`; - await this.saveConfig({ errorMsg }); - throw error; - } - } - - const newConfig = { version: this.appVersion }; - await saveDataStore(api, namespace, configKey, newConfig); - return newConfig; - } - - // dataStore backup methods are currently unused, call only if a migration needs it. - - async deleteBackup() { - try { - const { debug, api, namespace } = this; - const backupKeys = await this.getBackupKeys(); - debug(`Delete backup entries`); - - await promiseMap(backupKeys, async backupKey => { - await deleteDataStore(api, namespace, backupKey); - }); - } catch (err) { - this.debug(`Error deleting backup (non-fatal)`); - } - } - - async rollBackExistingBackup() { - if (this.config.migration) { - await this.rollbackDataStore(new Error("Rollback existing backup")); - } - } - - async backupDataStore() { - const { api, debug, namespace } = this; - debug(`Backup data store`); - const allKeys = await this.getDataStoreKeys(); - const keysToBackup = _(allKeys) - .reject(key => key.startsWith(this.backupPrefix)) - .difference([configKey]) - .compact() - .value(); - - await promiseMap(keysToBackup, async key => { - const value = await getDataStore(api, namespace, key, {}); - const backupKey = this.backupPrefix + key; - await saveDataStore(api, namespace, backupKey, value); - }); - } - - async getDataStoreKeys(): Promise { - return this.api.dataStore(this.options.dataStoreNamespace).getKeys().getData(); - } - - async getBackupKeys() { - const allKeys = await this.getDataStoreKeys(); - return allKeys.filter(key => key.startsWith(this.backupPrefix)); - } - - async rollbackDataStore(error: Error): Promise { - const { api, debug, config, namespace } = this; - const errorMsg = error.message || error.toString(); - const keysToRestore = await this.getBackupKeys(); - - if (_.isEmpty(keysToRestore)) return config; - - debug(`Error: ${errorMsg}`); - debug("Start rollback"); - - await promiseMap(keysToRestore, async backupKey => { - const value = await getDataStore(api, namespace, backupKey, {}); - const key = backupKey.replace(/^backup-/, ""); - await saveDataStore(api, namespace, key, value); - await deleteDataStore(api, namespace, backupKey); - }); - - return this.saveConfig({ errorMsg }); - } - - private async saveConfig(options: { errorMsg?: string } = {}) { - const { errorMsg } = options; - const newConfig: Config = { - ...this.config, - migration: { version: this.appVersion, error: errorMsg }, - }; - await saveDataStore(this.api, this.namespace, configKey, newConfig); - return newConfig; - } - - getMigrationToApply(allMigrations: MigrationWithVersion[], config: Config) { - return _(allMigrations) - .filter(info => info.version > config.version) - .sortBy(info => info.version) - .value(); - } - - hasPendingMigrations(): boolean { - return this.config.version !== this.appVersion; - } - - get instanceVersion(): number { - return this.config.version; - } -} - -async function checkCurrentUserIsSuperadmin(api: D2Api, debug: Debug) { - debug("Check that current user is superadmin"); - const currentUser = await api.currentUser.get({ fields: { authorities: true } }).getData(); - - if (!currentUser.authorities.includes("ALL")) - throw new Error("Only a user with authority ALL can run this migration"); -} - -export async function getDataStore( - api: D2Api, - dataStoreNamespace: string, - dataStoreKey: string, - defaultValue: T -): Promise { - const dataStore = api.dataStore(dataStoreNamespace); - const value = await dataStore.get(dataStoreKey).getData(); - if (!value) await dataStore.save(dataStoreKey, defaultValue).getData(); - return value ?? defaultValue; -} - -export async function saveDataStore( - api: D2Api, - dataStoreNamespace: string, - dataStoreKey: string, - value: any -): Promise { - const dataStore = api.dataStore(dataStoreNamespace); - await dataStore.save(dataStoreKey, value).getData(); -} - -export async function deleteDataStore( - api: D2Api, - dataStoreNamespace: string, - dataStoreKey: string -): Promise { - try { - await api.delete(`/dataStore/${dataStoreNamespace}/${dataStoreKey}`).getData(); - } catch (error) { - if (!error.response || error.response.status !== 404) { - throw error; - } - } -} diff --git a/src/migrations/tasks/04.history-notifications.ts b/src/migrations/tasks/04.history-notifications.ts deleted file mode 100644 index dc308e68e..000000000 --- a/src/migrations/tasks/04.history-notifications.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { SynchronizationReportData } from "../../domain/reports/entities/SynchronizationReport"; -import { deleteDataStore, getDataStore, saveDataStore } from "../../models/dataStore"; -import { D2Api } from "../../types/d2-api"; -import { promiseMap } from "../../utils/common"; -import { Migration } from "../types"; - -async function migrate(api: D2Api): Promise { - const dataStoreKeys = await api.dataStore("metadata-synchronization").getKeys().getData(); - - const notificationKeys = dataStoreKeys.filter(key => key.startsWith("notifications")); - - await promiseMap(notificationKeys, async key => { - const contents = await getDataStore(api, key, []); - const newKey = key.replace("notifications", "history"); - await saveDataStore(api, newKey, contents); - await deleteDataStore(api, key); - }); -} - -const migration: Migration = { name: "Update history notifications", migrate }; - -export default migration; diff --git a/src/migrations/tasks/05.multiple-stores.ts b/src/migrations/tasks/05.multiple-stores.ts deleted file mode 100644 index 6cbfe27d6..000000000 --- a/src/migrations/tasks/05.multiple-stores.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { generateUid } from "d2/uid"; -import { Store } from "../../domain/stores/entities/Store"; -import { deleteDataStore, saveDataStore } from "../../models/dataStore"; -import { D2Api } from "../../types/d2-api"; -import { Migration } from "../types"; - -export async function migrate(api: D2Api): Promise { - const oldKey = "store"; - const newKey = "stores"; - - const dataStore = api.dataStore("metadata-synchronization"); - const oldContents = await dataStore.get(oldKey).getData(); - - if (oldContents) { - const newContents = [{ ...oldContents, id: generateUid(), default: true }]; - - await saveDataStore(api, newKey, newContents); - await deleteDataStore(api, oldKey); - } -} - -const migration: Migration = { name: "Update history notifications", migrate }; - -export default migration; diff --git a/src/migrations/tasks/06.this-instance.ts b/src/migrations/tasks/06.this-instance.ts deleted file mode 100644 index 9cce71d26..000000000 --- a/src/migrations/tasks/06.this-instance.ts +++ /dev/null @@ -1,35 +0,0 @@ -import _ from "lodash"; -import { Instance, InstanceData } from "../../domain/instance/entities/Instance"; -import { D2Api } from "../../types/d2-api"; -import { Migration } from "../types"; - -export async function migrate(api: D2Api): Promise { - const dataStore = api.dataStore("metadata-synchronization"); - const oldContents = await dataStore.get("instances").getData(); - if (!oldContents) return; - - const oldInstances = oldContents.map(({ name, ...rest }) => - Instance.build({ - ...rest, - name: - name === "This instance" - ? `Local Instance with user ${rest.username ?? "unknown"}` - : name, - }).toObject() - ); - - const localInstance = Instance.build({ - type: "local", - id: "LOCAL", - name: "This instance", - url: api.baseUrl, - }).toObject(); - - const instances = _.uniqBy([localInstance, ...oldInstances], "id"); - - await dataStore.save("instances", instances).getData(); -} - -const migration: Migration = { name: "Update history notifications", migrate }; - -export default migration; diff --git a/src/migrations/types.ts b/src/migrations/types.ts deleted file mode 100644 index c2fa4ea1a..000000000 --- a/src/migrations/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { D2Api } from "../types/d2-api"; - -export interface Config { - version: number; - migration?: { version: number; error?: string }; -} - -export type Debug = (message: string) => void; - -export interface MigrationWithVersion { - version: number; - migrate: MigrationFn; - name: string; -} - -export type Migration = Omit; - -export type MigrationFn = (api: D2Api, debug: Debug) => Promise; - -export interface RunnerOptions { - api: D2Api; - debug?: Debug; - dataStoreNamespace: string; - migrations: MigrationWithVersion[]; -} - -export type MigrationTasks = MigrationWithVersion[]; - -export function migration(version: number, migration: Migration): MigrationWithVersion { - return { version, ...migration }; -} diff --git a/src/models/dataStore.ts b/src/models/dataStore.ts deleted file mode 100644 index dcd0e63f3..000000000 --- a/src/models/dataStore.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { D2Api, Ref } from "../types/d2-api"; -import _ from "lodash"; -import { Response } from "../types/d2"; -import { TableFilters, TableList, TablePagination } from "../types/d2-ui-components"; - -export const dataStoreNamespace = "metadata-synchronization"; - -export async function getDataStore( - api: D2Api, - dataStoreKey: string, - defaultValue: T -): Promise { - const dataStore = api.dataStore(dataStoreNamespace); - const value = await dataStore.get(dataStoreKey).getData(); - if (!value) await dataStore.save(dataStoreKey, defaultValue).getData(); - return value ?? defaultValue; -} - -export async function saveDataStore(api: D2Api, dataStoreKey: string, value: any): Promise { - const dataStore = api.dataStore(dataStoreNamespace); - await dataStore.save(dataStoreKey, value).getData(); -} - -export async function deleteDataStore(api: D2Api, dataStoreKey: string): Promise { - try { - await api.delete(`/dataStore/${dataStoreNamespace}/${dataStoreKey}`).getData(); - } catch (error) { - if (!error.response || error.response.status !== 404) { - throw error; - } - } -} - -export async function getData(api: D2Api, dataStoreKey: string): Promise { - return getDataStore(api, dataStoreKey, []); -} - -export async function getDataById( - api: D2Api, - dataStoreKey: string, - id: string -): Promise { - const rawData = await getDataStore(api, dataStoreKey, []); - return _.find(rawData, element => element.id === id); -} - -export async function getPaginatedData( - api: D2Api, - dataStoreKey: string, - filters: TableFilters | null, - pagination: TablePagination | null -): Promise { - const { search = null } = filters || {}; - const { page = 1, pageSize = 20, paging = true, sorting = ["id", "asc"] } = pagination || {}; - - const rawData = await getDataStore(api, dataStoreKey, []); - - const filteredData = search - ? _.filter(rawData, o => - _(o) - .keys() - .filter(k => typeof o[k] === "string") - .some(k => o[k].toLowerCase().includes(search.toLowerCase())) - ) - : rawData; - - const [field, direction] = sorting; - const sortedData = _.orderBy( - filteredData, - [data => (data[field] ? data[field].toLowerCase() : "")], - [direction as "asc" | "desc"] - ); - - const total = sortedData.length; - const pageCount = paging ? Math.ceil(sortedData.length / pageSize) : 1; - const firstItem = paging ? (page - 1) * pageSize : 0; - const lastItem = paging ? firstItem + pageSize : sortedData.length; - const paginatedData = _.slice(sortedData, firstItem, lastItem); - - return { objects: paginatedData, pager: { page, pageCount, total } }; -} - -export async function saveData(api: D2Api, dataStoreKey: string, data: any): Promise { - try { - const dataArray = await getDataStore(api, dataStoreKey, []); - const newDataArray = _([...dataArray, data]) - .reverse() - .uniqBy("id") - .reverse() - .value(); - await saveDataStore(api, dataStoreKey, newDataArray); - return { status: true }; - } catch (e) { - console.error(e); - return { - status: false, - error: e.toString(), - }; - } -} - -export async function deleteData(api: D2Api, dataStoreKey: string, data: any): Promise { - try { - const dataArray = await getDataStore(api, dataStoreKey, []); - const newDataArray = dataArray.filter( - (dataEl: { id: string }): boolean => dataEl.id !== data.id - ); - await saveDataStore(api, dataStoreKey, newDataArray); - return { status: true }; - } catch (e) { - console.error(e); - return { - status: false, - error: e.toString(), - }; - } -} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 0a913edcb..c73fa3391 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -5,6 +5,7 @@ import { FileD2Repository } from "../data/file/FileD2Repository"; import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepository"; import { MetadataD2ApiRepository } from "../data/metadata/MetadataD2ApiRepository"; import { MetadataJSONRepository } from "../data/metadata/MetadataJSONRepository"; +import { MigrationsAppRepository } from "../data/migrations/MigrationsAppRepository"; import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepository"; import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; @@ -43,6 +44,9 @@ import { ListMetadataUseCase } from "../domain/metadata/usecases/ListMetadataUse import { ListResponsiblesUseCase } from "../domain/metadata/usecases/ListResponsiblesUseCase"; import { MetadataSyncUseCase } from "../domain/metadata/usecases/MetadataSyncUseCase"; import { SetResponsiblesUseCase } from "../domain/metadata/usecases/SetResponsiblesUseCase"; +import { GetMigrationVersionsUseCase } from "../domain/migrations/usecases/GetMigrationVersionsUseCase"; +import { HasPendingMigrationsUseCase } from "../domain/migrations/usecases/HasPendingMigrationsUseCase"; +import { RunMigrationsUseCase } from "../domain/migrations/usecases/RunMigrationsUseCase"; import { DeleteModuleUseCase } from "../domain/modules/usecases/DeleteModuleUseCase"; import { DownloadModuleSnapshotUseCase } from "../domain/modules/usecases/DownloadModuleSnapshotUseCase"; import { GetModuleUseCase } from "../domain/modules/usecases/GetModuleUseCase"; @@ -103,6 +107,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.ReportsRepository, ReportsD2ApiRepository); this.repositoryFactory.bind(Repositories.RulesRepository, RulesD2ApiRepository); this.repositoryFactory.bind(Repositories.SystemInfoRepository, SystemInfoD2ApiRepository); + this.repositoryFactory.bind(Repositories.MigrationsRepository, MigrationsAppRepository); this.repositoryFactory.bind( Repositories.MetadataRepository, MetadataJSONRepository, @@ -347,6 +352,18 @@ export class CompositionRoot { setStorage: new SetStorageConfigUseCase(this.repositoryFactory, this.localInstance), }); } + + @cache() + public get migrations() { + return getExecute({ + run: new RunMigrationsUseCase(this.repositoryFactory, this.localInstance), + getVersions: new GetMigrationVersionsUseCase( + this.repositoryFactory, + this.localInstance + ), + hasPending: new HasPendingMigrationsUseCase(this.repositoryFactory, this.localInstance), + }); + } } function getExecute, Key extends keyof UseCases>( diff --git a/src/presentation/react/core/components/migrations/Migrations.tsx b/src/presentation/react/core/components/migrations/Migrations.tsx index c37efa852..dd3e619f8 100644 --- a/src/presentation/react/core/components/migrations/Migrations.tsx +++ b/src/presentation/react/core/components/migrations/Migrations.tsx @@ -1,15 +1,15 @@ import { ConfirmationDialog } from "d2-ui-components"; import React, { useCallback, useEffect, useState } from "react"; +import { MigrationVersions } from "../../../../../domain/migrations/entities/MigrationVersions"; import i18n from "../../../../../locales"; -import { MigrationsRunner } from "../../../../../migrations"; -import { UseMigrationsResult } from "../../../../../migrations/hooks"; +import { useAppContext } from "../../contexts/AppContext"; +import { UseMigrationsResult } from "./hooks"; export interface MigrationsProps { migrations: UseMigrationsResult; } export interface MigrationsRunnerProps { - runner: MigrationsRunner; onFinish: () => void; } @@ -19,35 +19,67 @@ const Migrations: React.FC = props => { if (state.type === "checking" || state.type === "checked") { return null; } else { - return ; + return ; } }; -type DialogState = - | { type: "show-info" } - | { type: "app-out-of-date" } - | { type: "migrating" } - | { type: "success" }; +interface DialogState { + type: "show-info" | "app-out-of-date" | "migrating" | "success" | "initializing"; +} + +const MigrationsDialog: React.FC = ({ onFinish }) => { + const { compositionRoot } = useAppContext(); -const MigrationsDialog: React.FC = props => { - const { runner, onFinish } = props; + const [state, setState] = useState({ type: "initializing" }); const [messages, setMessages] = useState([]); + const [versions, setVersions] = useState(); + + useEffect(() => { + compositionRoot.migrations.getVersions().then(versions => { + const { appVersion, instanceVersion } = versions; + setVersions(versions); + + if (instanceVersion === appVersion) { + setState({ type: "success" }); + } else if (instanceVersion > appVersion) { + setState({ type: "app-out-of-date" }); + } else { + setState({ type: "show-info" }); + } + }); + }, [compositionRoot]); - const [state, setState] = useState(getInitialState(runner)); useEffect(followContents, [messages]); const debug = useCallback((message: string) => { setMessages(messages => [...messages, message]); }, []); - const startMigration = useCallback(() => { - runMigrations(runner, debug, setState).then(setState); - }, [runner, debug]); + const startMigration = useCallback(async () => { + try { + setState({ type: "migrating" }); + await compositionRoot.migrations.run(debug); + setState({ type: "success" }); + } catch (err) { + debug("---"); + debug(`Error: ${err.message}`); + debug( + i18n.t( + "There has been an error. You can either retry or contact your administrator if you think there has been an un recoverable error" + ) + ); + setState({ type: "show-info" }); + } + }, [compositionRoot, debug]); const actionText = getActionText(state); + if (!versions) { + return null; + } + if (state.type === "app-out-of-date") { - return ; + return ; } return ( @@ -62,7 +94,7 @@ const MigrationsDialog: React.FC = props => { fullWidth={true} >
    -

    {getPendingMigrationsText(runner)}

    +

    {getPendingMigrationsText(versions)}

    {messages.map((msg, idx) => ( @@ -82,29 +114,6 @@ const MigrationsDialog: React.FC = props => { ); }; -function runMigrations( - runner: MigrationsRunner, - debug: (message: string) => void, - setState: React.Dispatch> -): Promise { - setState({ type: "migrating" }); - - return runner - .setDebug(debug) - .execute() - .then(() => ({ type: "success" as const })) - .catch(err => { - debug("---"); - debug(`Error: ${err.message}`); - debug( - i18n.t( - "There has been an error. You can either retry or contact your administrator if you think there has been an un recoverable error" - ) - ); - return { type: "show-info" as const }; - }); -} - function followContents() { const contentsEl = document.getElementById("migrations-contents"); const divEl = contentsEl ? contentsEl.parentElement : null; @@ -113,6 +122,8 @@ function followContents() { function getActionText(state: DialogState): string | undefined { switch (state.type) { + case "initializing": + return i18n.t("Checking migrations"); case "show-info": return i18n.t("Migrate instance"); case "migrating": @@ -124,27 +135,17 @@ function getActionText(state: DialogState): string | undefined { } } -function getInitialState(runner: MigrationsRunner): DialogState { - if (runner.instanceVersion === runner.appVersion) { - return { type: "success" }; - } else if (runner.instanceVersion > runner.appVersion) { - return { type: "app-out-of-date" }; - } else { - return { type: "show-info" }; - } -} - -function getPendingMigrationsText(runner: MigrationsRunner): string { +function getPendingMigrationsText(versions: MigrationVersions): string { return i18n.t( "The app needs to run pending migrations (from version {{instanceVersion}} to version {{appVersion}}) in order to continue. This may take a long time, make sure the process is not interrupted.", - runner + versions ); } const isDebug = process.env.NODE_ENV === "development"; -const MigrationsError: React.FC<{ runner: MigrationsRunner; onFinish: () => void }> = ({ - runner, +const MigrationsError: React.FC<{ versions: MigrationVersions; onFinish: () => void }> = ({ + versions, onFinish, }) => ( void > {i18n.t( "The database version ({{instanceVersion}}) is greater than the app version ({{appVersion}}), cannot continue. Please contact the administrator to update the app.", - runner + versions )} ); diff --git a/src/presentation/react/core/components/migrations/hooks.ts b/src/presentation/react/core/components/migrations/hooks.ts new file mode 100644 index 000000000..ae11ca1b8 --- /dev/null +++ b/src/presentation/react/core/components/migrations/hooks.ts @@ -0,0 +1,30 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useAppContext } from "../../contexts/AppContext"; + +export interface MigrationsState { + type: "checking" | "pending" | "checked"; +} + +export interface UseMigrationsResult { + state: MigrationsState; + onFinish: () => void; +} + +export function useMigrations(): UseMigrationsResult { + const { compositionRoot } = useAppContext(); + + const [state, setState] = useState({ type: "checking" }); + const onFinish = useCallback(() => setState({ type: "checked" }), [setState]); + + useEffect(() => { + compositionRoot.migrations + .hasPending() + .then(pendingMigrations => + setState({ type: pendingMigrations ? "pending" : "checked" }) + ); + }, [compositionRoot]); + + const result = useMemo(() => ({ state, onFinish }), [state, onFinish]); + + return result; +} diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index e2f249511..193c0d967 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -10,10 +10,10 @@ import _ from "lodash"; import OldMuiThemeProvider from "material-ui/styles/MuiThemeProvider"; import React, { useEffect, useState } from "react"; import { Instance } from "../../domain/instance/entities/Instance"; -import { useMigrations } from "../../migrations/hooks"; import { D2Api } from "../../types/d2-api"; import { initializeAppRoles } from "../../utils/permissions"; import { CompositionRoot } from "../CompositionRoot"; +import { useMigrations } from "../react/core/components/migrations/hooks"; import Migrations from "../react/core/components/migrations/Migrations"; import Share from "../react/core/components/share/Share"; import { AppContext } from "../react/core/contexts/AppContext"; @@ -71,7 +71,7 @@ function initFeedbackTool(d2: unknown, appConfig: AppConfig): void { const App: React.FC<{ api: D2Api }> = ({ api }) => { const { baseUrl } = useConfig(); - const migrations = useMigrations(api, "metadata-synchronization"); + const migrations = useMigrations(); const [appContext, setAppContext] = useState(null); const [showShareButton, setShowShareButton] = useState(false); diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index 16552545b..57a8670ca 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -8,9 +8,9 @@ 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 { useMigrations } from "../../migrations/hooks"; import { D2Api } from "../../types/d2-api"; import { CompositionRoot } from "../CompositionRoot"; +import { useMigrations } from "../react/core/components/migrations/hooks"; import { AppContext } from "../react/core/contexts/AppContext"; import muiThemeLegacy from "../react/core/themes/dhis2-legacy.theme"; import { muiTheme } from "../react/core/themes/dhis2.theme"; @@ -23,7 +23,7 @@ const generateClassName = createGenerateClassName({ const App: React.FC<{ api: D2Api }> = ({ api }) => { const { baseUrl } = useConfig(); - const migrations = useMigrations(api, "metadata-synchronization"); + const migrations = useMigrations(); const [appContext, setAppContext] = useState(null); diff --git a/src/scheduler/cli.ts b/src/scheduler/cli.ts index 5ce3e129a..dacf51ffe 100644 --- a/src/scheduler/cli.ts +++ b/src/scheduler/cli.ts @@ -4,8 +4,6 @@ import { configure, getLogger } from "log4js"; import path from "path"; import * as yargs from "yargs"; import { Instance } from "../domain/instance/entities/Instance"; -import { MigrationsRunner } from "../migrations"; -import { getMigrationTasks } from "../migrations/tasks"; import { CompositionRoot } from "../presentation/CompositionRoot"; import { D2Api } from "../types/d2-api"; import Scheduler from "./scheduler"; @@ -35,15 +33,8 @@ const { config } = yargs return JSON.parse(fs.readFileSync(path, "utf8")); }).argv; -const checkMigrations = async (api: D2Api) => { - const runner = await MigrationsRunner.init({ - api, - debug: getLogger("migrations").debug, - migrations: await getMigrationTasks(), - dataStoreNamespace: "data-management-app", - }); - - if (runner.hasPendingMigrations()) { +const checkMigrations = async (compositionRoot: CompositionRoot) => { + if (await compositionRoot.migrations.hasPending()) { getLogger("migrations").fatal("Scheduler is unable to continue due to database migrations"); throw new Error("There are pending migrations to be applied to the data store"); } @@ -57,23 +48,25 @@ const start = async (): Promise => { } const api = new D2Api({ baseUrl, auth: { username, password }, backend: "fetch" }); - await checkMigrations(api); + const version = await api.getVersion(); + const compositionRoot = new CompositionRoot( + Instance.build({ + type: "local", + name: "This instance", + url: baseUrl, + username, + password, + version, + }), + encryptionKey + ); + + await checkMigrations(compositionRoot); const welcomeMessage = `Script initialized on ${baseUrl} with user ${username}`; getLogger("main").info("-".repeat(welcomeMessage.length)); getLogger("main").info(welcomeMessage); - const version = await api.getVersion(); - const instance = Instance.build({ - type: "local", - name: "This instance", - url: baseUrl, - username, - password, - version, - }); - - const compositionRoot = new CompositionRoot(instance, encryptionKey); new Scheduler(api, compositionRoot).initialize(); }; From 5390f03fc42ab81fe11ab3ee9b685e9556db8689 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 17 Dec 2020 08:50:52 +0100 Subject: [PATCH 111/163] Remove temporal change to make project monitoring migrations work --- src/index.js | 5 ++--- src/presentation/PresentationLoader.tsx | 5 ++--- src/presentation/webapp/WebApp.tsx | 5 +++-- src/presentation/widget/WidgetApp.tsx | 5 +++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/index.js b/src/index.js index 121365c50..df7967079 100644 --- a/src/index.js +++ b/src/index.js @@ -32,9 +32,8 @@ const configI18n = ({ keyUiLocale }) => { async function main() { const baseUrl = await getBaseUrl(); - const api = new D2Api({ baseUrl, backend: "fetch" }); - try { + const api = new D2Api({ baseUrl, backend: "fetch" }); const userSettings = await api.get("/userSettings").getData(); if (typeof userSettings === "string") throw new Error("User needs to log in"); configI18n(userSettings); @@ -56,7 +55,7 @@ async function main() { try { ReactDOM.render( - + , document.getElementById("root") ); diff --git a/src/presentation/PresentationLoader.tsx b/src/presentation/PresentationLoader.tsx index fb5cd4686..8b926df0c 100644 --- a/src/presentation/PresentationLoader.tsx +++ b/src/presentation/PresentationLoader.tsx @@ -1,5 +1,4 @@ import React, { Suspense } from "react"; -import { D2Api } from "../types/d2-api"; const App = React.lazy(() => { switch (process.env.REACT_APP_PRESENTATION_TYPE) { @@ -12,10 +11,10 @@ const App = React.lazy(() => { } }); -export const PresentationLoader: React.FC<{ api: D2Api }> = ({ api }) => { +export const PresentationLoader: React.FC = () => { return ( - + ); }; diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index 193c0d967..b489ca897 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -69,7 +69,7 @@ function initFeedbackTool(d2: unknown, appConfig: AppConfig): void { } } -const App: React.FC<{ api: D2Api }> = ({ api }) => { +const App = () => { const { baseUrl } = useConfig(); const migrations = useMigrations(); @@ -88,6 +88,7 @@ const App: React.FC<{ api: D2Api }> = ({ api }) => { if (!encryptionKey) throw new Error("You need to provide a valid encryption key"); const d2 = await init({ baseUrl: `${baseUrl}/api` }); + const api = new D2Api({ baseUrl, backend: "fetch" }); const version = await api.getVersion(); const instance = Instance.build({ type: "local", @@ -108,7 +109,7 @@ const App: React.FC<{ api: D2Api }> = ({ api }) => { }; run(); - }, [baseUrl, api]); + }, [baseUrl]); if (migrations.state.type === "pending") { return ; diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index 57a8670ca..4e0bbe828 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -21,7 +21,7 @@ const generateClassName = createGenerateClassName({ productionPrefix: "c", }); -const App: React.FC<{ api: D2Api }> = ({ api }) => { +const App = () => { const { baseUrl } = useConfig(); const migrations = useMigrations(); @@ -37,6 +37,7 @@ const App: React.FC<{ api: D2Api }> = ({ api }) => { if (!encryptionKey) throw new Error("You need to provide a valid encryption key"); const d2 = await init({ baseUrl: `${baseUrl}/api` }); + const api = new D2Api({ baseUrl, backend: "fetch" }); const version = await api.getVersion(); const instance = Instance.build({ type: "local", @@ -50,7 +51,7 @@ const App: React.FC<{ api: D2Api }> = ({ api }) => { }; run(); - }, [baseUrl, api]); + }, [baseUrl]); if (migrations.state.type === "pending") { return ( From fc7f5fd5ca429eefbf35cdc34c207b4244c836b2 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 17 Dec 2020 09:03:40 +0100 Subject: [PATCH 112/163] Fix class invocation --- src/data/migrations/MigrationsAppRepository.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/data/migrations/MigrationsAppRepository.ts b/src/data/migrations/MigrationsAppRepository.ts index aae8aff92..0ac5808a5 100644 --- a/src/data/migrations/MigrationsAppRepository.ts +++ b/src/data/migrations/MigrationsAppRepository.ts @@ -42,11 +42,13 @@ export class MigrationsAppRepository implements MigrationsRepository { const storageClient = await this.configRepository.getStorageClient(); return { - get: storageClient.getObject, - getOrCreate: storageClient.getOrCreateObject, - save: storageClient.saveObject, - remove: storageClient.removeObject, - listKeys: storageClient.listKeys, + get: (key: string) => storageClient.getObject(key), + getOrCreate: (key: string, defaultValue: T) => + storageClient.getOrCreateObject(key, defaultValue), + save: (key: string, value: T) => + storageClient.saveObject(key, value), + remove: (key: string) => storageClient.removeObject(key), + listKeys: () => storageClient.listKeys(), }; } } From 8fb82a8cd91c81d1f3643a651264848b45ff466e Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Thu, 17 Dec 2020 09:08:27 +0100 Subject: [PATCH 113/163] Fix app context not being defined --- .../react/core/components/migrations/hooks.ts | 10 ++++------ src/presentation/webapp/WebApp.tsx | 9 ++++++--- src/presentation/widget/WidgetApp.tsx | 3 +-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/presentation/react/core/components/migrations/hooks.ts b/src/presentation/react/core/components/migrations/hooks.ts index ae11ca1b8..12d91db18 100644 --- a/src/presentation/react/core/components/migrations/hooks.ts +++ b/src/presentation/react/core/components/migrations/hooks.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect, useMemo, useState } from "react"; -import { useAppContext } from "../../contexts/AppContext"; +import { AppContext } from "../../contexts/AppContext"; export interface MigrationsState { type: "checking" | "pending" | "checked"; @@ -10,19 +10,17 @@ export interface UseMigrationsResult { onFinish: () => void; } -export function useMigrations(): UseMigrationsResult { - const { compositionRoot } = useAppContext(); - +export function useMigrations(appContext: AppContext | null): UseMigrationsResult { const [state, setState] = useState({ type: "checking" }); const onFinish = useCallback(() => setState({ type: "checked" }), [setState]); useEffect(() => { - compositionRoot.migrations + appContext?.compositionRoot.migrations .hasPending() .then(pendingMigrations => setState({ type: pendingMigrations ? "pending" : "checked" }) ); - }, [compositionRoot]); + }, [appContext]); const result = useMemo(() => ({ state, onFinish }), [state, onFinish]); diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index b489ca897..9afafe3b0 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -71,10 +71,9 @@ function initFeedbackTool(d2: unknown, appConfig: AppConfig): void { const App = () => { const { baseUrl } = useConfig(); - const migrations = useMigrations(); - const [appContext, setAppContext] = useState(null); const [showShareButton, setShowShareButton] = useState(false); + const migrations = useMigrations(appContext); const appTitle = process.env.REACT_APP_PRESENTATION_TITLE; @@ -112,7 +111,11 @@ const App = () => { }, [baseUrl]); if (migrations.state.type === "pending") { - return ; + return ( + + + + ); } if (migrations.state.type === "checked") { diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index 4e0bbe828..236902f14 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -23,9 +23,8 @@ const generateClassName = createGenerateClassName({ const App = () => { const { baseUrl } = useConfig(); - const migrations = useMigrations(); - const [appContext, setAppContext] = useState(null); + const migrations = useMigrations(appContext); useEffect(() => { const run = async () => { From a40595c0d3c55adca22ea0b689ad888d55ea0aca Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 18 Dec 2020 10:24:24 +0100 Subject: [PATCH 114/163] Use --files for ts-node (required to load custom types) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f13a6068..567a63655 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "start-scheduler": "yarn run-ts --files src/scheduler/cli.ts", "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\"}'", + "run-ts": "ts-node --files -O '{\"module\":\"commonjs\"}'", "test": "jest --env=jsdom-fourteen --passWithNoTests", "lint": "eslint \"{src,cypress}/**/*.{js,jsx,ts,tsx}\"", "eject": "react-scripts eject", From 54c0ecaa15829aabc45acb57480df6e991e681bf Mon Sep 17 00:00:00 2001 From: Arnau Sanchez Date: Fri, 18 Dec 2020 11:41:19 +0100 Subject: [PATCH 115/163] Abstract return type of getMigrationTasks --- .../migrations/MigrationsAppRepository.ts | 6 ++++- src/data/migrations/client/types.ts | 2 +- src/data/migrations/tasks/index.ts | 23 +++++++------------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/data/migrations/MigrationsAppRepository.ts b/src/data/migrations/MigrationsAppRepository.ts index 0ac5808a5..5ae50b8b1 100644 --- a/src/data/migrations/MigrationsAppRepository.ts +++ b/src/data/migrations/MigrationsAppRepository.ts @@ -6,6 +6,7 @@ import { cache } from "../../utils/cache"; import { MigrationsRunner } from "./client/MigrationsRunner"; import { AppStorage } from "./client/types"; import { getMigrationTasks, MigrationParams } from "./tasks"; +import { promiseMap } from "../../utils/common"; export class MigrationsAppRepository implements MigrationsRepository { constructor(private configRepository: ConfigRepository) {} @@ -29,11 +30,14 @@ export class MigrationsAppRepository implements MigrationsRepository { private async getMigrationsRunner(): Promise> { const storage = await this.getStorageClient(); const baseUrl = this.configRepository.getBaseUrl(); + const migrations = await promiseMap(getMigrationTasks(), async ([version, module_]) => { + return { version, ...(await module_).default }; + }); return MigrationsRunner.init({ storage, debug: console.debug, - migrations: await getMigrationTasks(), + migrations, migrationParams: { baseUrl }, }); } diff --git a/src/data/migrations/client/types.ts b/src/data/migrations/client/types.ts index 68f99718a..0ecfbc3d5 100644 --- a/src/data/migrations/client/types.ts +++ b/src/data/migrations/client/types.ts @@ -32,4 +32,4 @@ export interface RunnerOptions { migrationParams: T; } -export type MigrationTasks = MigrationWithVersion[]; +export type MigrationTasks = Array<[number, Promise<{ default: Migration }>]>; diff --git a/src/data/migrations/tasks/index.ts b/src/data/migrations/tasks/index.ts index 63b43254b..804faa65b 100644 --- a/src/data/migrations/tasks/index.ts +++ b/src/data/migrations/tasks/index.ts @@ -1,20 +1,13 @@ -import { Migration, MigrationWithVersion, MigrationTasks } from "../client/types"; +import { MigrationTasks } from "../client/types"; -function migration( - version: number, - migration: Migration -): MigrationWithVersion { - return { version, ...migration }; -} - -export async function getMigrationTasks(): Promise> { +export function getMigrationTasks(): MigrationTasks { return [ - migration(1, (await import("./01.instances-by-id")).default), - migration(2, (await import("./02.rules-by-id")).default), - migration(3, (await import("./03.sync-reports")).default), - migration(4, (await import("./04.history-notifications")).default), - migration(5, (await import("./05.multiple-stores")).default), - migration(6, (await import("./06.this-instance")).default), + [1, import("./01.instances-by-id")], + [2, import("./02.rules-by-id")], + [3, import("./03.sync-reports")], + [4, import("./04.history-notifications")], + [5, import("./05.multiple-stores")], + [6, import("./06.this-instance")], ]; } From 1687b4b8eadc4b954b3c7b2605cdcfff1b09f0f7 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 18 Dec 2020 13:25:35 +0100 Subject: [PATCH 116/163] Remove base url from migration --- src/data/config/ConfigAppRepository.ts | 4 ---- src/data/migrations/MigrationsAppRepository.ts | 3 +-- src/data/migrations/tasks/06.this-instance.ts | 4 ++-- src/data/migrations/tasks/index.ts | 4 +--- src/domain/config/repositories/ConfigRepository.ts | 3 ++- 5 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/data/config/ConfigAppRepository.ts b/src/data/config/ConfigAppRepository.ts index 28366eb91..46db52a54 100644 --- a/src/data/config/ConfigAppRepository.ts +++ b/src/data/config/ConfigAppRepository.ts @@ -10,10 +10,6 @@ import { StorageDataStoreClient } from "../storage/StorageDataStoreClient"; export class ConfigAppRepository implements ConfigRepository { constructor(private instance: Instance) {} - public getBaseUrl(): string { - return this.instance.url; - } - public async detectStorageClients(): Promise> { const dataStoreClient = new StorageDataStoreClient(this.instance); const constantClient = new StorageConstantClient(this.instance); diff --git a/src/data/migrations/MigrationsAppRepository.ts b/src/data/migrations/MigrationsAppRepository.ts index 5ae50b8b1..cc3f287da 100644 --- a/src/data/migrations/MigrationsAppRepository.ts +++ b/src/data/migrations/MigrationsAppRepository.ts @@ -29,7 +29,6 @@ export class MigrationsAppRepository implements MigrationsRepository { @cache() private async getMigrationsRunner(): Promise> { const storage = await this.getStorageClient(); - const baseUrl = this.configRepository.getBaseUrl(); const migrations = await promiseMap(getMigrationTasks(), async ([version, module_]) => { return { version, ...(await module_).default }; }); @@ -38,7 +37,7 @@ export class MigrationsAppRepository implements MigrationsRepository { storage, debug: console.debug, migrations, - migrationParams: { baseUrl }, + migrationParams: {}, }); } diff --git a/src/data/migrations/tasks/06.this-instance.ts b/src/data/migrations/tasks/06.this-instance.ts index bee943c52..587e0a85e 100644 --- a/src/data/migrations/tasks/06.this-instance.ts +++ b/src/data/migrations/tasks/06.this-instance.ts @@ -7,7 +7,7 @@ import { MigrationParams } from "./index"; export async function migrate( storageClient: AppStorage, _debug: Debug, - params: MigrationParams + _params: MigrationParams ): Promise { const oldContents = await storageClient.get("instances"); if (!oldContents) return; @@ -26,7 +26,7 @@ export async function migrate( type: "local", id: "LOCAL", name: "This instance", - url: params.baseUrl, + url: "", }).toObject(); const instances = _.uniqBy([localInstance, ...oldInstances], "id"); diff --git a/src/data/migrations/tasks/index.ts b/src/data/migrations/tasks/index.ts index 804faa65b..458fcf644 100644 --- a/src/data/migrations/tasks/index.ts +++ b/src/data/migrations/tasks/index.ts @@ -11,6 +11,4 @@ export function getMigrationTasks(): MigrationTasks { ]; } -export interface MigrationParams { - baseUrl: string; -} +export interface MigrationParams {} diff --git a/src/domain/config/repositories/ConfigRepository.ts b/src/domain/config/repositories/ConfigRepository.ts index 874ec2281..cb89f8e14 100644 --- a/src/domain/config/repositories/ConfigRepository.ts +++ b/src/domain/config/repositories/ConfigRepository.ts @@ -6,7 +6,8 @@ export interface ConfigRepositoryConstructor { } export interface ConfigRepository { - getBaseUrl(): string; + // Storage client should only be accessible from data layer + // This two methods will be removed in future refactors getStorageClient(): Promise; changeStorageClient(client: "dataStore" | "constant"): Promise; } From 4d073c1f5de1cfc5ad435059fd7cf92d73307ccd Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 18 Dec 2020 13:38:46 +0100 Subject: [PATCH 117/163] Add start-up routine for this instance initialization --- src/presentation/CompositionRoot.ts | 37 ++++++++++++++++++++++++++- src/presentation/webapp/WebApp.tsx | 1 + src/presentation/widget/WidgetApp.tsx | 2 ++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index c73fa3391..35d72d962 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -10,6 +10,7 @@ import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepositor import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; +import { Namespace } from "../data/storage/Namespaces"; import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; @@ -19,7 +20,7 @@ import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageCon import { SetStorageConfigUseCase } from "../domain/config/usecases/SetStorageConfigUseCase"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; -import { Instance } from "../domain/instance/entities/Instance"; +import { Instance, InstanceData } from "../domain/instance/entities/Instance"; import { DeleteInstanceUseCase } from "../domain/instance/usecases/DeleteInstanceUseCase"; import { GetInstanceApiUseCase } from "../domain/instance/usecases/GetInstanceApiUseCase"; import { GetInstanceByIdUseCase } from "../domain/instance/usecases/GetInstanceByIdUseCase"; @@ -119,6 +120,11 @@ export class CompositionRoot { ); } + public async initialize() { + const initializeRoutine = new StartApplicationRoutine(this.repositoryFactory, this.localInstance); + await initializeRoutine.execute(); + } + @cache() public get sync() { // TODO: Sync builder should be part of an execute method @@ -379,3 +385,32 @@ function getExecute, Key extends keyof Use return output; }, initialOutput); } + +export class StartApplicationRoutine implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(): Promise { + await this.verifyLocalInstanceExists(); + } + + private async verifyLocalInstanceExists() { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); + + if (objects.find(data => data.id === "LOCAL")) return; + + const localInstance = Instance.build({ + type: "local", + id: "LOCAL", + name: "This instance", + url: "", + }); + + await storageClient.saveObjectInCollection(Namespace.INSTANCES, localInstance); + } +} diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index 9afafe3b0..12d0f5bcf 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -97,6 +97,7 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); + await compositionRoot.initialize(); setAppContext({ d2: d2 as object, api, compositionRoot }); diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index 236902f14..f61838c4f 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -46,6 +46,8 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); + await compositionRoot.initialize(); + setAppContext({ d2: d2 as object, api, compositionRoot }); }; From 6671fa7251c843deb8cef56efd3bce4756fc2141 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Fri, 18 Dec 2020 13:40:04 +0100 Subject: [PATCH 118/163] Add toObject method --- src/presentation/CompositionRoot.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 35d72d962..ad5d47728 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -121,7 +121,10 @@ export class CompositionRoot { } public async initialize() { - const initializeRoutine = new StartApplicationRoutine(this.repositoryFactory, this.localInstance); + const initializeRoutine = new StartApplicationRoutine( + this.repositoryFactory, + this.localInstance + ); await initializeRoutine.execute(); } @@ -409,7 +412,7 @@ export class StartApplicationRoutine implements UseCase { id: "LOCAL", name: "This instance", url: "", - }); + }).toObject(); await storageClient.saveObjectInCollection(Namespace.INSTANCES, localInstance); } From 0176626af04349de854a4a2e98dd8cc8ad6f6e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 21 Dec 2020 06:19:50 +0100 Subject: [PATCH 119/163] Add data element group MSF settings --- i18n/en.pot | 7 +- i18n/es.po | 5 +- i18n/fr.po | 5 +- i18n/pt.po | 5 +- .../msf-Settings/MSFSettingsDialog.tsx | 68 ++++++++++++++++--- 5 files changed, 75 insertions(+), 15 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index a46ddd01c..c9dafd1a1 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-12-15T08:29:18.609Z\n" -"PO-Revision-Date: 2020-12-15T08:29:18.609Z\n" +"POT-Creation-Date: 2020-12-21T05:18:27.635Z\n" +"PO-Revision-Date: 2020-12-21T05:18:27.635Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1216,6 +1216,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 001f59138..76a70dae1 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-21T05:10:02.809Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1222,6 +1222,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 0745c3d2a..43b8dda6a 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-21T05:10:02.809Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1219,6 +1219,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 0745c3d2a..43b8dda6a 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-21T05:10:02.809Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1219,6 +1219,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx index 405b2b9a7..905deec5c 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx @@ -1,12 +1,17 @@ +import { makeStyles, Theme } from "@material-ui/core"; import { ConfirmationDialog } from "d2-ui-components"; -import React, { useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; +import { DataElementGroup } from "../../../../../domain/metadata/entities/MetadataEntities"; import i18n from "../../../../../locales"; -import Dropdown from "../../../core/components/dropdown/Dropdown"; +import { DataElementGroupModel } from "../../../../../models/dhis/metadata"; +import Dropdown, { DropdownOption } from "../../../core/components/dropdown/Dropdown"; +import { useAppContext } from "../../../core/contexts/AppContext"; export type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; export type MSFSettings = { runAnalytics: RunAnalyticsSettings; + categoryOptionGroupId?: string; }; export interface MSFSettingsDialogProps { @@ -20,7 +25,32 @@ export const MSFSettingsDialog: React.FC = ({ onSave, msfSettings, }) => { + const classes = useStyles(); + const { compositionRoot } = useAppContext(); const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); + const [catOptionGroups, setCatOptionGroups] = useState[]>([]); + const [selectedCatOptionGroup, setSelectedCatOptionGroup] = useState( + msfSettings.categoryOptionGroupId + ); + + useEffect(() => { + compositionRoot.metadata + .listAll({ + type: DataElementGroupModel.getCollectionName(), + paging: false, + order: { + field: "displayName" as const, + order: "asc" as const, + }, + }) + .then(data => { + const dataElementGroups = data as DataElementGroup[]; + + setCatOptionGroups( + dataElementGroups.map(group => ({ id: group.id, name: group.name })) + ); + }); + }, [compositionRoot.metadata]); const useSyncRuleItems = useMemo(() => { return [ @@ -47,6 +77,7 @@ export const MSFSettingsDialog: React.FC = ({ : useSyncRule === "true" ? true : false, + categoryOptionGroupId: selectedCatOptionGroup, }; onSave(msfSettings); @@ -55,7 +86,7 @@ export const MSFSettingsDialog: React.FC = ({ return ( = ({ cancelText={i18n.t("Cancel")} saveText={i18n.t("Save")} > - +

    + +
    +
    + +
    ); }; + +const useStyles = makeStyles((theme: Theme) => ({ + selector: { + margin: theme.spacing(4, 0, 4, 0), + }, +})); From aa26d36988ac9d10c8162988b0fae87150985ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 21 Dec 2020 07:00:49 +0100 Subject: [PATCH 120/163] Add deleteDataValuesBeforeSync to advanced settings --- i18n/en.pot | 10 +-- i18n/es.po | 8 +-- i18n/fr.po | 8 +-- i18n/pt.po | 8 +-- .../AdvancedSettingsDialog.tsx} | 61 ++++++++++++++----- .../MSFSettingsDialog.tsx | 2 +- .../msf-aggregate-data/pages/MSFHomePage.tsx | 28 +++++---- .../pages/MSFHomePagePresenter.ts | 27 ++++---- 8 files changed, 94 insertions(+), 58 deletions(-) rename src/presentation/react/{core/components/period-selection-dialog/PeriodSelectionDialog.tsx => msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx} (54%) rename src/presentation/react/msf-aggregate-data/components/{msf-Settings => msf-settings-dialog}/MSFSettingsDialog.tsx (98%) diff --git a/i18n/en.pot b/i18n/en.pot index c9dafd1a1..42c1907fb 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-12-21T05:18:27.635Z\n" -"PO-Revision-Date: 2020-12-21T05:18:27.635Z\n" +"POT-Creation-Date: 2020-12-21T05:59:50.425Z\n" +"PO-Revision-Date: 2020-12-21T05:59:50.425Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -767,9 +767,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1201,6 +1198,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 76a70dae1..d977f486f 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-12-21T05:10:02.809Z\n" +"POT-Creation-Date: 2020-12-21T05:46:39.583Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -771,9 +771,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1207,6 +1204,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 43b8dda6a..35f3a7c9d 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-12-21T05:10:02.809Z\n" +"POT-Creation-Date: 2020-12-21T05:46:39.583Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -768,9 +768,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1204,6 +1201,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 43b8dda6a..35f3a7c9d 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-12-21T05:10:02.809Z\n" +"POT-Creation-Date: 2020-12-21T05:46:39.583Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -768,9 +768,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1204,6 +1201,9 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + msgid "True" msgstr "" diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx similarity index 54% rename from src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx rename to src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx index 1b4de8e1b..46b7c483d 100644 --- a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx @@ -1,30 +1,43 @@ -import { Checkbox, FormControlLabel } from "@material-ui/core"; +import { Checkbox, FormControlLabel, makeStyles, Theme } from "@material-ui/core"; import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; import React, { useState } from "react"; import { Period } from "../../../../../domain/common/entities/Period"; import i18n from "../../../../../locales"; -import PeriodSelection, { ObjectWithPeriod } from "../period-selection/PeriodSelection"; +import PeriodSelection, { + ObjectWithPeriod, +} from "../../../core/components/period-selection/PeriodSelection"; +import { Toggle } from "../../../core/components/toggle/Toggle"; -export interface PeriodSelectionDialogProps { - title?: string; +export type AdvancedSettings = { period?: Period; + deleteDataValuesBeforeSync?: boolean; +}; + +export interface AdvancedSettingsDialogProps { + title?: string; + advancedSettings?: AdvancedSettings; onClose(): void; - onSave(period?: Period): void; + onSave(advancedSettings?: AdvancedSettings): void; } -export const PeriodSelectionDialog: React.FC = ({ +export const AdvancedSettingsDialog: React.FC = ({ title, onClose, onSave, - period, + advancedSettings, }) => { + const classes = useStyles(); const snackbar = useSnackbar(); + const [deleteDataValuesBeforeSync, setDeleteDataValuesBeforeSync] = useState( + advancedSettings?.deleteDataValuesBeforeSync || false + ); + const [objectWithPeriod, setObjectWithPeriod] = useState( - period + advancedSettings?.period ? { - period: period.type, - startDate: period.startDate, - endDate: period.endDate, + period: advancedSettings?.period.type, + startDate: advancedSettings?.period.startDate, + endDate: advancedSettings?.period.endDate, } : undefined ); @@ -47,7 +60,7 @@ export const PeriodSelectionDialog: React.FC = ({ periodValidation.match({ error: errors => snackbar.error(errors.map(error => error.description).join("\n")), - success: period => onSave(period), + success: period => onSave({ period, deleteDataValuesBeforeSync }), }); } else { onSave(undefined); @@ -76,11 +89,27 @@ export const PeriodSelectionDialog: React.FC = ({ /> {objectWithPeriod && ( - +
    + +
    )} + +
    + +
    ); }; + +const useStyles = makeStyles((theme: Theme) => ({ + period: { + margin: theme.spacing(3, 0), + }, +})); diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx similarity index 98% rename from src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx rename to src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx index 905deec5c..ea62b46d5 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx @@ -118,6 +118,6 @@ export const MSFSettingsDialog: React.FC = ({ const useStyles = makeStyles((theme: Theme) => ({ selector: { - margin: theme.spacing(4, 0, 4, 0), + margin: theme.spacing(3, 0, 3, 0), }, })); diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 563590af2..67a0df23f 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -1,16 +1,18 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@material-ui/core"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Period } from "../../../../domain/common/entities/Period"; import i18n from "../../../../locales"; import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; -import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; +import { + AdvancedSettings, + AdvancedSettingsDialog, +} from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; import { useAppContext } from "../../../react/core/contexts/AppContext"; import { MSFSettings, MSFSettingsDialog, -} from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +} from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; import { executeAggregateData, isGlobalInstance } from "./MSFHomePagePresenter"; export const MSFHomePage: React.FC = () => { @@ -21,7 +23,10 @@ export const MSFHomePage: React.FC = () => { const [syncProgress, setSyncProgress] = useState([]); const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); - const [period, setPeriod] = useState(); + const [advancedSettings, setAdvancedSettings] = useState({ + period: undefined, + deleteDataValuesBeforeSync: false, + }); const [msfSettings, setMsfSettings] = useState({ runAnalytics: "by-sync-rule-settings", @@ -41,11 +46,8 @@ export const MSFHomePage: React.FC = () => { }, []); const handleAggregateData = () => { - executeAggregateData( - compositionRoot, - msfSettings, - progress => setSyncProgress(progress), - period + executeAggregateData(compositionRoot, advancedSettings, msfSettings, progress => + setSyncProgress(progress) ); }; @@ -68,9 +70,9 @@ export const MSFHomePage: React.FC = () => { setShowPeriodDialog(false); }; - const handleSaveAdvancedSettings = (period: Period) => { + const handleSaveAdvancedSettings = (advancedSettings: AdvancedSettings) => { setShowPeriodDialog(false); - setPeriod(period); + setAdvancedSettings(advancedSettings); }; const handleCloseMSFSettings = () => { @@ -152,9 +154,9 @@ export const MSFHomePage: React.FC = () => { {showPeriodDialog && ( - diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 8a4042ed0..833500659 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -1,19 +1,19 @@ import _ from "lodash"; -import { Period } from "../../../../domain/common/entities/Period"; import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; import i18n from "../../../../locales"; import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; import { CompositionRoot } from "../../../CompositionRoot"; -import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +import { AdvancedSettings } from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; +import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; -//TODO: maybe convert to class and presenter to use MVP, MVI pattern +//TODO: maybe convert to class and presenter to use MVP, MVI or BLoC pattern export async function executeAggregateData( compositionRoot: CompositionRoot, + advancedSettings: AdvancedSettings, msfSettings: MSFSettings, - onProgressChange: (progress: string[]) => void, - period?: Period + onProgressChange: (progress: string[]) => void ) { let syncProgress: string[] = [i18n.t(`Starting Aggregate Data...`)]; @@ -55,7 +55,12 @@ export async function executeAggregateData( } for (const syncRule of rulesWithoutRunAnalylics) { - await executeSyncRule(compositionRoot, syncRule, onSyncRuleProgressChange, period); + await executeSyncRule( + compositionRoot, + syncRule, + onSyncRuleProgressChange, + advancedSettings + ); } onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); @@ -69,18 +74,18 @@ async function executeSyncRule( compositionRoot: CompositionRoot, rule: SynchronizationRule, onProgressChange: (event: string) => void, - period?: Period + advancedSettings: AdvancedSettings ): Promise { const { name, builder, id: syncRule, type = "metadata" } = rule; - const newBuilder = period + const newBuilder = advancedSettings.period ? { ...builder, dataParams: { ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, + period: advancedSettings.period.type, + startDate: advancedSettings.period.startDate, + endDate: advancedSettings.period.endDate, }, } : builder; From 2950ebe2196d05fd0a2d269632a5065a41299fd4 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 21 Dec 2020 08:17:22 +0100 Subject: [PATCH 121/163] Move to a use case --- .../usecases/StartApplicationUseCase.ts | 35 +++++++++++++++++++ src/presentation/CompositionRoot.ts | 35 ++----------------- 2 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 src/domain/common/usecases/StartApplicationUseCase.ts diff --git a/src/domain/common/usecases/StartApplicationUseCase.ts b/src/domain/common/usecases/StartApplicationUseCase.ts new file mode 100644 index 000000000..c6454af76 --- /dev/null +++ b/src/domain/common/usecases/StartApplicationUseCase.ts @@ -0,0 +1,35 @@ +import { Namespace } from "../../../data/storage/Namespaces"; +import { UseCase } from "../entities/UseCase"; +import { RepositoryFactory } from "../factories/RepositoryFactory"; +import { Instance, InstanceData } from "../../instance/entities/Instance"; + + +export class StartApplicationUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) { } + + public async execute(): Promise { + await this.verifyLocalInstanceExists(); + } + + private async verifyLocalInstanceExists() { + const storageClient = await this.repositoryFactory + .configRepository(this.localInstance) + .getStorageClient(); + + const objects = await storageClient.listObjectsInCollection( + Namespace.INSTANCES + ); + + if (objects.find(data => data.id === "LOCAL")) + return; + + const localInstance = Instance.build({ + type: "local", + id: "LOCAL", + name: "This instance", + url: "", + }).toObject(); + + await storageClient.saveObjectInCollection(Namespace.INSTANCES, localInstance); + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index ad5d47728..92e080db1 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -10,17 +10,17 @@ import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepositor import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; -import { Namespace } from "../data/storage/Namespaces"; import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; +import { StartApplicationUseCase } from "../domain/common/usecases/StartApplicationUseCase"; import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageConfigUseCase"; import { SetStorageConfigUseCase } from "../domain/config/usecases/SetStorageConfigUseCase"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; -import { Instance, InstanceData } from "../domain/instance/entities/Instance"; +import { Instance } from "../domain/instance/entities/Instance"; import { DeleteInstanceUseCase } from "../domain/instance/usecases/DeleteInstanceUseCase"; import { GetInstanceApiUseCase } from "../domain/instance/usecases/GetInstanceApiUseCase"; import { GetInstanceByIdUseCase } from "../domain/instance/usecases/GetInstanceByIdUseCase"; @@ -121,7 +121,7 @@ export class CompositionRoot { } public async initialize() { - const initializeRoutine = new StartApplicationRoutine( + const initializeRoutine = new StartApplicationUseCase( this.repositoryFactory, this.localInstance ); @@ -388,32 +388,3 @@ function getExecute, Key extends keyof Use return output; }, initialOutput); } - -export class StartApplicationRoutine implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} - - public async execute(): Promise { - await this.verifyLocalInstanceExists(); - } - - private async verifyLocalInstanceExists() { - const storageClient = await this.repositoryFactory - .configRepository(this.localInstance) - .getStorageClient(); - - const objects = await storageClient.listObjectsInCollection( - Namespace.INSTANCES - ); - - if (objects.find(data => data.id === "LOCAL")) return; - - const localInstance = Instance.build({ - type: "local", - id: "LOCAL", - name: "This instance", - url: "", - }).toObject(); - - await storageClient.saveObjectInCollection(Namespace.INSTANCES, localInstance); - } -} From 3c1194c0424f11f617c9795c5a6f67ea579a0ad2 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Mon, 21 Dec 2020 08:22:30 +0100 Subject: [PATCH 122/163] Update method definition --- src/domain/common/usecases/StartApplicationUseCase.ts | 6 ++---- src/presentation/CompositionRoot.ts | 11 +++++------ src/presentation/webapp/WebApp.tsx | 2 +- src/presentation/widget/WidgetApp.tsx | 2 +- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/domain/common/usecases/StartApplicationUseCase.ts b/src/domain/common/usecases/StartApplicationUseCase.ts index c6454af76..c9330d7a6 100644 --- a/src/domain/common/usecases/StartApplicationUseCase.ts +++ b/src/domain/common/usecases/StartApplicationUseCase.ts @@ -3,9 +3,8 @@ import { UseCase } from "../entities/UseCase"; import { RepositoryFactory } from "../factories/RepositoryFactory"; import { Instance, InstanceData } from "../../instance/entities/Instance"; - export class StartApplicationUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) { } + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(): Promise { await this.verifyLocalInstanceExists(); @@ -20,8 +19,7 @@ export class StartApplicationUseCase implements UseCase { Namespace.INSTANCES ); - if (objects.find(data => data.id === "LOCAL")) - return; + if (objects.find(data => data.id === "LOCAL")) return; const localInstance = Instance.build({ type: "local", diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 92e080db1..f9f54d63a 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -120,12 +120,11 @@ export class CompositionRoot { ); } - public async initialize() { - const initializeRoutine = new StartApplicationUseCase( - this.repositoryFactory, - this.localInstance - ); - await initializeRoutine.execute(); + @cache() + public get presentation() { + return getExecute({ + initialize: new StartApplicationUseCase(this.repositoryFactory, this.localInstance), + }); } @cache() diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index 12d0f5bcf..b9bb0573d 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -97,7 +97,7 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); - await compositionRoot.initialize(); + await compositionRoot.presentation.initialize(); setAppContext({ d2: d2 as object, api, compositionRoot }); diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index f61838c4f..b9321ef2f 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -46,7 +46,7 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); - await compositionRoot.initialize(); + await compositionRoot.presentation.initialize(); setAppContext({ d2: d2 as object, api, compositionRoot }); }; From 82ac22182dca1c6429a2e287dd52df37cca6f58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Mon, 21 Dec 2020 13:24:24 +0100 Subject: [PATCH 123/163] Implement delete previous data values before to sync --- i18n/en.pot | 20 +++- i18n/es.po | 18 +++- i18n/fr.po | 18 +++- i18n/pt.po | 18 +++- .../aggregated/AggregatedD2ApiRepository.ts | 4 + .../repositories/AggregatedRepository.ts | 2 + .../usecases/DeleteAggregatedUseCase.ts | 48 ++++++++++ src/presentation/CompositionRoot.ts | 8 ++ .../AdvancedSettingsDialog.tsx | 2 +- .../msf-settings-dialog/MSFSettingsDialog.tsx | 16 ++-- .../pages/MSFHomePagePresenter.ts | 94 ++++++++++++++++++- 11 files changed, 231 insertions(+), 17 deletions(-) create mode 100644 src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts diff --git a/i18n/en.pot b/i18n/en.pot index 42c1907fb..d751ae2e8 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-12-21T05:59:50.425Z\n" -"PO-Revision-Date: 2020-12-21T05:59:50.425Z\n" +"POT-Creation-Date: 2020-12-21T12:23:19.188Z\n" +"PO-Revision-Date: 2020-12-21T12:23:19.188Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -1790,6 +1790,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1805,6 +1810,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index d977f486f..d2fa280b3 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-12-21T05:46:39.583Z\n" +"POT-Creation-Date: 2020-12-21T12:23:19.188Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1797,6 +1797,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1812,6 +1817,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 35f3a7c9d..363fc5c8d 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-12-21T05:46:39.583Z\n" +"POT-Creation-Date: 2020-12-21T12:23:19.188Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1793,6 +1793,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1808,6 +1813,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 35f3a7c9d..363fc5c8d 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-12-21T05:46:39.583Z\n" +"POT-Creation-Date: 2020-12-21T12:23:19.188Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -1793,6 +1793,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1808,6 +1813,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/data/aggregated/AggregatedD2ApiRepository.ts b/src/data/aggregated/AggregatedD2ApiRepository.ts index 29ae78cab..4f7f4c66e 100644 --- a/src/data/aggregated/AggregatedD2ApiRepository.ts +++ b/src/data/aggregated/AggregatedD2ApiRepository.ts @@ -179,6 +179,10 @@ export class AggregatedD2ApiRepository implements AggregatedRepository { return dimensions.map(({ id }) => id); } + async delete(data: AggregatedPackage): Promise { + return await this.save(data, { strategy: "DELETES" }); + } + public async save( data: object, additionalParams: DataImportParams | undefined diff --git a/src/domain/aggregated/repositories/AggregatedRepository.ts b/src/domain/aggregated/repositories/AggregatedRepository.ts index 67ab0d749..d451155de 100644 --- a/src/domain/aggregated/repositories/AggregatedRepository.ts +++ b/src/domain/aggregated/repositories/AggregatedRepository.ts @@ -36,4 +36,6 @@ export interface AggregatedRepository { data: object, additionalParams: DataImportParams | undefined ): Promise; + + delete(data: AggregatedPackage): Promise; } diff --git a/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts b/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts new file mode 100644 index 000000000..d6568c87d --- /dev/null +++ b/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts @@ -0,0 +1,48 @@ +import { cache } from "../../../utils/cache"; +import { Period } from "../../common/entities/Period"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; +import { AggregatedRepository } from "../repositories/AggregatedRepository"; +import { DataSynchronizationParams } from "../types"; +import { buildPeriodFromParams } from "../utils"; + +export class DeleteAggregatedUseCase { + constructor(private repositoryFactory: RepositoryFactory) {} + + async execute( + orgUnitPaths: string[], + dataElementGroupId: string, + period: Period, + instance: Instance + ): Promise { + const aggregatedRepository = this.getAggregatedRepository(instance); + + const [startDate, endDate] = buildPeriodFromParams({ + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }); + + const filters: DataSynchronizationParams = { + startDate: startDate.toDate(), + endDate: endDate.toDate(), + orgUnitPaths, + }; + + const dataValuesToDelete = await aggregatedRepository.getAggregated( + filters, + [], + [dataElementGroupId] + ); + + const result = aggregatedRepository.delete(dataValuesToDelete); + + return result; + } + + @cache() + protected getAggregatedRepository(instance: Instance): AggregatedRepository { + return this.repositoryFactory.aggregatedRepository(instance); + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 0a913edcb..1f7432431 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -12,6 +12,7 @@ import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; +import { DeleteAggregatedUseCase } from "../domain/aggregated/usecases/DeleteAggregatedUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageConfigUseCase"; @@ -114,6 +115,13 @@ export class CompositionRoot { ); } + @cache() + public get aggregated() { + return getExecute({ + delete: new DeleteAggregatedUseCase(this.repositoryFactory), + }); + } + @cache() public get sync() { // TODO: Sync builder should be part of an execute method diff --git a/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx index 46b7c483d..dcf010a2b 100644 --- a/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx @@ -63,7 +63,7 @@ export const AdvancedSettingsDialog: React.FC = ({ success: period => onSave({ period, deleteDataValuesBeforeSync }), }); } else { - onSave(undefined); + onSave({ deleteDataValuesBeforeSync }); } }; diff --git a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx index ea62b46d5..02ec7a3ad 100644 --- a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx @@ -11,7 +11,7 @@ export type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; export type MSFSettings = { runAnalytics: RunAnalyticsSettings; - categoryOptionGroupId?: string; + dataElementGroupId?: string; }; export interface MSFSettingsDialogProps { @@ -28,9 +28,9 @@ export const MSFSettingsDialog: React.FC = ({ const classes = useStyles(); const { compositionRoot } = useAppContext(); const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); - const [catOptionGroups, setCatOptionGroups] = useState[]>([]); - const [selectedCatOptionGroup, setSelectedCatOptionGroup] = useState( - msfSettings.categoryOptionGroupId + const [catOptionGroups, setDataElementGroups] = useState[]>([]); + const [selectedDataElementGroup, setSelectedDataElementGroup] = useState( + msfSettings.dataElementGroupId ); useEffect(() => { @@ -46,7 +46,7 @@ export const MSFSettingsDialog: React.FC = ({ .then(data => { const dataElementGroups = data as DataElementGroup[]; - setCatOptionGroups( + setDataElementGroups( dataElementGroups.map(group => ({ id: group.id, name: group.name })) ); }); @@ -77,7 +77,7 @@ export const MSFSettingsDialog: React.FC = ({ : useSyncRule === "true" ? true : false, - categoryOptionGroupId: selectedCatOptionGroup, + dataElementGroupId: selectedDataElementGroup, }; onSave(msfSettings); @@ -107,8 +107,8 @@ export const MSFSettingsDialog: React.FC = ({
    diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index 833500659..5f0b58b1b 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -1,14 +1,19 @@ import _ from "lodash"; +import moment from "moment"; +import { Period } from "../../../../domain/common/entities/Period"; import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import i18n from "../../../../locales"; import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; +import { availablePeriods } from "../../../../utils/synchronization"; import { CompositionRoot } from "../../../CompositionRoot"; import { AdvancedSettings } from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; //TODO: maybe convert to class and presenter to use MVP, MVI or BLoC pattern +//TODO: maybe create MSF AggregateData use case? export async function executeAggregateData( compositionRoot: CompositionRoot, advancedSettings: AdvancedSettings, @@ -39,6 +44,14 @@ export async function executeAggregateData( ); } + if (advancedSettings.deleteDataValuesBeforeSync && !msfSettings.dataElementGroupId) { + onSyncRuleProgressChange( + i18n.t( + `Deleting previous data values is not possible because data element group is not defined, please contact with your administrator` + ) + ); + } + const eventSyncRules = await getSyncRules(compositionRoot); const runAnalyticsIsRequired = @@ -59,7 +72,8 @@ export async function executeAggregateData( compositionRoot, syncRule, onSyncRuleProgressChange, - advancedSettings + advancedSettings, + msfSettings ); } @@ -74,9 +88,10 @@ async function executeSyncRule( compositionRoot: CompositionRoot, rule: SynchronizationRule, onProgressChange: (event: string) => void, - advancedSettings: AdvancedSettings + advancedSettings: AdvancedSettings, + msfSettings: MSFSettings ): Promise { - const { name, builder, id: syncRule, type = "metadata" } = rule; + const { name, builder, id: syncRule, type = "metadata", targetInstances } = rule; const newBuilder = advancedSettings.period ? { @@ -92,6 +107,16 @@ async function executeSyncRule( onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); + if (advancedSettings.deleteDataValuesBeforeSync && msfSettings.dataElementGroupId) { + await deletePreviousDataValues( + compositionRoot, + targetInstances, + newBuilder, + msfSettings, + onProgressChange + ); + } + const sync = compositionRoot.sync[type]({ ...newBuilder, syncRule }); for await (const { message, syncReport, done } of sync.execute()) { @@ -138,3 +163,66 @@ async function getLastAnalyticsExecution(compositionRoot: CompositionRoot): Prom ? formatDateLong(systemInfo.lastAnalyticsTableSuccess) : i18n.t("never"); } + +async function deletePreviousDataValues( + compositionRoot: CompositionRoot, + targetInstances: string[], + newBuilder: SynchronizationBuilder, + msfSettings: MSFSettings, + onProgressChange: (event: string) => void +) { + const getPeriodText = (period: Period) => { + const formatDate = (date?: Date) => moment(date).format("YYYY-MM-DD"); + + return `${availablePeriods[period.type].name} ${ + period.type === "FIXED" + ? `- start: ${formatDate(period.startDate)} - end: ${formatDate(period.endDate)}` + : "" + }`; + }; + + for (const instanceId of targetInstances) { + const instanceResult = await compositionRoot.instances.getById(instanceId); + + instanceResult.match({ + error: () => + onProgressChange( + i18n.t(`Error retrieving instance {{name}} to delete previoud data values`, { + name: instanceId, + }) + ), + success: instance => { + if (newBuilder.dataParams?.period) { + const periodResult = Period.create({ + type: newBuilder.dataParams.period, + startDate: newBuilder.dataParams.startDate, + endDate: newBuilder.dataParams.endDate, + }); + + periodResult.match({ + error: () => onProgressChange(i18n.t(`Error creating period`)), + success: period => { + if (msfSettings.dataElementGroupId) { + const periodText = getPeriodText(period); + + onProgressChange( + i18n.t( + `Deleting previous data values in target instance {{name}} for period {{period}}...`, + { name: instance.name, period: periodText } + ) + ); + + compositionRoot.aggregated.delete( + newBuilder.dataParams?.orgUnitPaths ?? [], + msfSettings.dataElementGroupId, + period, + instance + ); + } + }, + }); + } + }, + }); + } +} From 433b35c12acfdb4d5e2641932d84dbe2b9432fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 22 Dec 2020 07:17:52 +0100 Subject: [PATCH 124/163] Add skipAudit to delete data values --- i18n/en.pot | 4 ++-- src/data/aggregated/AggregatedD2ApiRepository.ts | 2 +- src/types/d2.ts | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index d751ae2e8..bbabf1d02 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-12-21T12:23:19.188Z\n" -"PO-Revision-Date: 2020-12-21T12:23:19.188Z\n" +"POT-Creation-Date: 2020-12-22T06:07:11.385Z\n" +"PO-Revision-Date: 2020-12-22T06:07:11.385Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/data/aggregated/AggregatedD2ApiRepository.ts b/src/data/aggregated/AggregatedD2ApiRepository.ts index 4f7f4c66e..b14b7fac8 100644 --- a/src/data/aggregated/AggregatedD2ApiRepository.ts +++ b/src/data/aggregated/AggregatedD2ApiRepository.ts @@ -180,7 +180,7 @@ export class AggregatedD2ApiRepository implements AggregatedRepository { } async delete(data: AggregatedPackage): Promise { - return await this.save(data, { strategy: "DELETES" }); + return await this.save(data, { strategy: "DELETES", skipAudit: true }); } public async save( diff --git a/src/types/d2.ts b/src/types/d2.ts index 59118498b..ef63e41c5 100644 --- a/src/types/d2.ts +++ b/src/types/d2.ts @@ -62,6 +62,7 @@ export interface DataImportParams { dryRun?: boolean; preheatCache?: boolean; skipExistingCheck?: boolean; + skipAudit?: boolean; strategy?: "NEW_AND_UPDATES" | "NEW" | "UPDATES" | "DELETES"; format?: "json" | "xml" | "csv" | "pdf" | "adx"; } From 7c397648f2edc5318dad49830a1e5abf47bbd21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 22 Dec 2020 09:16:14 +0100 Subject: [PATCH 125/163] Save dataElementGroup as custom data --- i18n/en.pot | 4 ++-- .../custom-data/CustomDataD2ApiRepository.ts | 21 +++++++++++++++++++ .../common/factories/RepositoryFactory.ts | 10 +++++++++ src/domain/custom-data/entities/CustomData.ts | 1 + .../repository/CustomDataRepository.ts | 11 ++++++++++ .../usecases/GetCustomDataUseCase.ts | 12 +++++++++++ .../usecases/SaveCustomDataUseCase.ts | 14 +++++++++++++ src/presentation/CompositionRoot.ts | 12 +++++++++++ .../msf-aggregate-data/pages/MSFHomePage.tsx | 19 ++++++++++++----- 9 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 src/data/custom-data/CustomDataD2ApiRepository.ts create mode 100644 src/domain/custom-data/entities/CustomData.ts create mode 100644 src/domain/custom-data/repository/CustomDataRepository.ts create mode 100644 src/domain/custom-data/usecases/GetCustomDataUseCase.ts create mode 100644 src/domain/custom-data/usecases/SaveCustomDataUseCase.ts diff --git a/i18n/en.pot b/i18n/en.pot index bbabf1d02..9f6ce783a 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-12-22T06:07:11.385Z\n" -"PO-Revision-Date: 2020-12-22T06:07:11.385Z\n" +"POT-Creation-Date: 2020-12-22T07:59:30.511Z\n" +"PO-Revision-Date: 2020-12-22T07:59:30.511Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/data/custom-data/CustomDataD2ApiRepository.ts b/src/data/custom-data/CustomDataD2ApiRepository.ts new file mode 100644 index 000000000..683c8e41b --- /dev/null +++ b/src/data/custom-data/CustomDataD2ApiRepository.ts @@ -0,0 +1,21 @@ +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; +import { CustomData } from "../../domain/custom-data/entities/CustomData"; +import { CustomDataRepository } from "../../domain/custom-data/repository/CustomDataRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; + +export class CustomDataD2ApiRepository implements CustomDataRepository { + constructor(private configRepository: ConfigRepository) {} + + async get(customDataKey: string): Promise { + const storageClient = await this.getStorageClient(); + return await storageClient.getObject(customDataKey); + } + async save(customDataKey: string, data: CustomData): Promise { + const storageClient = await this.getStorageClient(); + await storageClient.saveObject(customDataKey, data); + } + + private getStorageClient(): Promise { + return this.configRepository.getStorageClient(); + } +} diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 66c4c314b..3c74822ea 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -4,6 +4,7 @@ import { AggregatedRepositoryConstructor, } from "../../aggregated/repositories/AggregatedRepository"; import { ConfigRepositoryConstructor } from "../../config/repositories/ConfigRepository"; +import { CustomDataRepositoryConstructor } from "../../custom-data/repository/CustomDataRepository"; import { EventsRepository, EventsRepositoryConstructor, @@ -118,6 +119,14 @@ export class RepositoryFactory { const config = this.configRepository(instance); return this.get(Repositories.RulesRepository, [config]); } + + @cache() + public customDataRepository(instance: Instance) { + const config = this.configRepository(instance); + return this.get(Repositories.CustomDataRepository, [ + config, + ]); + } } type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; @@ -126,6 +135,7 @@ export const Repositories = { InstanceRepository: "instanceRepository", StoreRepository: "storeRepository", ConfigRepository: "configRepository", + CustomDataRepository: "customDataRepository", DownloadRepository: "downloadRepository", GitHubRepository: "githubRepository", AggregatedRepository: "aggregatedRepository", diff --git a/src/domain/custom-data/entities/CustomData.ts b/src/domain/custom-data/entities/CustomData.ts new file mode 100644 index 000000000..9dc0b9efb --- /dev/null +++ b/src/domain/custom-data/entities/CustomData.ts @@ -0,0 +1 @@ +export type CustomData = Record; diff --git a/src/domain/custom-data/repository/CustomDataRepository.ts b/src/domain/custom-data/repository/CustomDataRepository.ts new file mode 100644 index 000000000..196752a8e --- /dev/null +++ b/src/domain/custom-data/repository/CustomDataRepository.ts @@ -0,0 +1,11 @@ +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; +import { CustomData } from "../entities/CustomData"; + +export interface CustomDataRepositoryConstructor { + new (configRepository: ConfigRepository): CustomDataRepository; +} + +export interface CustomDataRepository { + get(customDataKey: string): Promise; + save(customDataKey: string, data: CustomData): Promise; +} diff --git a/src/domain/custom-data/usecases/GetCustomDataUseCase.ts b/src/domain/custom-data/usecases/GetCustomDataUseCase.ts new file mode 100644 index 000000000..f346d086d --- /dev/null +++ b/src/domain/custom-data/usecases/GetCustomDataUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { CustomData } from "../entities/CustomData"; + +export class GetCustomDataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(customDataKey: string): Promise { + return this.repositoryFactory.customDataRepository(this.localInstance).get(customDataKey); + } +} diff --git a/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts b/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts new file mode 100644 index 000000000..5bb74d41a --- /dev/null +++ b/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts @@ -0,0 +1,14 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { CustomData } from "../entities/CustomData"; + +export class SaveCustomDataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(customDataKey: string, customData: CustomData): Promise { + await this.repositoryFactory + .customDataRepository(this.localInstance) + .save(customDataKey, customData); + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 1f7432431..0618a9a90 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -1,5 +1,6 @@ import { AggregatedD2ApiRepository } from "../data/aggregated/AggregatedD2ApiRepository"; import { ConfigAppRepository } from "../data/config/ConfigAppRepository"; +import { CustomDataD2ApiRepository } from "../data/custom-data/CustomDataD2ApiRepository"; import { EventsD2ApiRepository } from "../data/events/EventsD2ApiRepository"; import { FileD2Repository } from "../data/file/FileD2Repository"; import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepository"; @@ -17,6 +18,8 @@ import { UseCase } from "../domain/common/entities/UseCase"; import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageConfigUseCase"; import { SetStorageConfigUseCase } from "../domain/config/usecases/SetStorageConfigUseCase"; +import { GetCustomDataUseCase } from "../domain/custom-data/usecases/GetCustomDataUseCase"; +import { SaveCustomDataUseCase } from "../domain/custom-data/usecases/SaveCustomDataUseCase"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; import { Instance } from "../domain/instance/entities/Instance"; @@ -95,6 +98,7 @@ export class CompositionRoot { this.repositoryFactory = new RepositoryFactory(encryptionKey); this.repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); this.repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); + this.repositoryFactory.bind(Repositories.CustomDataRepository, CustomDataD2ApiRepository); this.repositoryFactory.bind(Repositories.DownloadRepository, DownloadWebRepository); this.repositoryFactory.bind(Repositories.GitHubRepository, GitHubOctokitRepository); this.repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); @@ -177,6 +181,14 @@ export class CompositionRoot { }); } + @cache() + public get customData() { + return getExecute({ + get: new GetCustomDataUseCase(this.repositoryFactory, this.localInstance), + save: new SaveCustomDataUseCase(this.repositoryFactory, this.localInstance), + }); + } + @cache() public get responsibles() { return getExecute({ diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 67a0df23f..aa4b89f77 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -15,6 +15,8 @@ import { } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; import { executeAggregateData, isGlobalInstance } from "./MSFHomePagePresenter"; +const msfStorage = "msf-storage"; + export const MSFHomePage: React.FC = () => { const classes = useStyles(); const history = useHistory(); @@ -38,12 +40,16 @@ export const MSFHomePage: React.FC = () => { }, [api]); useEffect(() => { - const msfSettings: MSFSettings = isGlobalInstance() - ? { runAnalytics: false } - : { runAnalytics: "by-sync-rule-settings" }; + compositionRoot.customData.get(msfStorage).then(data => { + const runAnalytics = isGlobalInstance() ? false : "by-sync-rule-settings"; - setMsfSettings(msfSettings); - }, []); + if (data) { + setMsfSettings({ runAnalytics, dataElementGroupId: data.dataElementGroupId }); + } else { + setMsfSettings({ runAnalytics }); + } + }); + }, [compositionRoot]); const handleAggregateData = () => { executeAggregateData(compositionRoot, advancedSettings, msfSettings, progress => @@ -82,6 +88,9 @@ export const MSFHomePage: React.FC = () => { const handleSaveMSFSettings = (msfSettings: MSFSettings) => { setShowMSFSettingsDialog(false); setMsfSettings(msfSettings); + compositionRoot.customData.save(msfStorage, { + dataElementGroupId: msfSettings.dataElementGroupId, + }); }; return ( From a29c686c757a875a549f68827cffd3d8db8376c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 22 Dec 2020 09:35:43 +0100 Subject: [PATCH 126/163] Remove delete data values toggle from sync rules --- i18n/en.pot | 10 +++++----- i18n/es.po | 8 ++++---- i18n/fr.po | 8 ++++---- i18n/pt.po | 8 ++++---- src/domain/aggregated/types.ts | 1 - .../SyncParamsSelector.tsx | 19 ------------------- .../sync-wizard/common/SummaryStep.tsx | 12 ------------ 7 files changed, 17 insertions(+), 49 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 9f6ce783a..1dd29ab53 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-12-22T07:59:30.511Z\n" -"PO-Revision-Date: 2020-12-22T07:59:30.511Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" +"PO-Revision-Date: 2020-12-22T08:34:56.287Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -874,9 +874,6 @@ msgstr "" msgid "Run Analytics before sync" msgstr "" -msgid "Delete data values before sync" -msgstr "" - msgid "Type" msgstr "" @@ -1201,6 +1198,9 @@ msgstr "" msgid "Use sync rules periods" msgstr "" +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index d2fa280b3..29cb1dbf3 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-12-21T12:23:19.188Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -879,9 +879,6 @@ msgstr "" msgid "Run Analytics before sync" msgstr "" -msgid "Delete data values before sync" -msgstr "" - msgid "Type" msgstr "" @@ -1207,6 +1204,9 @@ msgstr "" msgid "Use sync rules periods" msgstr "" +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 363fc5c8d..00b71359d 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-12-21T12:23:19.188Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -876,9 +876,6 @@ msgstr "" msgid "Run Analytics before sync" msgstr "" -msgid "Delete data values before sync" -msgstr "" - msgid "Type" msgstr "" @@ -1204,6 +1201,9 @@ msgstr "" msgid "Use sync rules periods" msgstr "" +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 363fc5c8d..00b71359d 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-12-21T12:23:19.188Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -876,9 +876,6 @@ msgstr "" msgid "Run Analytics before sync" msgstr "" -msgid "Delete data values before sync" -msgstr "" - msgid "Type" msgstr "" @@ -1204,6 +1201,9 @@ msgstr "" msgid "Use sync rules periods" msgstr "" +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" diff --git a/src/domain/aggregated/types.ts b/src/domain/aggregated/types.ts index 426ef883b..9610faf3e 100644 --- a/src/domain/aggregated/types.ts +++ b/src/domain/aggregated/types.ts @@ -13,7 +13,6 @@ export interface DataSynchronizationParams extends DataImportParams { enableAggregation?: boolean; aggregationType?: DataSyncAggregation; runAnalytics?: boolean; - deleteDataValuesBeforeSync?: boolean; } export type DataSyncPeriod = diff --git a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx index 007cf8fd5..3753e746e 100644 --- a/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx +++ b/src/presentation/react/core/components/sync-params-selector/SyncParamsSelector.tsx @@ -111,15 +111,6 @@ const SyncParamsSelector: React.FC = ({ ); }; - const changeDeleteDataValuesBeforeSync = (deleteDataValuesBeforeSync: boolean) => { - onChange( - syncRule.updateDataParams({ - ...dataParams, - deleteDataValuesBeforeSync, - }) - ); - }; - return ( @@ -222,16 +213,6 @@ const SyncParamsSelector: React.FC = ({ /> )} - - {syncRule.type === "events" && ( -
    - -
    - )}
    ); }; diff --git a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx index 31e624638..2163d6511 100644 --- a/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx +++ b/src/presentation/react/core/components/sync-wizard/common/SummaryStep.tsx @@ -435,18 +435,6 @@ const SaveStep = ({ syncRule, onCancel }: SyncWizardStepProps) => { } /> - {syncRule.type === "events" && ( -
      - -
    - )} )} From 635e5b33c338091c078d2e3bfc8963671cfe4e37 Mon Sep 17 00:00:00 2001 From: Adrian Quintana Date: Tue, 22 Dec 2020 09:22:13 +0000 Subject: [PATCH 127/163] upgrade d2-ui-comp and d2-api --- package.json | 4 ++-- yarn.lock | 52 ++++++++++++---------------------------------------- 2 files changed, 14 insertions(+), 42 deletions(-) diff --git a/package.json b/package.json index 8f13a6068..a3c0704ce 100644 --- a/package.json +++ b/package.json @@ -28,9 +28,9 @@ "cronstrue": "1.95.0", "cryptr": "4.0.2", "d2": "31.8.1", - "d2-api": "1.4.2-beta.1", + "d2-api": "1.6.0-beta.1", "d2-manifest": "1.0.0", - "d2-ui-components": "2.4.0-beta.1", + "d2-ui-components": "2.4.0-beta.3", "file-saver": "2.0.2", "font-awesome": "4.7.0", "husky": "4.2.5", diff --git a/yarn.lock b/yarn.lock index b7698806e..ea7758d6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1163,7 +1163,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.2.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== @@ -1279,7 +1279,7 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@date-io/core@1.x", "@date-io/core@^1.3.13", "@date-io/core@^1.3.6": +"@date-io/core@^1.3.13", "@date-io/core@^1.3.6": version "1.3.13" resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== @@ -1854,18 +1854,6 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@material-ui/pickers@3.2.10": - version "3.2.10" - resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" - integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w== - dependencies: - "@babel/runtime" "^7.6.0" - "@date-io/core" "1.x" - "@types/styled-jsx" "^2.2.8" - clsx "^1.0.2" - react-transition-group "^4.0.0" - rifm "^0.7.0" - "@material-ui/styles@4.10.0", "@material-ui/styles@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071" @@ -2466,13 +2454,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/styled-jsx@^2.2.8": - version "2.2.8" - resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" - integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== - dependencies: - "@types/react" "*" - "@types/webpack-env@1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" @@ -4498,7 +4479,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.2, clsx@^1.0.4: +clsx@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -5211,10 +5192,10 @@ cypress@4.10.0: url "0.11.0" yauzl "2.10.0" -d2-api@1.4.2-beta.1: - version "1.4.2-beta.1" - resolved "https://registry.yarnpkg.com/d2-api/-/d2-api-1.4.2-beta.1.tgz#cff7dcf3bc2784cfa5fc2113712ee1b24d3f7a84" - integrity sha512-SW456Nu8wDoqPg2f3st/AibUC5Z+3Ps/WITBDNCCtWETUmnstQNNzphuV9djzCrbi1JssPsOJNF1tUHg0/icIQ== +d2-api@1.6.0-beta.1: + version "1.6.0-beta.1" + resolved "https://registry.yarnpkg.com/d2-api/-/d2-api-1.6.0-beta.1.tgz#c9725ce8411b5c26e392cccafe57d7364413c7db" + integrity sha512-Gko/DMim5voYtzp0cclk/773UlHxhnT3Snl+Srp05jOIYGXVIY5Md+Sds7noNBjFOQ1AmY5+E9maEy2/4be0pw== dependencies: "@babel/runtime" "^7.5.4" "@dhis2/d2-i18n" "^1.0.5" @@ -5252,17 +5233,15 @@ d2-manifest@1.0.0: minimist "^1.1.0" readline-sync "^1.4.1" -d2-ui-components@2.4.0-beta.1: - version "2.4.0-beta.1" - resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.1.tgz#c81ccb562f5cca5c80d7a0f0ca536da7d1019bf9" - integrity sha512-xq8Zi1VeLjLRXnVtTCRb8Vfs5q2L9CoMevDvQDK9NaLx70apal6G1fQxL3k/RFEYDtS7fOrsd66PpKPNnKgS2A== +d2-ui-components@2.4.0-beta.3: + version "2.4.0-beta.3" + resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.3.tgz#f423cad9fa5ebe44f018ca1aa75fbabe60d1350d" + integrity sha512-OMN3Yuur1y0+ZFnWMSTAo3KamwBsHImJeMZm7l7fjUHXL1axZEkW8gnr7MwCfQsoSfkSMJmjtm1T0UQHWsOrew== dependencies: "@date-io/core" "^1.3.6" "@date-io/moment" "^1.0.2" "@dhis2/d2-i18n" "1.0.6" "@dhis2/d2-ui-core" "6.3.0" - "@material-ui/icons" "4.9.1" - "@material-ui/pickers" "3.2.10" classnames "^2.2.6" downshift "^5.4.2" lodash "4.17.19" @@ -13118,7 +13097,7 @@ react-transition-group@^1.2.1: prop-types "^15.5.6" warning "^3.0.0" -react-transition-group@^4.0.0, react-transition-group@^4.4.0: +react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -13656,13 +13635,6 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rifm@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" - integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== - dependencies: - "@babel/runtime" "^7.3.1" - rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" From 3498c79b40c77fb7b2d159569e9e4fc4bcb41c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 22 Dec 2020 11:40:59 +0100 Subject: [PATCH 128/163] update d2-ui-components --- i18n/en.pot | 4 ++-- package.json | 2 +- yarn.lock | 49 +++++++------------------------------------------ 3 files changed, 10 insertions(+), 45 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index cf44bbe92..f56ceab9e 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-12-22T10:05:10.427Z\n" -"PO-Revision-Date: 2020-12-22T10:05:10.427Z\n" +"POT-Creation-Date: 2020-12-22T10:37:25.717Z\n" +"PO-Revision-Date: 2020-12-22T10:37:25.717Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/package.json b/package.json index 1e029117a..2738e52c4 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "d2": "31.8.1", "d2-api": "1.6.0-beta.1", "d2-manifest": "1.0.0", - "d2-ui-components": "2.3.1", + "d2-ui-components": "2.4.0-beta.3", "file-saver": "2.0.2", "font-awesome": "4.7.0", "husky": "4.2.5", diff --git a/yarn.lock b/yarn.lock index 21a54f50d..f10302fd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1170,13 +1170,6 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.6.0": - version "7.12.5" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" - integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.4.0", "@babel/template@^7.8.6": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -1286,7 +1279,7 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@date-io/core@1.x", "@date-io/core@^1.3.13", "@date-io/core@^1.3.6": +"@date-io/core@^1.3.13", "@date-io/core@^1.3.6": version "1.3.13" resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== @@ -1861,18 +1854,6 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@material-ui/pickers@3.2.10": - version "3.2.10" - resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" - integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w== - dependencies: - "@babel/runtime" "^7.6.0" - "@date-io/core" "1.x" - "@types/styled-jsx" "^2.2.8" - clsx "^1.0.2" - react-transition-group "^4.0.0" - rifm "^0.7.0" - "@material-ui/styles@4.10.0", "@material-ui/styles@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071" @@ -2473,13 +2454,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== -"@types/styled-jsx@^2.2.8": - version "2.2.8" - resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" - integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== - dependencies: - "@types/react" "*" - "@types/webpack-env@1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" @@ -4505,7 +4479,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.2, clsx@^1.0.4: +clsx@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -5259,17 +5233,15 @@ d2-manifest@1.0.0: minimist "^1.1.0" readline-sync "^1.4.1" -d2-ui-components@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.3.1.tgz#e44b80f6d6eb05dbf9cfb816e9677a81b0185edc" - integrity sha512-DM10dG+giVHjjhQoBLlWluc2Vla9O0YDWvyxZuCTwnQlxUZljDYANHIZtbJuCM6d6yNxet81fY5VVUXH61r4Ew== +d2-ui-components@2.4.0-beta.3: + version "2.4.0-beta.3" + resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.3.tgz#f423cad9fa5ebe44f018ca1aa75fbabe60d1350d" + integrity sha512-OMN3Yuur1y0+ZFnWMSTAo3KamwBsHImJeMZm7l7fjUHXL1axZEkW8gnr7MwCfQsoSfkSMJmjtm1T0UQHWsOrew== dependencies: "@date-io/core" "^1.3.6" "@date-io/moment" "^1.0.2" "@dhis2/d2-i18n" "1.0.6" "@dhis2/d2-ui-core" "6.3.0" - "@material-ui/icons" "4.9.1" - "@material-ui/pickers" "3.2.10" classnames "^2.2.6" downshift "^5.4.2" lodash "4.17.19" @@ -13130,7 +13102,7 @@ react-transition-group@^1.2.1: prop-types "^15.5.6" warning "^3.0.0" -react-transition-group@^4.0.0, react-transition-group@^4.4.0: +react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -13668,13 +13640,6 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= -rifm@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" - integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== - dependencies: - "@babel/runtime" "^7.3.1" - rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" From 2a42b2a4f27ce246af5534a89d0fedc8b5b3dd7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Tue, 22 Dec 2020 12:33:22 +0100 Subject: [PATCH 129/163] Fix d2-ui-compoments bug adding @material-ui/pickers --- i18n/en.pot | 4 ++-- package.json | 1 + yarn.lock | 41 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index f56ceab9e..5f713b5a3 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-12-22T10:37:25.717Z\n" -"PO-Revision-Date: 2020-12-22T10:37:25.717Z\n" +"POT-Creation-Date: 2020-12-22T11:31:25.097Z\n" +"PO-Revision-Date: 2020-12-22T11:31:25.097Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/package.json b/package.json index 2738e52c4..b78cc7ff6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@material-ui/icons": "4.9.1", "@material-ui/lab": "4.0.0-alpha.56", "@material-ui/styles": "4.10.0", + "@material-ui/pickers": "3.2.10", "@octokit/rest": "18.0.0", "axios": "0.19.2", "btoa": "1.2.1", diff --git a/yarn.lock b/yarn.lock index f10302fd8..b98cad1bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1170,6 +1170,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.6.0": + version "7.12.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e" + integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.10.4", "@babel/template@^7.3.3", "@babel/template@^7.4.0", "@babel/template@^7.8.6": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" @@ -1279,7 +1286,7 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@date-io/core@^1.3.13", "@date-io/core@^1.3.6": +"@date-io/core@1.x", "@date-io/core@^1.3.13", "@date-io/core@^1.3.6": version "1.3.13" resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== @@ -1854,6 +1861,18 @@ prop-types "^15.7.2" react-is "^16.8.0" +"@material-ui/pickers@3.2.10": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" + integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w== + dependencies: + "@babel/runtime" "^7.6.0" + "@date-io/core" "1.x" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + "@material-ui/styles@4.10.0", "@material-ui/styles@^4.10.0": version "4.10.0" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071" @@ -2454,6 +2473,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/styled-jsx@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" + integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== + dependencies: + "@types/react" "*" + "@types/webpack-env@1.15.2": version "1.15.2" resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.2.tgz#927997342bb9f4a5185a86e6579a0a18afc33b0a" @@ -4479,7 +4505,7 @@ cloneable-readable@^1.0.0: process-nextick-args "^2.0.0" readable-stream "^2.3.5" -clsx@^1.0.4: +clsx@^1.0.2, clsx@^1.0.4: version "1.1.1" resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== @@ -9653,7 +9679,7 @@ json-stable-stringify@^1.0.1: json-stringify-deterministic@^1.0.1: version "1.0.1" - resolved "https://registry.npmjs.org/json-stringify-deterministic/-/json-stringify-deterministic-1.0.1.tgz#3334798c374d723d46f7ba0e47d6e5e5ac8511f9" + resolved "https://registry.yarnpkg.com/json-stringify-deterministic/-/json-stringify-deterministic-1.0.1.tgz#3334798c374d723d46f7ba0e47d6e5e5ac8511f9" integrity sha512-9Fg0OY3uyzozpvJ8TVbUk09PjzhT7O2Q5kEe30g6OrKhbA/Is92igcx0XDDX7E3yAwnIlUcYLRl+ZkVrBYVP7A== json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: @@ -13102,7 +13128,7 @@ react-transition-group@^1.2.1: prop-types "^15.5.6" warning "^3.0.0" -react-transition-group@^4.4.0: +react-transition-group@^4.0.0, react-transition-group@^4.4.0: version "4.4.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== @@ -13640,6 +13666,13 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rifm@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" + integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== + dependencies: + "@babel/runtime" "^7.3.1" + rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" From bf7109ca534ef6e01618edd65a42211f382d4585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 23 Dec 2020 07:06:03 +0100 Subject: [PATCH 130/163] Fix bug with store repository --- i18n/en.pot | 4 ++-- src/domain/common/factories/RepositoryFactory.ts | 5 +++-- src/domain/packages/usecases/ImportPackageUseCase.ts | 4 +--- src/domain/packages/usecases/ListStorePackagesUseCase.ts | 2 +- src/domain/stores/repositories/StoreRepository.ts | 4 ++-- src/presentation/CompositionRoot.ts | 2 ++ 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/i18n/en.pot b/i18n/en.pot index 5f713b5a3..2e913b928 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-12-22T11:31:25.097Z\n" -"PO-Revision-Date: 2020-12-22T11:31:25.097Z\n" +"POT-Creation-Date: 2020-12-23T06:01:58.871Z\n" +"PO-Revision-Date: 2020-12-23T06:01:58.871Z\n" msgid "Field {{field}} cannot be blank" msgstr "" diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 2f9d7641c..c7c8ce9cb 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -28,7 +28,7 @@ import { type ClassType = new (...args: any[]) => any; export class RepositoryFactory { - constructor(private encryptionKey: string) { } + constructor(private encryptionKey: string) {} private repositories: Map = new Map(); // TODO: TS 4.1 `${RepositoryKeys}-${string}` @@ -65,7 +65,8 @@ export class RepositoryFactory { @cache() public storeRepository(instance: Instance) { - return this.get(Repositories.StoreRepository, [instance]); + const config = this.configRepository(instance); + return this.get(Repositories.StoreRepository, [config]); } @cache() diff --git a/src/domain/packages/usecases/ImportPackageUseCase.ts b/src/domain/packages/usecases/ImportPackageUseCase.ts index f791a9975..90e0fa4a4 100644 --- a/src/domain/packages/usecases/ImportPackageUseCase.ts +++ b/src/domain/packages/usecases/ImportPackageUseCase.ts @@ -178,8 +178,6 @@ export class ImportPackageUseCase implements UseCase { } private async getStorageClient(): Promise { - return await this.repositoryFactory - .configRepository(this.localInstance) - .getStorageClient(); + return await this.repositoryFactory.configRepository(this.localInstance).getStorageClient(); } } diff --git a/src/domain/packages/usecases/ListStorePackagesUseCase.ts b/src/domain/packages/usecases/ListStorePackagesUseCase.ts index 821185a2d..bb3e39b38 100644 --- a/src/domain/packages/usecases/ListStorePackagesUseCase.ts +++ b/src/domain/packages/usecases/ListStorePackagesUseCase.ts @@ -15,7 +15,7 @@ import { moduleFile } from "../repositories/GitHubRepository"; export type ListStorePackagesError = GitHubError | "STORE_NOT_FOUND"; export class ListStorePackagesUseCase implements UseCase { - constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) { } + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} public async execute(storeId: string): Promise> { const store = await this.repositoryFactory diff --git a/src/domain/stores/repositories/StoreRepository.ts b/src/domain/stores/repositories/StoreRepository.ts index 44b4a445c..f00acd265 100644 --- a/src/domain/stores/repositories/StoreRepository.ts +++ b/src/domain/stores/repositories/StoreRepository.ts @@ -1,8 +1,8 @@ -import { Instance } from "../../instance/entities/Instance"; +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; import { Store } from "../entities/Store"; export interface StoreRepositoryConstructor { - new (instance: Instance): StoreRepository; + new (configRepository: ConfigRepository): StoreRepository; } export interface StoreRepository { diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 0a913edcb..dd1432279 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -9,6 +9,7 @@ import { GitHubOctokitRepository } from "../data/packages/GitHubOctokitRepositor import { ReportsD2ApiRepository } from "../data/reports/ReportsD2ApiRepository"; import { RulesD2ApiRepository } from "../data/rules/RulesD2ApiRepository"; import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; +import { StoreD2ApiRepository } from "../data/stores/StoreD2ApiRepository"; import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; @@ -102,6 +103,7 @@ export class CompositionRoot { this.repositoryFactory.bind(Repositories.FileRepository, FileD2Repository); this.repositoryFactory.bind(Repositories.ReportsRepository, ReportsD2ApiRepository); this.repositoryFactory.bind(Repositories.RulesRepository, RulesD2ApiRepository); + this.repositoryFactory.bind(Repositories.StoreRepository, StoreD2ApiRepository); this.repositoryFactory.bind(Repositories.SystemInfoRepository, SystemInfoD2ApiRepository); this.repositoryFactory.bind( Repositories.MetadataRepository, From 5670215a8741119da0cb9d097d7f4ae2bd52e332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 23 Dec 2020 07:30:21 +0100 Subject: [PATCH 131/163] Merge remote-tracking branch 'origin/feature/delete_data_values_before_sync' into feature/verify_events_previous_to_period --- i18n/en.pot | 32 ++++- i18n/es.po | 30 ++++- i18n/fr.po | 30 ++++- i18n/pt.po | 30 ++++- .../aggregated/AggregatedD2ApiRepository.ts | 4 + .../custom-data/CustomDataD2ApiRepository.ts | 21 +++ .../repositories/AggregatedRepository.ts | 2 + .../usecases/DeleteAggregatedUseCase.ts | 48 +++++++ .../common/factories/RepositoryFactory.ts | 10 ++ src/domain/custom-data/entities/CustomData.ts | 1 + .../repository/CustomDataRepository.ts | 11 ++ .../usecases/GetCustomDataUseCase.ts | 12 ++ .../usecases/SaveCustomDataUseCase.ts | 14 ++ src/presentation/CompositionRoot.ts | 20 +++ .../AdvancedSettingsDialog.tsx} | 63 ++++++--- .../msf-Settings/MSFSettingsDialog.tsx | 75 ----------- .../msf-settings-dialog/MSFSettingsDialog.tsx | 123 ++++++++++++++++++ .../msf-aggregate-data/pages/MSFHomePage.tsx | 44 ++++--- .../pages/MSFHomePagePresenter.ts | 122 ++++++++++++++--- src/types/d2.ts | 1 + 20 files changed, 554 insertions(+), 139 deletions(-) create mode 100644 src/data/custom-data/CustomDataD2ApiRepository.ts create mode 100644 src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts create mode 100644 src/domain/custom-data/entities/CustomData.ts create mode 100644 src/domain/custom-data/repository/CustomDataRepository.ts create mode 100644 src/domain/custom-data/usecases/GetCustomDataUseCase.ts create mode 100644 src/domain/custom-data/usecases/SaveCustomDataUseCase.ts rename src/presentation/react/{core/components/period-selection-dialog/PeriodSelectionDialog.tsx => msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx} (53%) delete mode 100644 src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx create mode 100644 src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx diff --git a/i18n/en.pot b/i18n/en.pot index 471cb80fd..f9ab2bafc 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-12-15T08:29:18.609Z\n" -"PO-Revision-Date: 2020-12-15T08:29:18.609Z\n" +"POT-Creation-Date: 2020-12-22T15:15:02.068Z\n" +"PO-Revision-Date: 2020-12-22T15:15:02.068Z\n" msgid "Field {{field}} cannot be blank" msgstr "" @@ -767,9 +767,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1198,6 +1195,12 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" @@ -1213,6 +1216,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1790,6 +1796,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1805,6 +1816,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/es.po b/i18n/es.po index 65d72a271..4675b70fd 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -771,9 +771,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1204,6 +1201,12 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" @@ -1219,6 +1222,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1797,6 +1803,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1812,6 +1823,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/fr.po b/i18n/fr.po index 9e8036406..89da0c63f 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -768,9 +768,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1201,6 +1198,12 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" @@ -1216,6 +1219,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1793,6 +1799,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1808,6 +1819,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/i18n/pt.po b/i18n/pt.po index 9e8036406..89da0c63f 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-12-14T12:09:33.611Z\n" +"POT-Creation-Date: 2020-12-22T08:34:56.287Z\n" "PO-Revision-Date: 2020-07-10T06:53:30.625Z\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -768,9 +768,6 @@ msgstr "" msgid "End date" msgstr "" -msgid "Use sync rules periods" -msgstr "" - msgid "You need to provide a subject" msgstr "" @@ -1201,6 +1198,12 @@ msgstr "" msgid "You do not have assigned any organisation unit" msgstr "" +msgid "Use sync rules periods" +msgstr "" + +msgid "Delete data values before sync" +msgstr "" + msgid "True" msgstr "" @@ -1216,6 +1219,9 @@ msgstr "" msgid "Run Analytics" msgstr "" +msgid "Category Option Group" +msgstr "" + msgid "Metadata Synchronization History" msgstr "" @@ -1793,6 +1799,11 @@ msgstr "" msgid "Run analytics is disabled, last analytics execution: {{lastExecution}}" msgstr "" +msgid "" +"Deleting previous data values is not possible because data element group is " +"not defined, please contact with your administrator" +msgstr "" + msgid "Finished Aggregate Data" msgstr "" @@ -1808,6 +1819,17 @@ msgstr "" msgid "never" msgstr "" +msgid "Error retrieving instance {{name}} to delete previoud data values" +msgstr "" + +msgid "Error creating period" +msgstr "" + +msgid "" +"Deleting previous data values in target instance {{name}} for period " +"{{period}}..." +msgstr "" + msgid "Widget cannot be used until an administrator opens the application" msgstr "" diff --git a/src/data/aggregated/AggregatedD2ApiRepository.ts b/src/data/aggregated/AggregatedD2ApiRepository.ts index 29ae78cab..b14b7fac8 100644 --- a/src/data/aggregated/AggregatedD2ApiRepository.ts +++ b/src/data/aggregated/AggregatedD2ApiRepository.ts @@ -179,6 +179,10 @@ export class AggregatedD2ApiRepository implements AggregatedRepository { return dimensions.map(({ id }) => id); } + async delete(data: AggregatedPackage): Promise { + return await this.save(data, { strategy: "DELETES", skipAudit: true }); + } + public async save( data: object, additionalParams: DataImportParams | undefined diff --git a/src/data/custom-data/CustomDataD2ApiRepository.ts b/src/data/custom-data/CustomDataD2ApiRepository.ts new file mode 100644 index 000000000..683c8e41b --- /dev/null +++ b/src/data/custom-data/CustomDataD2ApiRepository.ts @@ -0,0 +1,21 @@ +import { ConfigRepository } from "../../domain/config/repositories/ConfigRepository"; +import { CustomData } from "../../domain/custom-data/entities/CustomData"; +import { CustomDataRepository } from "../../domain/custom-data/repository/CustomDataRepository"; +import { StorageClient } from "../../domain/storage/repositories/StorageClient"; + +export class CustomDataD2ApiRepository implements CustomDataRepository { + constructor(private configRepository: ConfigRepository) {} + + async get(customDataKey: string): Promise { + const storageClient = await this.getStorageClient(); + return await storageClient.getObject(customDataKey); + } + async save(customDataKey: string, data: CustomData): Promise { + const storageClient = await this.getStorageClient(); + await storageClient.saveObject(customDataKey, data); + } + + private getStorageClient(): Promise { + return this.configRepository.getStorageClient(); + } +} diff --git a/src/domain/aggregated/repositories/AggregatedRepository.ts b/src/domain/aggregated/repositories/AggregatedRepository.ts index 67ab0d749..d451155de 100644 --- a/src/domain/aggregated/repositories/AggregatedRepository.ts +++ b/src/domain/aggregated/repositories/AggregatedRepository.ts @@ -36,4 +36,6 @@ export interface AggregatedRepository { data: object, additionalParams: DataImportParams | undefined ): Promise; + + delete(data: AggregatedPackage): Promise; } diff --git a/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts b/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts new file mode 100644 index 000000000..d6568c87d --- /dev/null +++ b/src/domain/aggregated/usecases/DeleteAggregatedUseCase.ts @@ -0,0 +1,48 @@ +import { cache } from "../../../utils/cache"; +import { Period } from "../../common/entities/Period"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { SynchronizationResult } from "../../reports/entities/SynchronizationResult"; +import { AggregatedRepository } from "../repositories/AggregatedRepository"; +import { DataSynchronizationParams } from "../types"; +import { buildPeriodFromParams } from "../utils"; + +export class DeleteAggregatedUseCase { + constructor(private repositoryFactory: RepositoryFactory) {} + + async execute( + orgUnitPaths: string[], + dataElementGroupId: string, + period: Period, + instance: Instance + ): Promise { + const aggregatedRepository = this.getAggregatedRepository(instance); + + const [startDate, endDate] = buildPeriodFromParams({ + period: period.type, + startDate: period.startDate, + endDate: period.endDate, + }); + + const filters: DataSynchronizationParams = { + startDate: startDate.toDate(), + endDate: endDate.toDate(), + orgUnitPaths, + }; + + const dataValuesToDelete = await aggregatedRepository.getAggregated( + filters, + [], + [dataElementGroupId] + ); + + const result = aggregatedRepository.delete(dataValuesToDelete); + + return result; + } + + @cache() + protected getAggregatedRepository(instance: Instance): AggregatedRepository { + return this.repositoryFactory.aggregatedRepository(instance); + } +} diff --git a/src/domain/common/factories/RepositoryFactory.ts b/src/domain/common/factories/RepositoryFactory.ts index 66c4c314b..3c74822ea 100644 --- a/src/domain/common/factories/RepositoryFactory.ts +++ b/src/domain/common/factories/RepositoryFactory.ts @@ -4,6 +4,7 @@ import { AggregatedRepositoryConstructor, } from "../../aggregated/repositories/AggregatedRepository"; import { ConfigRepositoryConstructor } from "../../config/repositories/ConfigRepository"; +import { CustomDataRepositoryConstructor } from "../../custom-data/repository/CustomDataRepository"; import { EventsRepository, EventsRepositoryConstructor, @@ -118,6 +119,14 @@ export class RepositoryFactory { const config = this.configRepository(instance); return this.get(Repositories.RulesRepository, [config]); } + + @cache() + public customDataRepository(instance: Instance) { + const config = this.configRepository(instance); + return this.get(Repositories.CustomDataRepository, [ + config, + ]); + } } type RepositoryKeys = typeof Repositories[keyof typeof Repositories]; @@ -126,6 +135,7 @@ export const Repositories = { InstanceRepository: "instanceRepository", StoreRepository: "storeRepository", ConfigRepository: "configRepository", + CustomDataRepository: "customDataRepository", DownloadRepository: "downloadRepository", GitHubRepository: "githubRepository", AggregatedRepository: "aggregatedRepository", diff --git a/src/domain/custom-data/entities/CustomData.ts b/src/domain/custom-data/entities/CustomData.ts new file mode 100644 index 000000000..9dc0b9efb --- /dev/null +++ b/src/domain/custom-data/entities/CustomData.ts @@ -0,0 +1 @@ +export type CustomData = Record; diff --git a/src/domain/custom-data/repository/CustomDataRepository.ts b/src/domain/custom-data/repository/CustomDataRepository.ts new file mode 100644 index 000000000..196752a8e --- /dev/null +++ b/src/domain/custom-data/repository/CustomDataRepository.ts @@ -0,0 +1,11 @@ +import { ConfigRepository } from "../../config/repositories/ConfigRepository"; +import { CustomData } from "../entities/CustomData"; + +export interface CustomDataRepositoryConstructor { + new (configRepository: ConfigRepository): CustomDataRepository; +} + +export interface CustomDataRepository { + get(customDataKey: string): Promise; + save(customDataKey: string, data: CustomData): Promise; +} diff --git a/src/domain/custom-data/usecases/GetCustomDataUseCase.ts b/src/domain/custom-data/usecases/GetCustomDataUseCase.ts new file mode 100644 index 000000000..f346d086d --- /dev/null +++ b/src/domain/custom-data/usecases/GetCustomDataUseCase.ts @@ -0,0 +1,12 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { CustomData } from "../entities/CustomData"; + +export class GetCustomDataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(customDataKey: string): Promise { + return this.repositoryFactory.customDataRepository(this.localInstance).get(customDataKey); + } +} diff --git a/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts b/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts new file mode 100644 index 000000000..5bb74d41a --- /dev/null +++ b/src/domain/custom-data/usecases/SaveCustomDataUseCase.ts @@ -0,0 +1,14 @@ +import { UseCase } from "../../common/entities/UseCase"; +import { RepositoryFactory } from "../../common/factories/RepositoryFactory"; +import { Instance } from "../../instance/entities/Instance"; +import { CustomData } from "../entities/CustomData"; + +export class SaveCustomDataUseCase implements UseCase { + constructor(private repositoryFactory: RepositoryFactory, private localInstance: Instance) {} + + public async execute(customDataKey: string, customData: CustomData): Promise { + await this.repositoryFactory + .customDataRepository(this.localInstance) + .save(customDataKey, customData); + } +} diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index 0a913edcb..0618a9a90 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -1,5 +1,6 @@ import { AggregatedD2ApiRepository } from "../data/aggregated/AggregatedD2ApiRepository"; import { ConfigAppRepository } from "../data/config/ConfigAppRepository"; +import { CustomDataD2ApiRepository } from "../data/custom-data/CustomDataD2ApiRepository"; import { EventsD2ApiRepository } from "../data/events/EventsD2ApiRepository"; import { FileD2Repository } from "../data/file/FileD2Repository"; import { InstanceD2ApiRepository } from "../data/instance/InstanceD2ApiRepository"; @@ -12,10 +13,13 @@ import { DownloadWebRepository } from "../data/storage/DownloadWebRepository"; import { SystemInfoD2ApiRepository } from "../data/system-info/SystemInfoD2ApiRepository"; import { TransformationD2ApiRepository } from "../data/transformations/TransformationD2ApiRepository"; import { AggregatedSyncUseCase } from "../domain/aggregated/usecases/AggregatedSyncUseCase"; +import { DeleteAggregatedUseCase } from "../domain/aggregated/usecases/DeleteAggregatedUseCase"; import { UseCase } from "../domain/common/entities/UseCase"; import { Repositories, RepositoryFactory } from "../domain/common/factories/RepositoryFactory"; import { GetStorageConfigUseCase } from "../domain/config/usecases/GetStorageConfigUseCase"; import { SetStorageConfigUseCase } from "../domain/config/usecases/SetStorageConfigUseCase"; +import { GetCustomDataUseCase } from "../domain/custom-data/usecases/GetCustomDataUseCase"; +import { SaveCustomDataUseCase } from "../domain/custom-data/usecases/SaveCustomDataUseCase"; import { EventsSyncUseCase } from "../domain/events/usecases/EventsSyncUseCase"; import { ListEventsUseCase } from "../domain/events/usecases/ListEventsUseCase"; import { Instance } from "../domain/instance/entities/Instance"; @@ -94,6 +98,7 @@ export class CompositionRoot { this.repositoryFactory = new RepositoryFactory(encryptionKey); this.repositoryFactory.bind(Repositories.InstanceRepository, InstanceD2ApiRepository); this.repositoryFactory.bind(Repositories.ConfigRepository, ConfigAppRepository); + this.repositoryFactory.bind(Repositories.CustomDataRepository, CustomDataD2ApiRepository); this.repositoryFactory.bind(Repositories.DownloadRepository, DownloadWebRepository); this.repositoryFactory.bind(Repositories.GitHubRepository, GitHubOctokitRepository); this.repositoryFactory.bind(Repositories.AggregatedRepository, AggregatedD2ApiRepository); @@ -114,6 +119,13 @@ export class CompositionRoot { ); } + @cache() + public get aggregated() { + return getExecute({ + delete: new DeleteAggregatedUseCase(this.repositoryFactory), + }); + } + @cache() public get sync() { // TODO: Sync builder should be part of an execute method @@ -169,6 +181,14 @@ export class CompositionRoot { }); } + @cache() + public get customData() { + return getExecute({ + get: new GetCustomDataUseCase(this.repositoryFactory, this.localInstance), + save: new SaveCustomDataUseCase(this.repositoryFactory, this.localInstance), + }); + } + @cache() public get responsibles() { return getExecute({ diff --git a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx similarity index 53% rename from src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx rename to src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx index 1b4de8e1b..dcf010a2b 100644 --- a/src/presentation/react/core/components/period-selection-dialog/PeriodSelectionDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx @@ -1,30 +1,43 @@ -import { Checkbox, FormControlLabel } from "@material-ui/core"; +import { Checkbox, FormControlLabel, makeStyles, Theme } from "@material-ui/core"; import { ConfirmationDialog, useSnackbar } from "d2-ui-components"; import React, { useState } from "react"; import { Period } from "../../../../../domain/common/entities/Period"; import i18n from "../../../../../locales"; -import PeriodSelection, { ObjectWithPeriod } from "../period-selection/PeriodSelection"; +import PeriodSelection, { + ObjectWithPeriod, +} from "../../../core/components/period-selection/PeriodSelection"; +import { Toggle } from "../../../core/components/toggle/Toggle"; -export interface PeriodSelectionDialogProps { - title?: string; +export type AdvancedSettings = { period?: Period; + deleteDataValuesBeforeSync?: boolean; +}; + +export interface AdvancedSettingsDialogProps { + title?: string; + advancedSettings?: AdvancedSettings; onClose(): void; - onSave(period?: Period): void; + onSave(advancedSettings?: AdvancedSettings): void; } -export const PeriodSelectionDialog: React.FC = ({ +export const AdvancedSettingsDialog: React.FC = ({ title, onClose, onSave, - period, + advancedSettings, }) => { + const classes = useStyles(); const snackbar = useSnackbar(); + const [deleteDataValuesBeforeSync, setDeleteDataValuesBeforeSync] = useState( + advancedSettings?.deleteDataValuesBeforeSync || false + ); + const [objectWithPeriod, setObjectWithPeriod] = useState( - period + advancedSettings?.period ? { - period: period.type, - startDate: period.startDate, - endDate: period.endDate, + period: advancedSettings?.period.type, + startDate: advancedSettings?.period.startDate, + endDate: advancedSettings?.period.endDate, } : undefined ); @@ -47,10 +60,10 @@ export const PeriodSelectionDialog: React.FC = ({ periodValidation.match({ error: errors => snackbar.error(errors.map(error => error.description).join("\n")), - success: period => onSave(period), + success: period => onSave({ period, deleteDataValuesBeforeSync }), }); } else { - onSave(undefined); + onSave({ deleteDataValuesBeforeSync }); } }; @@ -76,11 +89,27 @@ export const PeriodSelectionDialog: React.FC = ({ /> {objectWithPeriod && ( - +
    + +
    )} + +
    + +
    ); }; + +const useStyles = makeStyles((theme: Theme) => ({ + period: { + margin: theme.spacing(3, 0), + }, +})); diff --git a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx deleted file mode 100644 index 405b2b9a7..000000000 --- a/src/presentation/react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { ConfirmationDialog } from "d2-ui-components"; -import React, { useMemo, useState } from "react"; -import i18n from "../../../../../locales"; -import Dropdown from "../../../core/components/dropdown/Dropdown"; - -export type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; - -export type MSFSettings = { - runAnalytics: RunAnalyticsSettings; -}; - -export interface MSFSettingsDialogProps { - msfSettings: MSFSettings; - onClose(): void; - onSave(msfSettings: MSFSettings): void; -} - -export const MSFSettingsDialog: React.FC = ({ - onClose, - onSave, - msfSettings, -}) => { - const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); - - const useSyncRuleItems = useMemo(() => { - return [ - { - id: "true", - name: i18n.t("True"), - }, - { - id: "false", - name: i18n.t("False"), - }, - { - id: "by-sync-rule-settings", - name: i18n.t("Use sync rule settings"), - }, - ]; - }, []); - - const handleSave = () => { - const msfSettings: MSFSettings = { - runAnalytics: - useSyncRule === "by-sync-rule-settings" - ? "by-sync-rule-settings" - : useSyncRule === "true" - ? true - : false, - }; - - onSave(msfSettings); - }; - - return ( - handleSave()} - cancelText={i18n.t("Cancel")} - saveText={i18n.t("Save")} - > - - - ); -}; diff --git a/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx new file mode 100644 index 000000000..02ec7a3ad --- /dev/null +++ b/src/presentation/react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog.tsx @@ -0,0 +1,123 @@ +import { makeStyles, Theme } from "@material-ui/core"; +import { ConfirmationDialog } from "d2-ui-components"; +import React, { useEffect, useMemo, useState } from "react"; +import { DataElementGroup } from "../../../../../domain/metadata/entities/MetadataEntities"; +import i18n from "../../../../../locales"; +import { DataElementGroupModel } from "../../../../../models/dhis/metadata"; +import Dropdown, { DropdownOption } from "../../../core/components/dropdown/Dropdown"; +import { useAppContext } from "../../../core/contexts/AppContext"; + +export type RunAnalyticsSettings = boolean | "by-sync-rule-settings"; + +export type MSFSettings = { + runAnalytics: RunAnalyticsSettings; + dataElementGroupId?: string; +}; + +export interface MSFSettingsDialogProps { + msfSettings: MSFSettings; + onClose(): void; + onSave(msfSettings: MSFSettings): void; +} + +export const MSFSettingsDialog: React.FC = ({ + onClose, + onSave, + msfSettings, +}) => { + const classes = useStyles(); + const { compositionRoot } = useAppContext(); + const [useSyncRule, setUseSyncRule] = useState(msfSettings.runAnalytics.toString()); + const [catOptionGroups, setDataElementGroups] = useState[]>([]); + const [selectedDataElementGroup, setSelectedDataElementGroup] = useState( + msfSettings.dataElementGroupId + ); + + useEffect(() => { + compositionRoot.metadata + .listAll({ + type: DataElementGroupModel.getCollectionName(), + paging: false, + order: { + field: "displayName" as const, + order: "asc" as const, + }, + }) + .then(data => { + const dataElementGroups = data as DataElementGroup[]; + + setDataElementGroups( + dataElementGroups.map(group => ({ id: group.id, name: group.name })) + ); + }); + }, [compositionRoot.metadata]); + + const useSyncRuleItems = useMemo(() => { + return [ + { + id: "true", + name: i18n.t("True"), + }, + { + id: "false", + name: i18n.t("False"), + }, + { + id: "by-sync-rule-settings", + name: i18n.t("Use sync rule settings"), + }, + ]; + }, []); + + const handleSave = () => { + const msfSettings: MSFSettings = { + runAnalytics: + useSyncRule === "by-sync-rule-settings" + ? "by-sync-rule-settings" + : useSyncRule === "true" + ? true + : false, + dataElementGroupId: selectedDataElementGroup, + }; + + onSave(msfSettings); + }; + + return ( + handleSave()} + cancelText={i18n.t("Cancel")} + saveText={i18n.t("Save")} + > +
    + +
    +
    + +
    +
    + ); +}; + +const useStyles = makeStyles((theme: Theme) => ({ + selector: { + margin: theme.spacing(3, 0, 3, 0), + }, +})); diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index 9c9efe92f..db3571e99 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -2,18 +2,22 @@ import { Box, Button, List, makeStyles, Paper, Theme, Typography } from "@materi import { ConfirmationDialog } from "d2-ui-components"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; -import { Period } from "../../../../domain/common/entities/Period"; import i18n from "../../../../locales"; import { isGlobalAdmin } from "../../../../utils/permissions"; import PageHeader from "../../../react/core/components/page-header/PageHeader"; -import { PeriodSelectionDialog } from "../../../react/core/components/period-selection-dialog/PeriodSelectionDialog"; +import { + AdvancedSettings, + AdvancedSettingsDialog, +} from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; import { useAppContext } from "../../../react/core/contexts/AppContext"; import { MSFSettings, MSFSettingsDialog, -} from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +} from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; import { executeAggregateData, isGlobalInstance } from "./MSFHomePagePresenter"; +const msfStorage = "msf-storage"; + export const MSFHomePage: React.FC = () => { const classes = useStyles(); const history = useHistory(); @@ -22,8 +26,11 @@ export const MSFHomePage: React.FC = () => { const [syncProgress, setSyncProgress] = useState([]); const [showPeriodDialog, setShowPeriodDialog] = useState(false); const [showMSFSettingsDialog, setShowMSFSettingsDialog] = useState(false); - const [period, setPeriod] = useState(); const [msfValidationErrors, setMsfValidationErrors] = useState(); + const [advancedSettings, setAdvancedSettings] = useState({ + period: undefined, + deleteDataValuesBeforeSync: false, + }); const [msfSettings, setMsfSettings] = useState({ runAnalytics: "by-sync-rule-settings", @@ -35,21 +42,25 @@ export const MSFHomePage: React.FC = () => { }, [api]); useEffect(() => { - const msfSettings: MSFSettings = isGlobalInstance() - ? { runAnalytics: false } - : { runAnalytics: "by-sync-rule-settings" }; + compositionRoot.customData.get(msfStorage).then(data => { + const runAnalytics = isGlobalInstance() ? false : "by-sync-rule-settings"; - setMsfSettings(msfSettings); - }, []); + if (data) { + setMsfSettings({ runAnalytics, dataElementGroupId: data.dataElementGroupId }); + } else { + setMsfSettings({ runAnalytics }); + } + }); + }, [compositionRoot]); const handleAggregateData = (validateRequired: boolean) => { executeAggregateData( compositionRoot, + advancedSettings, msfSettings, validateRequired, progress => setSyncProgress(progress), - errors => setMsfValidationErrors(errors), - period + errors => setMsfValidationErrors(errors) ); }; @@ -72,9 +83,9 @@ export const MSFHomePage: React.FC = () => { setShowPeriodDialog(false); }; - const handleSaveAdvancedSettings = (period: Period) => { + const handleSaveAdvancedSettings = (advancedSettings: AdvancedSettings) => { setShowPeriodDialog(false); - setPeriod(period); + setAdvancedSettings(advancedSettings); }; const handleCloseMSFSettings = () => { @@ -84,6 +95,9 @@ export const MSFHomePage: React.FC = () => { const handleSaveMSFSettings = (msfSettings: MSFSettings) => { setShowMSFSettingsDialog(false); setMsfSettings(msfSettings); + compositionRoot.customData.save(msfStorage, { + dataElementGroupId: msfSettings.dataElementGroupId, + }); }; return ( @@ -156,9 +170,9 @@ export const MSFHomePage: React.FC = () => { {showPeriodDialog && ( - diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts index ea9c3f83b..cbb8806f2 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePagePresenter.ts @@ -1,25 +1,30 @@ import _ from "lodash"; +import moment from "moment"; import { Period } from "../../../../domain/common/entities/Period"; import { PublicInstance } from "../../../../domain/instance/entities/Instance"; import { SynchronizationRule } from "../../../../domain/rules/entities/SynchronizationRule"; import { Store } from "../../../../domain/stores/entities/Store"; +import { SynchronizationBuilder } from "../../../../domain/synchronization/entities/SynchronizationBuilder"; import i18n from "../../../../locales"; import { executeAnalytics } from "../../../../utils/analytics"; import { promiseMap } from "../../../../utils/common"; import { formatDateLong } from "../../../../utils/date"; import { availablePeriods } from "../../../../utils/synchronization"; import { CompositionRoot } from "../../../CompositionRoot"; -import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-Settings/MSFSettingsDialog"; +import { AdvancedSettings } from "../../../react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog"; +import { MSFSettings } from "../../../react/msf-aggregate-data/components/msf-settings-dialog/MSFSettingsDialog"; -//TODO: maybe convert to class and presenter to use MVP, MVI pattern +//TODO: maybe convert to class and presenter to use MVP, MVI or BLoC pattern +//TODO: maybe create MSF AggregateData use case? export async function executeAggregateData( compositionRoot: CompositionRoot, + advancedSettings: AdvancedSettings, msfSettings: MSFSettings, validateRequired: boolean, onProgressChange: (progress: string[]) => void, onValidationError: (errors: string[]) => void, - period?: Period ) { + validateRequired = false; //TODO: Merge period here const eventSyncRules = await getSyncRules(compositionRoot); @@ -55,6 +60,15 @@ export async function executeAggregateData( }) ); } + if (advancedSettings.deleteDataValuesBeforeSync && !msfSettings.dataElementGroupId) { + onSyncRuleProgressChange( + i18n.t( + `Deleting previous data values is not possible because data element group is not defined, please contact with your administrator` + ) + ); + } + + const eventSyncRules = await getSyncRules(compositionRoot); const runAnalyticsIsRequired = msfSettings.runAnalytics === "by-sync-rule-settings" @@ -70,7 +84,13 @@ export async function executeAggregateData( } for (const syncRule of rulesWithoutRunAnalylics) { - await executeSyncRule(compositionRoot, syncRule, onSyncRuleProgressChange, period); + await executeSyncRule( + compositionRoot, + syncRule, + onSyncRuleProgressChange, + advancedSettings, + msfSettings + ); } onProgressChange([...syncProgress, i18n.t(`Finished Aggregate Data`)]); @@ -109,24 +129,35 @@ async function executeSyncRule( compositionRoot: CompositionRoot, rule: SynchronizationRule, onProgressChange: (event: string) => void, - period?: Period + advancedSettings: AdvancedSettings, + msfSettings: MSFSettings ): Promise { - const { name, builder, id: syncRule, type = "metadata" } = rule; + const { name, builder, id: syncRule, type = "metadata", targetInstances } = rule; - const newBuilder = period + const newBuilder = advancedSettings.period ? { - ...builder, - dataParams: { - ...builder.dataParams, - period: period.type, - startDate: period.startDate, - endDate: period.endDate, - }, - } + ...builder, + dataParams: { + ...builder.dataParams, + period: advancedSettings.period.type, + startDate: advancedSettings.period.startDate, + endDate: advancedSettings.period.endDate, + }, + } : builder; onProgressChange(i18n.t(`Starting Sync Rule {{name}} ...`, { name })); + if (advancedSettings.deleteDataValuesBeforeSync && msfSettings.dataElementGroupId) { + await deletePreviousDataValues( + compositionRoot, + targetInstances, + newBuilder, + msfSettings, + onProgressChange + ); + } + const sync = compositionRoot.sync[type]({ ...newBuilder, syncRule }); for await (const { message, syncReport, done } of sync.execute()) { @@ -198,3 +229,64 @@ const getOriginName = (source: PublicInstance | Store) => { return instance.name; } }; +async function deletePreviousDataValues( + compositionRoot: CompositionRoot, + targetInstances: string[], + newBuilder: SynchronizationBuilder, + msfSettings: MSFSettings, + onProgressChange: (event: string) => void +) { + const getPeriodText = (period: Period) => { + const formatDate = (date?: Date) => moment(date).format("YYYY-MM-DD"); + + return `${availablePeriods[period.type].name} ${period.type === "FIXED" + ? `- start: ${formatDate(period.startDate)} - end: ${formatDate(period.endDate)}` + : "" + }`; + }; + + for (const instanceId of targetInstances) { + const instanceResult = await compositionRoot.instances.getById(instanceId); + + instanceResult.match({ + error: () => + onProgressChange( + i18n.t(`Error retrieving instance {{name}} to delete previoud data values`, { + name: instanceId, + }) + ), + success: instance => { + if (newBuilder.dataParams?.period) { + const periodResult = Period.create({ + type: newBuilder.dataParams.period, + startDate: newBuilder.dataParams.startDate, + endDate: newBuilder.dataParams.endDate, + }); + + periodResult.match({ + error: () => onProgressChange(i18n.t(`Error creating period`)), + success: period => { + if (msfSettings.dataElementGroupId) { + const periodText = getPeriodText(period); + + onProgressChange( + i18n.t( + `Deleting previous data values in target instance {{name}} for period {{period}}...`, + { name: instance.name, period: periodText } + ) + ); + + compositionRoot.aggregated.delete( + newBuilder.dataParams?.orgUnitPaths ?? [], + msfSettings.dataElementGroupId, + period, + instance + ); + } + }, + }); + } + }, + }); + } +} diff --git a/src/types/d2.ts b/src/types/d2.ts index 59118498b..ef63e41c5 100644 --- a/src/types/d2.ts +++ b/src/types/d2.ts @@ -62,6 +62,7 @@ export interface DataImportParams { dryRun?: boolean; preheatCache?: boolean; skipExistingCheck?: boolean; + skipAudit?: boolean; strategy?: "NEW_AND_UPDATES" | "NEW" | "UPDATES" | "DELETES"; format?: "json" | "xml" | "csv" | "pdf" | "adx"; } From c0af1079ce75d604344a3a94ac113c8cf2704aae Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 23 Dec 2020 07:44:14 +0100 Subject: [PATCH 132/163] Rename wrapper --- src/presentation/CompositionRoot.ts | 2 +- src/presentation/webapp/WebApp.tsx | 2 +- src/presentation/widget/WidgetApp.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/presentation/CompositionRoot.ts b/src/presentation/CompositionRoot.ts index f9f54d63a..06f853a02 100644 --- a/src/presentation/CompositionRoot.ts +++ b/src/presentation/CompositionRoot.ts @@ -121,7 +121,7 @@ export class CompositionRoot { } @cache() - public get presentation() { + public get app() { return getExecute({ initialize: new StartApplicationUseCase(this.repositoryFactory, this.localInstance), }); diff --git a/src/presentation/webapp/WebApp.tsx b/src/presentation/webapp/WebApp.tsx index b9bb0573d..0da4d758f 100644 --- a/src/presentation/webapp/WebApp.tsx +++ b/src/presentation/webapp/WebApp.tsx @@ -97,7 +97,7 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); - await compositionRoot.presentation.initialize(); + await compositionRoot.app.initialize(); setAppContext({ d2: d2 as object, api, compositionRoot }); diff --git a/src/presentation/widget/WidgetApp.tsx b/src/presentation/widget/WidgetApp.tsx index b9321ef2f..91119413e 100644 --- a/src/presentation/widget/WidgetApp.tsx +++ b/src/presentation/widget/WidgetApp.tsx @@ -46,7 +46,7 @@ const App = () => { }); const compositionRoot = new CompositionRoot(instance, encryptionKey); - await compositionRoot.presentation.initialize(); + await compositionRoot.app.initialize(); setAppContext({ d2: d2 as object, api, compositionRoot }); }; From 91780b6f9d61b55d49866416fa0c80fd8bf18739 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 23 Dec 2020 08:34:54 +0100 Subject: [PATCH 133/163] Bump d2-ui-components beta --- package.json | 5 ++--- yarn.lock | 9 +++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index f3c9f0476..f196e1a3b 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,6 @@ "@material-ui/icons": "4.9.1", "@material-ui/lab": "4.0.0-alpha.56", "@material-ui/styles": "4.10.0", - "@material-ui/pickers": "3.2.10", "@octokit/rest": "18.0.0", "axios": "0.19.2", "btoa": "1.2.1", @@ -31,7 +30,7 @@ "d2": "31.8.1", "d2-api": "1.6.0-beta.1", "d2-manifest": "1.0.0", - "d2-ui-components": "2.4.0-beta.3", + "d2-ui-components": "2.4.0-beta.4", "file-saver": "2.0.2", "font-awesome": "4.7.0", "husky": "4.2.5", @@ -148,4 +147,4 @@ } } } -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index b98cad1bf..03042da35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5259,15 +5259,16 @@ d2-manifest@1.0.0: minimist "^1.1.0" readline-sync "^1.4.1" -d2-ui-components@2.4.0-beta.3: - version "2.4.0-beta.3" - resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.3.tgz#f423cad9fa5ebe44f018ca1aa75fbabe60d1350d" - integrity sha512-OMN3Yuur1y0+ZFnWMSTAo3KamwBsHImJeMZm7l7fjUHXL1axZEkW8gnr7MwCfQsoSfkSMJmjtm1T0UQHWsOrew== +d2-ui-components@2.4.0-beta.4: + version "2.4.0-beta.4" + resolved "https://registry.yarnpkg.com/d2-ui-components/-/d2-ui-components-2.4.0-beta.4.tgz#30fe94194eb539e60090bf5e3fc4b993f2c3f761" + integrity sha512-cgaMeJxAg/nEeEpLwBhJqtqVXu7eDkCiu1xjEb5cxZrPe3LbVR8PpmLdJtEeMkzZvkyVDuDu8qyfWABHxFmyWg== dependencies: "@date-io/core" "^1.3.6" "@date-io/moment" "^1.0.2" "@dhis2/d2-i18n" "1.0.6" "@dhis2/d2-ui-core" "6.3.0" + "@material-ui/pickers" "3.2.10" classnames "^2.2.6" downshift "^5.4.2" lodash "4.17.19" From 36f9bfe7a5ae9569a53d87c96e7c54ca12686d20 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 23 Dec 2020 08:51:19 +0100 Subject: [PATCH 134/163] Fetch export property to show UI --- src/models/dhis/mapping.ts | 21 +++++++++++++++++++++ src/utils/d2.ts | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/src/models/dhis/mapping.ts b/src/models/dhis/mapping.ts index 6d8acd2a3..46331ad2b 100644 --- a/src/models/dhis/mapping.ts +++ b/src/models/dhis/mapping.ts @@ -3,7 +3,9 @@ import i18n from "../../locales"; import { D2CategoryOptionSchema, D2DataSetSchema, + D2IndicatorSchema, D2OptionSchema, + D2ProgramIndicatorSchema, D2ProgramSchema, SelectedPick, } from "../../types/d2-api"; @@ -11,6 +13,7 @@ import { categoryOptionFields, dataElementFields, dataSetFields, + indicatorFields, optionFields, programFieldsWithDataElements, programFieldsWithIndicators, @@ -49,7 +52,16 @@ export class CategoryOptionMappedModel extends CategoryOptionModel { } export class IndicatorMappedModel extends IndicatorModel { + protected static fields = indicatorFields; protected static mappingType = "aggregatedDataElements"; + + protected static modelTransform = ( + objects: SelectedPick[] + ) => { + return _.map(objects, ({ aggregateExportCategoryOptionCombo = "default", ...rest }) => { + return { ...rest, aggregateExportCategoryOptionCombo }; + }); + }; } export class OptionMappedModel extends OptionModel { @@ -61,7 +73,16 @@ export class OrganisationUnitMappedModel extends OrganisationUnitModel { } export class ProgramIndicatorMappedModel extends ProgramIndicatorModel { + protected static fields = indicatorFields; protected static mappingType = "aggregatedDataElements"; + + protected static modelTransform = ( + objects: SelectedPick[] + ) => { + return _.map(objects, ({ aggregateExportCategoryOptionCombo = "default", ...rest }) => { + return { ...rest, aggregateExportCategoryOptionCombo }; + }); + }; } export class ProgramStageMappedModel extends ProgramStageModel { diff --git a/src/utils/d2.ts b/src/utils/d2.ts index 467d18c84..aaa035449 100644 --- a/src/utils/d2.ts +++ b/src/utils/d2.ts @@ -179,3 +179,8 @@ export const documentFields = { url: include, external: include, }; + +export const indicatorFields = { + ...d2BaseModelFields, + aggregateExportCategoryOptionCombo: include, +}; From c3f65f1b9747d5ae0109ec982a502794d7073031 Mon Sep 17 00:00:00 2001 From: Alexis Rico Date: Wed, 23 Dec 2020 08:51:32 +0100 Subject: [PATCH 135/163] Show related metadata for indicators --- src/presentation/react/core/components/mapping-wizard/Steps.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentation/react/core/components/mapping-wizard/Steps.tsx b/src/presentation/react/core/components/mapping-wizard/Steps.tsx index 3d6a39251..28464d54c 100644 --- a/src/presentation/react/core/components/mapping-wizard/Steps.tsx +++ b/src/presentation/react/core/components/mapping-wizard/Steps.tsx @@ -24,7 +24,7 @@ export const buildModelSteps = (type: string): MappingWizardStepBuilder[] => { component: (props: MappingTableProps) => , models: [CategoryOptionMappedModel], isVisible: (_type: string, element: MetadataType) => { - return !!element.categoryCombo?.id; + return !!element.categoryCombo?.id || !!element.aggregateExportCategoryOptionCombo; }, }, options: { From 86863eb887622b09059b2f5770a3b4af9e7f1770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Sa=CC=81nchez?= Date: Wed, 23 Dec 2020 08:55:25 +0100 Subject: [PATCH 136/163] Add checkInPreviousPeriods in advanced settings --- .../AdvancedSettingsDialog.tsx | 18 ++++- .../msf-aggregate-data/pages/MSFHomePage.tsx | 11 +-- .../pages/MSFHomePagePresenter.ts | 79 ++++++++----------- 3 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx index dcf010a2b..d5f872a3e 100644 --- a/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx +++ b/src/presentation/react/msf-aggregate-data/components/advanced-settings-dialog/AdvancedSettingsDialog.tsx @@ -11,6 +11,7 @@ import { Toggle } from "../../../core/components/toggle/Toggle"; export type AdvancedSettings = { period?: Period; deleteDataValuesBeforeSync?: boolean; + checkInPreviousPeriods?: boolean; }; export interface AdvancedSettingsDialogProps { @@ -32,6 +33,10 @@ export const AdvancedSettingsDialog: React.FC = ({ advancedSettings?.deleteDataValuesBeforeSync || false ); + const [checkInPreviousPeriods, setCheckInPreviousPeriods] = useState( + advancedSettings?.checkInPreviousPeriods || false + ); + const [objectWithPeriod, setObjectWithPeriod] = useState( advancedSettings?.period ? { @@ -60,10 +65,11 @@ export const AdvancedSettingsDialog: React.FC = ({ periodValidation.match({ error: errors => snackbar.error(errors.map(error => error.description).join("\n")), - success: period => onSave({ period, deleteDataValuesBeforeSync }), + success: period => + onSave({ period, deleteDataValuesBeforeSync, checkInPreviousPeriods }), }); } else { - onSave({ deleteDataValuesBeforeSync }); + onSave({ deleteDataValuesBeforeSync, checkInPreviousPeriods }); } }; @@ -104,6 +110,14 @@ export const AdvancedSettingsDialog: React.FC = ({ value={deleteDataValuesBeforeSync} /> + +
    + +
    ); }; diff --git a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx index db3571e99..0d6e5ae81 100644 --- a/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx +++ b/src/presentation/webapp/msf-aggregate-data/pages/MSFHomePage.tsx @@ -53,12 +53,13 @@ export const MSFHomePage: React.FC = () => { }); }, [compositionRoot]); - const handleAggregateData = (validateRequired: boolean) => { + const handleAggregateData = (skipCheckInPreviousPeriods?: boolean) => { executeAggregateData( compositionRoot, - advancedSettings, + skipCheckInPreviousPeriods + ? { ...advancedSettings, checkInPreviousPeriods: false } + : advancedSettings, msfSettings, - validateRequired, progress => setSyncProgress(progress), errors => setMsfValidationErrors(errors) ); @@ -107,7 +108,7 @@ export const MSFHomePage: React.FC = () => {